3

I want to allow users of my Android app to export the SQLite database file for content they create. My current solution copies the file to private storage (/data/data/com.package.name/files/Content.db), then creates a URI for this file and opens the Share dialog. This is working, allowing me to export the database file using Dropbox, for example. Here is the code I'm using, partially adapted from https://stackoverflow.com/a/2661882 -

private void exportContent() {
    copyContentToPrivateStorage();

    Intent intent = new Intent(Intent.ACTION_SEND);
    intent.setType("application/octet-stream");

    Uri uri = new FileProvider().getDatabaseURI(this);

    intent.putExtra(Intent.EXTRA_STREAM, uri);
    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

    startActivity(Intent.createChooser(intent, "Backup via:"));
}

private void copyContentToPrivateStorage() {
    // From https://stackoverflow.com/a/2661882
    try {
        File data = Environment.getDataDirectory();
        File sd = getFilesDir();

        if (sd.canWrite()) {
            String currentDBPath = "//data//com.package.name//databases//Content.db";
            String backupDBPath = "Content.db";
            File currentDB = new File(data, currentDBPath);
            File backupDB = new File(sd, backupDBPath);

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

public class FileProvider extends android.support.v4.content.FileProvider {

    public Uri getDatabaseURI(Context c) {
        File exportFile = new File(c.getFilesDir(), "Content.db");
        Uri uri = getUriForFile(c, "com.package.name.fileprovider", exportFile);

        c.grantUriPermission("*", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);

        return uri;
    }
}

It seems like I should be able to directly create a URI from the existing database path, instead of doing an intermediate copy. Is there a way to do this?

I could keep doing the intermediate copy, but I believe it would be bad practice to leave the second copy of the database in the data directory longer than necessary. Is there a way to clean it up and delete it after the chosen app has finished using the URI to share the file?

Community
  • 1
  • 1
Patrick
  • 1,227
  • 14
  • 17
  • sharing data between your apps , is that your expectation ?? – Heshan Sandeepa Mar 31 '15 at 18:44
  • This sounds like a really bad idea. You should use Contentproviders. – wvdz Mar 31 '15 at 18:45
  • ContentProviders would be great if I just wanted to give another app access to some of the data. In this case, I want to send the actual SQLite database file. This is primarily for the purposes of backup and exporting the data to another device. I may eventually head towards a cloud solution as well, but I also want to allow users to have their own copy of the database file backed up on their personal storage. – Patrick Apr 01 '15 at 18:30
  • @Patrick - did you ever get this working? I'm trying to do the same thing via a custom ContentProvider that references the database file directly, but not having any luck so far. It's incredible how little information I can find on this – Neil C. Obremski Feb 12 '16 at 15:01
  • @NeilC.Obremski I did! I posted it and accepted my answer. – Patrick Feb 21 '16 at 05:46

4 Answers4

11

I solved this on my own. I'm documenting it here, per Neil's request.

This is where I launch the export/backup from my activity:

public class MyActivity {

    private void exportUserContent() {
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType("application/octet-stream");

        Uri uri = new FileProvider().getDatabaseURI(this);

        intent.putExtra(Intent.EXTRA_STREAM, uri);

        startActivity(Intent.createChooser(intent, "Backup via:"));
    }
    
}

The FileProvider:

public class FileProvider extends android.support.v4.content.FileProvider {

    public Uri getDatabaseURI(Context c) {
        // https://developer.android.com/reference/android/support/v4/content/FileProvider.html
        // old approach that worked until 2020-ish
        // File data = Environment.getDataDirectory();
        // String dbName = "UserContent.db";
        // String currentDBPath = "//data//com.url.myapp//databases//" + dbName;

        // File exportFile = new File(data, currentDBPath);
        File exportFile = c.getDatabasePath(dbName); // new approach

        return getFileUri(c, exportFile);
    }

    public Uri getFileUri(Context c, File f){
        return getUriForFile(c, "com.url.myapp.fileprovider", f);
    }

}

Inside AndroidManifest.xml:

<manifest ...>
    <application ...>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.url.myapp.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>

Inside \app\src\main\res\xml\filepaths.xml (I think the first entry is the relevant one, but I'll include the whole file for completeness):

<paths>
    <files-path
        path="../databases/"
        name="mydatabases"/>

    <files-path
        path=""
        name="migrations"/>

    <external-path
        path=""
        name="external"/>
</paths>
Patrick
  • 1,227
  • 14
  • 17
  • Hey, i'm trying to implement this in my app. But how do you call the methodexportUserContent()? Did you create a button for it? – alpitaka Jul 20 '20 at 17:33
  • @alpitaka yeah, in my case, I designed a workflow that ultimately allows the user to click a button that triggers this operation. So I call button.setOnClickListener(...) and inside that, I call exportUserContent(). – Patrick Jul 29 '20 at 00:02
0

Share the SQLite database using content providers. This tutorial can guide you more on SQLite database and content provider: Android SQLite DB and Content Provider

Iqbal S
  • 1,156
  • 10
  • 16
0

Here's how I solved this with a custom ContentProvider:

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;

import java.io.File;
import java.io.FileNotFoundException;

/**
 * ContentProvider to share SQLite database
 *
 * Credit: https://android.googlesource.com/platform/frameworks/support/+/android-support-lib-19.1.0/v4/java/android/support/v4/content/FileProvider.java
 */
public class MyProvider extends ContentProvider {

    private final File file = new File("/data/data/com.example.provider/databases", "mydatabase.db");

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        if (projection == null) {
            projection = new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
        }

        String[] cols = new String[projection.length];
        Object[] values = new Object[projection.length];
        int i = 0;
        for (String col : projection) {
            if (OpenableColumns.DISPLAY_NAME.equals(col)) {
                cols[i] = OpenableColumns.DISPLAY_NAME;
                values[i++] = file.getName();
            } else if (OpenableColumns.SIZE.equals(col)) {
                cols[i] = OpenableColumns.SIZE;
                values[i++] = file.length();
            }
        }
        cols = copyOf(cols, i);
        values = copyOf(values, i);
        final MatrixCursor cursor = new MatrixCursor(cols, 1);
        cursor.addRow(values);
        return cursor;
    }

    @Override
    public String getType(Uri uri) {
        return "application/octet-stream";
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        throw new UnsupportedOperationException("No external inserts");
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("No external updates");
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    }

    private static String[] copyOf(String[] original, int newLength) {
        final String[] result = new String[newLength];
        System.arraycopy(original, 0, result, 0, newLength);
        return result;
    }

    private static Object[] copyOf(Object[] original, int newLength) {
        final Object[] result = new Object[newLength];
        System.arraycopy(original, 0, result, 0, newLength);
        return result;
    }
}

Then in the manifest:

<provider
    android:name="com.example.appname.MyProvider"
    android:authorities="com.example.provider">
</provider>
Neil C. Obremski
  • 18,696
  • 24
  • 83
  • 112
0

here is kotlin code and some improvements for androidX

 private fun exportUserContent() {
        val intent = Intent(Intent.ACTION_SEND)
        intent.type = "application/octet-stream"

        val dbName = "database_name.db";
        val exportFile: File = getDatabasePath(dbName)

        val uri = FileProvider.getUriForFile(
            this@MainActivity, BuildConfig.APPLICATION_ID + ".provider", exportFile
        )

        intent.putExtra(Intent.EXTRA_STREAM, uri)

        startActivity(Intent.createChooser(intent, "Backup via:"))
    }

\app\src\main\res\xml\filepaths.xm

<paths>
    <files-path
        path="../databases/"
        name="mydatabases"/>

    <files-path
        path=""
        name="migrations"/>

    <external-path
        path=""
        name="external"/>
</paths>

manifest

<application ...>
...
 <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths"/>
        </provider>
</application>
Atras VidA
  • 195
  • 8