09 February 2016

Android: Reading SQLite databases selected by the user



Hello people!
After long hours, days and months... I'm back in the saddle again!
I'm here to describe a problem I was facing regarding an android application development. The exception I was fighting is the same as described in this thread: android-could-not-open-database

But my problem was quite different because the file I was trying to open is chosen via an Intent. I mean, the scenario is that an user chooses a database file to overwrite the current one used by the application. This is a manual backup/ recovery procedure where the user first backs-up its data and sends via email or whatever it wants to. After, he/ she grabs the file from the storage (after downloading it from email or whatever) and overwrite the current database.

But this procedure must be obviously validated regarding the file type and the version of the (SQLite) database. The version of the backed-up database must be the same as the current one for the application keep working.
The exception was thrown when I was trying to get the SQLiteDatabase from the chosen file:
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, SQLiteDatabase.OPEN_READONLY);
And the exception was:
SQLiteCantOpenDatabaseException: unknown error (code 14): Could not open database 
It's important to say that this procedure worked fine using an emulator, but it failed in my real device (Motorola XT1058 with Android 5.5 Lollipop).
I had in my manifest the permissions set correctly:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
And the file I was trying to read from SQLite was obtained in the result of the Intent used to get the file from the user's folder:

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case FILE_SELECT_CODE:
                if (resultCode == RESULT_OK) {
                    Uri uri = data.getData();
                    try {
                        InputStream fileInputStream = getContentResolver().openInputStream(data.getData());
                        BackupFile file  = new BackupFile(fileInputStream, uri.getPath());
                        BackupData backupData = new SQLiteBackupData(context, file);
                        DataSecurityEvent dataSecurityEvent = new SQLiteRecovery(backupData);
                        new BackupDataAsyncTask(context, dataSecurityEvent).execute();
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                        ToastOnMiddle.makeText(context, context.getString(R.string.recover_fail), 
                           Integer.parseInt(context.getString(R.string.app_toast_time))).show();
                    }

                }
                break;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

Note that the file used by the recovery validation procedure was the same chosen by the user. After hours I could figure out that this was the problem. My device didn't make use of an external storage and the downloaded files was stored internally. That was the difference between the device and emulator. Then, I changed my code to create a temporary file and fill its content with the content of the file grabbed by the user. This way, the file managed afterwards by the recovery component was located in the temp folder and no permission problem would happen. This is how the code looks like after the refactor:

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case FILE_SELECT_CODE:
                if (resultCode == RESULT_OK) {
                    Uri uri = data.getData();
                    try {
                        InputStream fileInputStream = getContentResolver().openInputStream(data.getData());
                        File tempFile = File.createTempFile(uri.getLastPathSegment(), null, context.getCacheDir());

                        FileChannel originChannel = ((FileInputStream)fileInputStream).getChannel();
                        FileOutputStream destOutput = new FileOutputStream(tempFile);
                        FileChannel destChannel = destOutput.getChannel();
                        originChannel.transferTo(0, fileInputStream.available(), destChannel);

                        BackupFile file  = new BackupFile(new FileInputStream(tempFile), tempFile.getPath());
                        BackupData backupData = new SQLiteBackupData(context, file, new SQLiteBackupDataValidator(file));
                        DataSecurityEvent dataSecurityEvent = new SQLiteRecovery(backupData);
                        List<DataSecurityEvent> events = new ArrayList<DataSecurityEvent>(0);
                        events.add(dataSecurityEvent);
                        new BackupDataAsyncTask(context, events).execute();
                    } catch (IOException e) {
                        e.printStackTrace();
                        ToastOnMiddle.makeText(context, context.getString(R.string.recover_fail), 
                           Integer.parseInt(context.getString(R.string.app_toast_time))).show();
                    } catch (BackupDataException bde) {
                        ToastOnMiddle.makeText(context, getExceptionMessageValue(bde.getMessage()), 
                           Integer.parseInt(context.getString(R.string.app_toast_time))).show();
                    }
                }
                break;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

Note that I used FileChannel to fill the temp file with the selected file content.
I don't know exactly yet why this problem occurs when trying to create or open a SQLite database directly from an internal storage folder. If someone can explain in the comments below, I would be very glad! =)
Lesson learned: Do not get a database file from a file chooser intent and try to use it directly to create or open a SQLite database. You better off using a temporary file instead.



No comments:

Post a Comment