Android 11 在应用之间共享文件

Android 11 sharing file between apps

Android 11 执行了一些存储规则,link 参考:Storage updates in Android 11

用例: 我有 2 个应用程序,应用程序 A 将文件 (.txt) 写入外部存储,应用程序 B 将从外部存储读取文件 而无需用户交互 .但是在 Android 11 上读取/写入时抛出异常,表示权限被拒绝。

所以我做了一些研究,发现只有 MediaStore APIStorage Access Framework 允许访问由创建的文件其他申请,link 参考:Data and file storage overview

但这两种方法都不适合我的用例:

那么有没有其他方法可以访问外部存储上由 Android 11 上的不同应用程序创建的非媒体文件?

尽管我进行了所有研究,但我没有找到解决问题的方法。

感谢您的帮助。

更新

我尝试了 FileProvider 但是当我尝试启动时 activity,它总是显示错误

E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.testapp, PID: 20141 android.content.ActivityNotFoundException: No Activity found to handle Intent { act=com.example.app2.action.RECEIVE dat=content://com.example.testapp.fileprovider/myfiles/default_user.txt flg=0x1 } at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2067) at android.app.Instrumentation.execStartActivity(Instrumentation.java:1727) at android.app.Activity.startActivityForResult(Activity.java:5320)

这就是我从 App 1

启动 App 2 activity 的方式
File filePath = new File(getFilesDir(), "files");
File newFile = new File(filePath, "default_user.txt");
Intent intent = new Intent();
intent.setAction("com.example.app2.action.RECEIVE");
intent.setData(FileProvider.getUriForFile(this, "com.example.testapp.fileprovider", newFile));
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);

应用 1 清单

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<application
    android:allowBackup="true"
    android:requestLegacyExternalStorage="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.TestApp">
    <activity android:name=".StorageActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <activity android:name=".MainActivity">
    </activity>

    <service
        android:name=".service.TestService"
        android:enabled="true"
        android:exported="true" />

    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.example.testapp.fileprovider"
        android:grantUriPermissions="true"
        android:exported="false">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepaths" />
    </provider>
    
</application>

应用 2 清单

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:requestLegacyExternalStorage="true"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.TestApp">
    <activity android:name=".ReceiverActivity">
        <intent-filter>
            <action android:name="com.example.app2.action.RECEIVE"/>

            <category android:name="android.intent.category.DEFAULT"/>
            <data android:scheme="content"
                android:host="com.example.testapp" />
        </intent-filter>
    </activity>
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

您应该使用启动意图直接启动 app2。

                        try {
                            File file = new File( .... );
                            Uri uri = FileProvider.getUriForFile(context, getPackageName() + ".fileprovider", file);

                            String apkPackage = "com.example.app2";

                            Intent intent = context.getPackageManager().getLaunchIntentForPackage(apkPackage);

                            if ( intent==null )
                            {
                                Toast.makeText(context, "Sorry, could not get launch intent for: " + apkPackage, Toast.LENGTH_LONG).show();

                                return;
                            }

                            intent.setAction(Intent.ACTION_VIEW);
                            intent.setDataAndType(uri, mimeType);

                            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

                            context.startActivity(intent);
                        }
                        catch ( IllegalArgumentException e)
                        {
                            e.printStackTrace();
                            Toast.makeText(context, "IllegalArgumentException: " + e.getMessage(), Toast.LENGTH_LONG).show();

                        }
                        catch ( Exception e)
                        {
                            e.printStackTrace();

                            Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show();
                        }

您不需要 app2 清单中的 intent-filters。

接收方可以通过以下方式获取 uri:

Uri uri = getIntent().getData();
import android.content.res.AssetManager;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class MainActivity extends AppCompatActivity {


    PDFiumHelper mPdFiumHelper;
    private void copyAssets() {
        AssetManager assetManager = getAssets();
        String[] files = null;
        try {
            files = assetManager.list("");
        } catch (IOException e) {
            Log.e("tag", "Failed to get asset file list.", e);
        }
        for (String filename : files) {
            InputStream in = null;
            OutputStream out = null;
            try {
                in = assetManager.open(filename);

                String outDir = getFilesDir().getAbsolutePath();

                File outFile = new File(outDir, filename);

                out = new FileOutputStream(outFile);
                copyFile(in, out);
                in.close();
                in = null;
                out.flush();
                out.close();
                out = null;
            } catch (IOException e) {
                Log.e("tag", "Failed to copy asset file: " + filename, e);
            }
        }
    }

    private void copyFile(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[1024];
        int read;
        while ((read = in.read(buffer)) != -1) {
            out.write(buffer, 0, read);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        copyAssets();
        mPdFiumHelper = new PDFiumHelper(this);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
       return mPdFiumHelper.onKeyDown(keyCode,event);
    }
}

帮手class

import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.FileProvider;

import com.github.barteksc.pdfviewer.PDFView;
import com.github.barteksc.pdfviewer.listener.OnErrorListener;
import com.github.barteksc.pdfviewer.listener.OnLoadCompleteListener;
import com.github.barteksc.pdfviewer.util.FitPolicy;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.karumi.dexter.MultiplePermissionsReport;
import com.karumi.dexter.PermissionToken;
import com.karumi.dexter.listener.PermissionRequest;
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;

import java.io.File;
import java.util.Calendar;
import java.util.List;

public class PDFiumHelper implements MultiplePermissionsListener {

    AppCompatActivity mAppCompatActivity;

    public PDFiumHelper(AppCompatActivity mAppCompatActivity) {
        this.mAppCompatActivity = mAppCompatActivity;
        onCreate();
    }
    public PDFiumHelper() {
    }
    Button btnFile1, btnFile2;

    long firstTime;
    PDFView pdfView;
    CommonUtility mCommonUtility;
    boolean hasPermissions;
    FloatingActionButton ibtn_share;
    File localPDFFile;
    String pdfTitle = "Mathematics Paper 2020";
    String pdfExtraText = "this paper is made using the quantum paper.";
    ConstraintLayout root;
    String path1,path2;
    protected void onCreate() {

        path1 = mAppCompatActivity.getFilesDir() + "/SatsangDiksha.pdf";
        path2 = mAppCompatActivity.getFilesDir() + "/sample.pdf";

        btnFile1 = (Button) mAppCompatActivity.findViewById(R.id.btnFile1);
        btnFile2 = (Button) mAppCompatActivity.findViewById(R.id.btnFile2);

        root = mAppCompatActivity.findViewById(R.id.root);

        pdfView = new PDFView(mAppCompatActivity, null);
        pdfView.setLayoutParams(new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        root.addView(pdfView);

        ibtn_share = new FloatingActionButton(mAppCompatActivity);
        ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        int margin = convertDpToPixel(20);
        params.setMargins(margin, margin, margin, margin);
        //this will constrain FAB programmatically as like XML.
        params.bottomToBottom = root.getId();
        params.endToEnd = root.getId();
        ibtn_share.setLayoutParams(params);
        //change FAB icon here
        ibtn_share.setImageResource(R.drawable.ic_share);
        //change background color of FAB here
        ibtn_share.setBackgroundTintList(ColorStateList.valueOf(mAppCompatActivity.getResources().getColor(R.color.purple_500)));
        //FAB icon color can be change from here
        ibtn_share.setColorFilter(Color.WHITE);

        root.addView(ibtn_share);


        btnFile1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadPdfFromFile(path1);
            }
        });
        btnFile2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadPdfFromFile(path2);
            }
        });


        mCommonUtility = new CommonUtility(mAppCompatActivity);
        if (hasPermissions) {
        } else {
            mCommonUtility.askForPermissionBeforeStart(this);
        }

        ibtn_share.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (hasPermissions) {
                    shareFile();
                } else {
                    mCommonUtility.askForPermissionBeforeStart(PDFiumHelper.this);
                }
            }
        });

    }

    boolean flag = false;

    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_DPAD_LEFT)) {
            if (flag) {
                loadPdfFromFile(path1);
                flag = false;
            } else {
                loadPdfFromFile(path2);
                flag = true;
            }
//            Toast.makeText(MainActivity.this, "Left Arrow Pressed", Toast.LENGTH_SHORT).show();
            return false;
        }
        return false;
    }

    public void shareFile() {
        Intent intentShareFile = new Intent(Intent.ACTION_SEND);
        if (localPDFFile.exists()) {
            intentShareFile.setType("application/pdf");
            intentShareFile.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(mAppCompatActivity, BuildConfig.APPLICATION_ID + ".provider", localPDFFile));
            intentShareFile.putExtra(Intent.EXTRA_SUBJECT,
                    pdfTitle);
            intentShareFile.putExtra(Intent.EXTRA_TEXT, pdfExtraText);
            mAppCompatActivity.startActivity(Intent.createChooser(intentShareFile, "Share File using"));
        } else {
            Toast.makeText(mAppCompatActivity, "File not loaded.", Toast.LENGTH_SHORT).show();
        }
    }

    public long showCurrentTime() {
        Calendar c = Calendar.getInstance();
        int hours = c.get(Calendar.HOUR);
        int minutes = c.get(Calendar.MINUTE);
        int seconds = c.get(Calendar.SECOND);
        int mseconds = c.get(Calendar.MILLISECOND);
//        Log.e(TAG, "showCurrentTime: Current Time :- " + hours + ":" + minutes + ":" + seconds + ":" + mseconds);
        return System.currentTimeMillis();
    }


    public void loadPdfFromFile(String path) {
        firstTime = showCurrentTime();
        try {
            localPDFFile = new File(path);
            pdfView.fromFile(localPDFFile)
                    .enableAntialiasing(true)
                    .enableDoubletap(true)
                    .onLoad(new OnLoadCompleteListener() {
                        @Override
                        public void loadComplete(int nbPages) {
                            double timee = firstTime - showCurrentTime();
                            double ms = Math.abs(timee) / 1000;
                            Log.e("===", "onDocumentLoaded: Time Taken :- " + ms + " Seconds");
                            Toast.makeText(mAppCompatActivity, ms + " Seconds", Toast.LENGTH_SHORT).show();
                        }
                    })
                    .onError(new OnErrorListener() {
                        @Override
                        public void onError(Throwable t) {
                            t.printStackTrace();
                            Toast.makeText(mAppCompatActivity, "Load some file.", Toast.LENGTH_SHORT).show();
                        }
                    })
                    .scrollHandle(new DefaultScrollHandle(mAppCompatActivity))
                    .pageFitPolicy(FitPolicy.WIDTH)
                    .autoSpacing(false)
                    .load();
        } catch (Throwable th) {

//            th.printStackTrace();
        }
    }

    @Override
    public void onPermissionsChecked(MultiplePermissionsReport multiplePermissionsReport) {
        // check if all permissions are granted
        if (multiplePermissionsReport.areAllPermissionsGranted()) {
            // do work
            hasPermissions = true;
            if (hasPermissions) {
            }
        }
        if (multiplePermissionsReport.isAnyPermissionPermanentlyDenied()) {
            // permission is denied permenantly, navigate user to app settings
            mCommonUtility.showSettingsDialog();
        }
    }

    @Override
    public void onPermissionRationaleShouldBeShown(List<PermissionRequest> list, PermissionToken permissionToken) {
        mCommonUtility.showPermissionRationale(permissionToken);
    }

    public int convertDpToPixel(int dp) {
        return dp * (mAppCompatActivity.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
    }

    public int convertPixelsToDp(int px) {
        return px / (mAppCompatActivity.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT);
    }

}

提供程序路径文件。

<?xml version="1.0" encoding="utf-8"?>
    <paths>
        <external-path
            name="external"
            path="." />
        <external-files-path
            name="external_files"
            path="." />
        <cache-path
            name="cache"
            path="." />
        <external-cache-path
            name="external_cache"
            path="." />
        <files-path
            name="files"
            path="." />
    </paths>

Android 清单文件。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.qp.pdfiumproject">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:requestLegacyExternalStorage="true"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.PdfiumProject">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <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/provider_paths" />
        </provider>

    </application>

</manifest>

使用Android11,每个应用程序只能在自己的私有目录中读写文件。这适用于 Java 文件 class 和本机代码。这些是我目前知道的解决此限制的方法:

  1. 您可以将 SAF 用于所有文件访问。在某些情况下这是不可能的,例如使用 SQLite 时。 SQLite 只接受数据类型 File,不接受 DocumentFile.
  2. 您可以使用 SAF 将您需要的所有文件复制到您的私人目录,然后使用 File 或本机代码访问它们。如果您想更改它们,您必须随后使用 SAF 将它们复制回它们的原始位置。
  3. 您可以请求 MANAGE_EXTERNAL_STORAGE 许可(为什么是“外部”?!?)。在这种情况下,您无法在 Play 商店中发布您的应用程序。
  4. 您可以使用目标 SDK Android 10. 在这种情况下,您无法在 Play 商店中发布您的应用程序。
  5. 您可以让 Android 相信您的文件(可能是“bla.txt”和“labre.db”)是媒体文件。将它们重命名为“bla.txt.m4a”和“labre.db.jpg”。作为副作用,您的媒体数据库将被不需要的条目污染,即不良音频和不良图片,它们可能会无意中出现在图片或音乐专辑中。

在我看来 Google 可以找到更好的解决方案,不会给开发人员带来太多工作。顺便说一句:SAF 太慢了,即使只是传输原始数据。