从 Android 应用共享 SQLite 数据库,无需中间副本

Share SQLite database from Android app, without intermediate copy

我想允许我的 Android 应用程序的用户为他们创建的内容导出 SQLite 数据库文件。我当前的解决方案将文件复制到私有存储 (/data/data/com.package.name/files/Content.db),然后为该文件创建一个 URI 并打开“共享”对话框。这很有效,例如,允许我使用 Dropbox 导出数据库文件。这是我正在使用的代码,部分改编自 -

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 
    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;
    }
}

看来我应该能够直接从现有数据库路径创建 URI,而不是进行中间复制。有没有办法做到这一点?

我可以继续做中间副本,但我认为将数据库的第二个副本留在数据目录中的时间过长是不好的做法。有没有办法在所选应用完成使用 URI 共享文件后清理并删除它?

使用内容提供商共享 SQLite 数据库。本教程可以指导您更多地了解 SQLite 数据库和内容提供程序:Android SQLite DB and Content Provider

下面是我如何使用自定义 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;
    }
}

然后在清单中:

<provider
    android:name="com.example.appname.MyProvider"
    android:authorities="com.example.provider">
</provider>

我自己解决了这个问题。根据 Neil 的要求,我将其记录在这里。

这是我从 activity 启动 export/backup 的地方:

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:"));
    }
    
}

文件提供者:

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);
    }

}

里面 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>

里面 \app\src\main\res\xml\filepaths.xml (我认为第一个条目是相关的,但为了完整性我会包括整个文件):

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

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

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