6

Below is the working method to Import and Export SQLite database. Its Working just fine in all android versions excluding Android Pie. When I am trying to import in Android pie, it shows successful toast but database is not being restored. Can anybody help me workaround in Android Pie(API 28).

private void importDB() {

    try {
        File sd = Environment.getExternalStorageDirectory();
        File cur_db_pat = new File(this.getDatabasePath(DATABASE_NAME).getAbsolutePath());

        if (sd.canWrite()) {
            String backupDBPath = bac_dir_nam +"/" + DATABASE_NAME;
            File currentDB = new File(sd, backupDBPath);

            FileChannel src = new FileInputStream(currentDB).getChannel();
            FileChannel dst = new FileOutputStream(cur_db_pat).getChannel();
            dst.transferFrom(src, 0, src.size());
            src.close();
            dst.close();
            Toast.makeText(getBaseContext(), cur_db_pat.toString(),
                    Toast.LENGTH_LONG).show();
        }
    } catch (Exception e) {

        Toast.makeText(getBaseContext(), e.toString(), Toast.LENGTH_LONG)
                .show();

    }
}

private void exportDB() {

    try {
        File sd = Environment.getExternalStorageDirectory();
        File cur_db_pat = new File(this.getDatabasePath(DATABASE_NAME).getAbsolutePath());

        if (sd.canWrite()) {
            String backupDBPath = bac_dir_nam+"/" + DATABASE_NAME;
            File backupDB = new File(sd, backupDBPath);

            FileChannel src = new FileInputStream(cur_db_pat).getChannel();
            FileChannel dst = new FileOutputStream(backupDB).getChannel();
            dst.transferFrom(src, 0, src.size());
            src.close();
            dst.close();
            Toast.makeText(getBaseContext(), backupDB.toString(),
                    Toast.LENGTH_LONG).show();

        }
    } catch (Exception e) {

        Toast.makeText(getBaseContext(), e.toString(), Toast.LENGTH_LONG)
                .show();

    }
}

I don't have much experience with file system. So an example would help a lot.

ItsRedwan
  • 173
  • 1
  • 9
  • Have you requested the permission to access the sd-card from the user? See e.g. https://stackoverflow.com/questions/33139754/android-6-0-marshmallow-cannot-write-to-sd-card – Robert Jan 05 '19 at 12:52

3 Answers3

10

In Android Pie+ SQLite has changed to default to using the generally more efficient Write Ahead Logging (WAL) instead of Journal mode.

As such there will be two files with the same name as the database but suffixed with -shm (shared memory file) and -wal (write ahead log) and their presence is what I believe causes the issue(s). Temporary Files Used By SQLite (see 2.2 and 2.3)

One fix would be to disable Write Ahead Logging using use the SQliteDatabase disableWriteAheadLogging method and the previous method would work as before but with the less efficient journal mode.

  • (if using a subclass of SQliteOpenHelper then override the onConfigure method to invoke this method. ) disableWriteAheadLogging.

Another fix is to delete these two files when restoring. To avoid the potential for corruption you have to ensure that the database was adequately checkpointed before making the backup. see PRAGMA checkpoint;

The following is a snippet that deletes these two files when restoring (noting that the backup is assumed to have been taken with adequate checkpointing):-

                    // Added for Android 9+ to delete shm and wal file if they exist
                    File dbshm = new File(dbfile.getPath() + "-shm");
                    File dbwal = new File(dbfile.getPath()+ "-wal");
                    if (dbshm.exists()) {
                        dbshm.delete();
                    }
                    if (dbwal.exists()) {
                        dbwal.delete();
                    }

Another fix would be to additionally backup and subsequently restore the -shm and -wal files.

You may also wish considering the potential benefits of renaming the original files when importing/restoring, checking the new files after they have been copied (e.g. using PRAGMA integrity_check;) if the results indicat no issues then delete the renamed original files, otherwise delete the imported files and rename the original files to their original name, indicating that the import failed.

MikeT
  • 51,415
  • 16
  • 49
  • 68
  • If an old database file is restored, is it save to simply restore the `.db` file without the journal file? Or do I have to make some migration of the `.db` file as well? If so, do you know how I could migrate an old `.db` file with journal mode correctly? – prom85 Jun 26 '19 at 14:45
  • @prom85 Journal mode is a log of the updates that have been applied allowing them to be rolled back/undone and thus the journal is not required. However, WAL is a log of the updates that are yet to be applied and thus the updates in the WAL are needed and hence the need to have them committed/done/applied unless the updates are not needed. As such the -shm and -wal files, if not empty should either be committed (which empties them) or the files should be copied in addition to the main database file. – MikeT Jun 28 '19 at 22:02
  • thanks a lot, I know what the WAL mode files are, but did not yet know what the journal file is for. So moving from journal to WAL mode is save but not the other way round. Before a change from WAL to journal mode, I would need to push all pending changes from the WAL helper files into the database file. – prom85 Jul 01 '19 at 08:31
  • what about if you close db ? looks like wal and shm files are deleted. So isn't better to close db then to copy only one file containing database and same for restore ? – Zhar Jan 04 '20 at 18:13
  • @Zhar yes that's covered by *adeqautely checkpointed*. However, typically this issue is due to the way that the database is checked for existence and unfortunately many people copy/adapt code that does this by opening the database via SQLite (getWritable/readable) which then creates a -wal file so it's that -wal file that is typically causes the issue and is being deleted. Of course the better way is to check for the database file existing. This question doesn't show the check but the symptoms indicate this. – MikeT Jan 04 '20 at 18:47
  • @MikeT What do you think about the answer from Bronz which under yours? – Михаил Jun 27 '20 at 20:14
5

In your class for Db WorkHelper ovverride onOpen() method and set disableWriteAheadLogging then call onOpen() standard, if version of android sdk 28 , sure then old version remain old modality.

@Override
public void onOpen(SQLiteDatabase database) {
    super.onOpen(database);
    if(Build.VERSION.SDK_INT >= 28)
    {
        database.disableWriteAheadLogging();
    }
}

In my case WORK perfect.

Bronz
  • 217
  • 3
  • 7
  • Without being needed to change anything I directly copied the snippet you have supplied. Worked in my case too, thanks a lot – mears Oct 20 '19 at 16:02
  • Once again this answer has proved useful. So hard to find the solution to this problem! – androidneil Aug 19 '20 at 15:38
  • it work, but if you use LiveData + PagedList, data not be refreshed, it will failed with bug: Sqlite no such table: room_table_modification_log.... – Zuy Pham Oct 24 '21 at 03:07
  • I don't know LIVEDATA but I imagine that when you import or export you can easily do it without additional files if it is done in a static moment, with operations not in progress. – Bronz Mar 16 '22 at 00:45
0

Unlike what the other commenters have suggested, you can't rely on the database consisting of a single file after write-ahead logging is disabled, and you can't assume that the -shl and -wal filenames remain correct. This is all an implementation detail of sqlite3 / Android and therefore subject to change at any time (just like the old code broke).

One way of doing this that I expect to continue working is to use sqlite3's .dump command to convert the database into SQL that can later be executed in order to recreate the database.

I haven't tested the following, but expect that something similar to it should work:

// Copyright 2021 Google LLC.
// SPDX-License-Identifier: Apache-2.0

// Untested:
    private byte[] exportDatabase(SQLiteDatabase database) throws IOException {
        Process process = new ProcessBuilder()
                .command("/system/bin/sqlite3", database.getPath(), ".dump")
                .redirectOutput(ProcessBuilder.Redirect.PIPE)
                .start();
        try (InputStream inputStream = process.getInputStream()) {
            // [read the full contents of inputStream and save them somewhere]
            return ByteStreams.toByteArray(inputStream);
        } finally {
            waitForProcess(process);
        }
    }

    private void importDatabase(String databasePath, InputStream backedUpData) throws IOException {
        // restore the database:
        Process process = new ProcessBuilder()
                .command("/system/bin/sqlite3", databasePath)
                .redirectInput(ProcessBuilder.Redirect.PIPE)
                .start();
        try (OutputStream outputStream = process.getOutputStream()) {
            // now write the backed-up contents back to outputStream
            ByteStreams.copy(backedUpData, outputStream);
        } 
        waitForProcess(process);
    }

    private static void waitForProcess(Process process) {
        try {
            process.waitFor();
        } catch (InterruptedException e) {
            // ignore interruption, restore interrupt flag
            Thread.currentThread().interrupt();
        }
    }

Obviously, you'll have to ensure that:

  • The database that you're backing up isn't currently open.
  • The database that you're restoring doesn't already exist.
Tobias
  • 1,107
  • 7
  • 8