Android 11 sharing file between apps

Viewed 5882

Android 11 had enforce some rules for storage, link refer : Storage updates in Android 11

Use Case : I have 2 applications, application A will write file (.txt) into external storage, application B will read the file from external storage without user interaction. But exception was thrown when read / write on Android 11, stated that permission was denied.

So I did some research and found that only MediaStore API and Storage Access Framework allow access files that created by other application, link refer : Data and file storage overview

But both methods are not suitable for my use case:

  • MediaStore API can only access Media files(images, audio files, videos)
  • Storage Access Framework need user interaction

So is there any other way that I can access non-media files on external storage that created by different apps on Android 11?

Despite all my researches, I didn't find a solution to my problem.

Thank you for your help.

Updates

I tried FileProvider but when I tried start activity, it always show error

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)

This is how I start App 2 activity from App 1

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

App 1 Manifest

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

App 2 Manifest

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

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

}

Provider paths file.

<?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 Manifest File.

<?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>

With Android 11, each app can only read and write files in its own private directories. This applies for both Java File class and native code. These are the methods I currently know to work around this restriction:

  1. You can use SAF for all file accesses. In some cases this is not possible, e.g. when using SQLite. SQLite only accepts datatype File, not DocumentFile.
  2. You can use SAF to copy all files you need to your private directories and access them there using File or native code. If you want to change them, you must afterwards use SAF to copy them back to their original location.
  3. You can ask for MANAGE_EXTERNAL_STORAGE permission (why "external"?!?). In that case you cannot publish your application in Play Store.
  4. You can use target SDK Android 10. In that case you cannot publish your application in Play Store.
  5. You can make Android believe that your files (maybe "bla.txt" and "labre.db") are media files. Rename them to "bla.txt.m4a" and "labre.db.jpg". As side effect your media databases will be polluted with unwanted entries, i.e. bad audio and bad pictures, which may inadvertently appear in picture or music albums.

In my opinion Google could have find a better solution that does not cause so much work for developers. BTW: SAF is extreeeeemly slooooow, even when just transferring raw data.

You should directly start app2 using a launch intent for it.

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

You do not need intent-filters in manifest of app2.

The receiving side can get the uri with:

Uri uri = getIntent().getData();
Related