Android 7.0 behavior changes to share files between apps via FileProvider

This article has been originally published on my public account hongyangAndroid.
Please indicate the source for reprinting:
http://blog.csdn.net/lmj623565791/article/details/72859156
This article is from Zhang Hongyang's blog

This article has been originally published on my public account hongyangAndroid, a collection of articles .

I. Overview

Before the project's new feature adaptation work was done by colleagues, I have not paid much attention to it, but it is necessary to make some records for similar adaptation work.

For Android 7.0, a lot of changes are provided. For details, you can read the official document Android 7.0 Behavior Changes . I remember that at that time, multi-window support, FileProvider and 7.1 3D Touch support were made, but the most related to our developers, or What must be adapted is to remove file://the uri that passes a similar format in the project.

On systems older than official 7.0, attempting to pass file://URImay trigger FileUriExposedException.

Therefore, this article mainly describes how to adapt to this problem, there is no difficulty, just record.

Note: this article targetSdkVersion 25, compileSdkVersion 25

The case of taking pictures

Everyone should be familiar with taking pictures with mobile phones. When we want to get a high-definition picture, we will pass a Uri of File to the camera application through Intent.

The approximate code is as follows:

private static final int REQUEST_CODE_TAKE_PHOTO = 0x110;
    private String mCurrentPhotoPath;

    public void takePhotoNoCompress(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {

            String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                    .format(new Date()) + ".png";
            File file = new File(Environment.getExternalStorageDirectory(), filename);
            mCurrentPhotoPath = file.getAbsolutePath();

            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_TAKE_PHOTO) {
            mIvPhoto.setImageBitmap(BitmapFactory.decodeFile(mCurrentPhotoPath));
        }
        // else tip?

    }

Post a picture of the effect~

The 6.0 permission has not been processed. If necessary, handle it yourself. If the nexus series has not processed it, you need to manually enable the storage permission on the settings page.

At this point, if we use the native system of Android 7.0 or above and run it again, you will find that the application stops running directly and throws android.os.FileUriExposedException:

Caused by: android.os.FileUriExposedException: 
    file:///storage/emulated/0/20170601-030254.png 
        exposed beyond app through ClipData.Item.getUri()
    at android.os.StrictMode.onFileUriExposed(StrictMode.java:1932)
    at android.net.Uri.checkFileUriExposed(Uri.java:2348)

So if you realize that the code you wrote, it is very convenient to crash directly on the mobile phone of the 7.0 native system~

The reason has been explained on the official website:

For apps targeting Android 7.0, the StrictMode API policy enforced by the Android framework prohibits exposing file:// URIs outside your app. If an intent containing a file URI leaves your app, the app crashes with a FileUriExposedException exception.

Similarly, the official website also gives the solution:

To share files between apps, you should send a content:// URI and grant temporary access to the URI. The easiest way to do this is to use the FileProvider class. For more information about permissions and sharing files, see Sharing files.
https://developer.android.com/about/versions/nougat/android-7.0-changes.html#accessibility

So let's take a look at how to solve this problem through FileProvider.

3. Use FileProvider to be compatible with taking pictures

In fact, for how to use FileProvider, there are also detailed steps on the API page of FileProvider. If you are interested, you can read it.

https://developer.android.com/reference/android/support/v4/content/FileProvider.html

FileProvider is actually a subclass of ContentProvider, and its function is more obvious. If it file:///Uriis not used, then change to Uri content://instead.

Let's take a look at the overall implementation steps and consider why and how?

(1) declare provider

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.zhy.android7.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

Why declare it? Because FileProvider is a subclass of ContentProvider wow~~

Note that he needs to set a meta-data, which points to an xml file.

(2) Write resource xml file

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path name="root" path="" />
    <files-path name="files" path="" />
    <cache-path name="cache" path="" />
    <external-path name="external" path="" />
    <external-files-path name="name" path="path" />
     <external-cache-path name="name" path="path" />
</paths>

The following sub-nodes are supported inside the paths node, which are:

  • <root-path/>represents the root directory of the device new File("/");
  • <files-path/>representcontext.getFilesDir()
  • <cache-path/>representcontext.getCacheDir()
  • <external-path/>representEnvironment.getExternalStorageDirectory()
  • <external-files-path>representcontext.getExternalFilesDirs()
  • <external-cache-path>representgetExternalCacheDirs()

Each node supports two properties:

  • name
  • path

path is the subdirectory under the representative directory, for example:

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

The representative directory is: Environment.getExternalStorageDirectory()/pics, and the same is true for others.

When so declared, the code can use the current folder you declared and its subfolders.

This example uses SDCard so write it like this:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external" path="" />
</paths>

For simplicity, we directly use the SDCard root directory, so no subdirectories are filled in the path~

Here you may have questions, why write such an xml file, what is the use?

We just said that we need to use content://urisubstitution now file://uri, then, content://how to define the uri? You can't use the file path, isn't that deceiving yourself~

Therefore, a virtual path is needed to map the file path, so you need to write an xml file, determine the accessible directory through the path and xml nodes, and map the real file path through the name attribute.

(3) Using the FileProvider API

Well, then we can convert our file content://urito

public void takePhotoNoCompress(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {

            String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                    .format(new Date()) + ".png";
            File file = new File(Environment.getExternalStorageDirectory(), filename);
            mCurrentPhotoPath = file.getAbsolutePath();

            Uri fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
        }
    }

The core code is this line~

FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);

The second parameter is what we configured authorities. This is normal. It must be mapped to a certain ContentProvider~ So this parameter is needed.

Then take another look at our generated uri:

content://com.zhy.android7.fileprovider/external/20170601-041411.png

You can see that the format is: content://authorities/定义的name属性/文件的相对路径, that is, the name hides the storable folder path.

Now take the 7.0 native phone to run normally~

But it doesn't end here~

Open a 4.4 emulator, run the above code, you will find that Crash is thrown again: Permission Denial~

Caused by: java.lang.SecurityException: Permission Denial: opening provider android.support.v4.content.FileProvider from ProcessRecord{52b029b8 1670:com.android.camera/u0a36} (pid=1670, uid=10036) that is not exported from uid 10052
at android.os.Parcel.readException(Parcel.java:1465)
at android.os.Parcel.readException(Parcel.java:1419)
at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:2848)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:4399)

Because the lower version of the system only uses this as an ordinary Provider, and we do not authorize it, the export of contentprovider is also set to false; resulting Permission Denial.

So, can we set export to true?

Regrettably it is not possible.

Inside FileProvider:

@Override
public void attachInfo(Context context, ProviderInfo info) {
    super.attachInfo(context, info);

    // Sanity check our security
    if (info.exported) {
        throw new SecurityException("Provider must not be exported");
    }
    if (!info.grantUriPermissions) {
        throw new SecurityException("Provider must grant uri permissions");
    }

    mStrategy = getPathStrategy(context, info.authority);
}

Determined that exported must be false, grantUriPermissions must be true ~~

So the only way is to authorize~

context provides two methods:

  • grantUriPermission (String toPackage, Uri uri,
    int modeFlags)
  • revokeUriPermission(Uri uri, int modeFlags);

You can see that grantUriPermission needs to pass a package name, which is which app you authorize, but in many cases, such as sharing, we don't know which app the end user will choose, so we can do this:

List<ResolveInfo> resInfoList = context.getPackageManager()
            .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
    String packageName = resolveInfo.activityInfo.packageName;
    context.grantUriPermission(packageName, uri, flag);
}

According to the Intent query, all the matching applications are authorized to them~~

Well, you can remove the permission through revokeUriPermission when you don't need it~

Then the code after adding authorization is as follows:

public void takePhotoNoCompress(View view) {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {

        String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                .format(new Date()) + ".png";
        File file = new File(Environment.getExternalStorageDirectory(), filename);
        mCurrentPhotoPath = file.getAbsolutePath();

        Uri fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);

        List<ResolveInfo> resInfoList = getPackageManager()
                .queryIntentActivities(takePictureIntent, PackageManager.MATCH_DEFAULT_ONLY);
        for (ResolveInfo resolveInfo : resInfoList) {
            String packageName = resolveInfo.activityInfo.packageName;
            grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        }

        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
        startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
    }
}

This is done, but it is still quite troublesome. If you are only compatible with the old system, it is recommended to do version verification.

Uri fileUri = null;
if (Build.VERSION.SDK_INT >= 24) {
    fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
} else {
    fileUri = Uri.fromFile(file);
}

This will be more convenient ~ and avoid causing some problems. Of course, there are some advantages to using uri completely, for example, you can use a private directory to store the photos you take~

The article will give a quick adaptation plan at the end~~No need to be so troublesome~

It seems that there is any knowledge point that has not been mentioned, let's look at another example~

Fourth, use FileProvider compatible to install apk

Normally when we write and install apk, it is like this:

public void installApk(View view) {
    File file = new File(Environment.getExternalStorageDirectory(), "testandroid7-debug.apk");

    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setDataAndType(Uri.fromFile(file),
            "application/vnd.android.package-archive");
    startActivity(intent);
}

Take a 7.0 native phone and run it android.os.FileUriExposedExceptionagain~~

android.os.FileUriExposedException: file:///storage/emulated/0/testandroid7-debug.apk exposed beyond app through Intent.getData()

Fortunately, with experience, simply modify the way to get uri.

if (Build.VERSION.SDK_INT >= 24) {
    fileUri = FileProvider.getUriForFile(this, "com.zhy.android7.fileprovider", file);
} else {
    fileUri = Uri.fromFile(file);
}

Run it again, but it still throws an exception (warning, no crash):

java.lang.SecurityException: Permission Denial: 
opening provider android.support.v4.content.FileProvider 
        from ProcessRecord{18570a 27107:com.google.android.packageinstaller/u0a26} (pid=27107, uid=10026) that is not exported from UID 10004

It can be seen that it is a permission problem. We just mentioned a way for permissions grantUriPermission. Of course, this way is no problem~

After adding it, run it.

In fact, for permissions, there is also a way, that is:

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

We can add the above code before installing the package, and it will run normally again~

Now I have two very perplexing questions:

  • Question 1: Why did the Android 7 device not encounter Permission Denialany problems when taking pictures just now?

Well, the reason why permissions are not required is mainly because the action of the Intent is ACTION_IMAGE_CAPTUREthat when we startActivity, Instrumentation的execStartActivitythe method will be called, and inside the method, the method will be called intent.migrateExtraStreamToClipData();.

This method contains:

if (MediaStore.ACTION_IMAGE_CAPTURE.equals(action)
        || MediaStore.ACTION_IMAGE_CAPTURE_SECURE.equals(action)
        || MediaStore.ACTION_VIDEO_CAPTURE.equals(action)) {
    final Uri output;
    try {
        output = getParcelableExtra(MediaStore.EXTRA_OUTPUT);
    } catch (ClassCastException e) {
        return false;
    }
    if (output != null) {
        setClipData(ClipData.newRawUri("", output));
        addFlags(FLAG_GRANT_WRITE_URI_PERMISSION|FLAG_GRANT_READ_URI_PERMISSION);
        return true;
    }
}

You can see that our EXTRA_OUTPUT was converted to setClipData, and WRITE and READ permissions were added directly to us.

Note: This part of the logic should be added after 21.

  • Question 2: Why did the Android 4.4 device encounter a permission problem when I took a photo of the case just now, and it was not solved by addFlags?

Because addFlags is mainly used setData, setDataAndTypeand setClipData(note: in 4.4, it will not be ACTION_IMAGE_CAPTUREconverted to setClipData implementation) this way.

Therefore, the addFlags method ACTION_IMAGE_CAPTUREis invalid for below 5.0, so it needs to be used grantUriPermission. If it is a normal uri shared through setData, there is no problem using addFlags (you can write a simple example to test, two apps interact and pass content://).

Fifth, to summarize

Finally covered all the knowledge points~

To sum up, the use of content://substitution file://mainly requires the support of FileProvider, and because FileProvider is a subclass of ContentProvider, it needs to be registered in AndroidManifest.xml; and because the real filepath needs to be mapped, an xml document needs to be written for Describes the available folders, and maps the folder by name.

For permissions, there are two ways:

  • The first method is Intent.addFlags, which is mainly used to transfer uri for intent.setData, setDataAndType and setClipData related methods.
  • The second method is grantUriPermission to authorize

In comparison, the second method is more troublesome, because it is necessary to specify the target application package name, which is often unclear, so it is necessary to find all matching applications through the PackageManager and authorize them all. But more stable

The first method is relatively simple. For intent.setData, setDataAndType can be used normally, but for setClipData, because Intent#migrateExtraStreamToClipDatathe code changes before and after 5.0, you need to pay attention~

Well, see if you think it is troublesome to adapt to 7.0 now, in fact, it is not troublesome at all, here is a summary of a quick adaptation method.

6. Quickly complete the adaptation

(1) Create a new module

Create a librarymodule and complete the registration of FileProvider in its AndroidManifest.xml. The code is written as:

<application>
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.android7.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths" />
    </provider>
</application>

Be careful, android:authoritiesdon't write hard, because the library may eventually be referenced by multiple projects, android:authoritiesbut it cannot be repeated. If the same is defined in two apps, the latter cannot be installed on the phone (authority conflict).

The same write file_paths~

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path
        name="root"
        path="" />
    <files-path
        name="files"
        path="" />

    <cache-path
        name="cache"
        path="" />

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

    <external-files-path
        name="external_file_path"
        path="" />
    <external-cache-path
        name="external_cache_path"
        path="" />

</paths>

Finally, write a helper class, for example:

public class FileProvider7 {

    public static Uri getUriForFile(Context context, File file) {
        Uri fileUri = null;
        if (Build.VERSION.SDK_INT >= 24) {
            fileUri = getUriForFile24(context, file);
        } else {
            fileUri = Uri.fromFile(file);
        }
        return fileUri;
    }

    public static Uri getUriForFile24(Context context, File file) {
        Uri fileUri = android.support.v4.content.FileProvider.getUriForFile(context,
                context.getPackageName() + ".android7.fileprovider",
                file);
        return fileUri;
    }


    public static void setIntentDataAndType(Context context,
                                            Intent intent,
                                            String type,
                                            File file,
                                            boolean writeAble) {
        if (Build.VERSION.SDK_INT >= 24) {
            intent.setDataAndType(getUriForFile(context, file), type);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            if (writeAble) {
                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            }
        } else {
            intent.setDataAndType(Uri.fromFile(file), type);
        }
    }
}

You can add methods according to your needs.

Okay, so our little library is written~~

(2) use

If any project needs to adapt to 7.0, then you only need to reference the library like this, and then you only need to change one line of code to complete the adaptation, for example:

Photograph

public void takePhotoNoCompress(View view) {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        String filename = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.CHINA)
                .format(new Date()) + ".png";
        File file = new File(Environment.getExternalStorageDirectory(), filename);
        mCurrentPhotoPath = file.getAbsolutePath();

        Uri fileUri = FileProvider7.getUriForFile(this, file);
        takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri);
        startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
    }
}

just need to change

 Uri fileUri = FileProvider7.getUriForFile(this, file);

That's it.

install apk

The same modification setDataAndType is:

FileProvider7.setIntentDataAndType(this,
      intent, "application/vnd.android.package-archive", file, true);

That's it.

ok, the tedious repetitive operations are finally simplified into one line of code~

Source address:

https://github.com/hongyangAndroid/FitAndroid7


If you support me, you can follow my official account, and I will push new knowledge every day~

Welcome to pay attention to my WeChat public account: hongyangAndroid
(you can leave me a message for the articles you want to learn, support submission)

refer to

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325765160&siteId=291194637