preface

Storage adaptation series articles:

Android- Storage Basics Android-10, 11- Storage fully adapted (top) Android-10, 11- Storage fully adapted (bottom) Android-FileProvider- Easy to master

When I was analyzing the knowledge points related to Android storage, some students proposed to analyze FileProvider as well. At that time, I was busy summarizing the concurrent knowledge points of threads and did not immediately start sharing. This time, we will focus on how Android apps open files using third-party apps and how to share files with third-party apps. Through this article, you will learn:

1. Sharing files between Android applications 2. Application and Principle of FileProvider 3

1. Share files between Android applications

A Shared basis

When it comes to file sharing, the first thing that comes to mind is to store a file on a local disk, which can be accessed by multiple applications, as follows:

Ideally, any application can read or write a file if it knows where it is stored. /sdcard/DCIM/, /sdcard/Pictures/. /sdcard/DCIM/, /sdcard/Movies/.

Sharing way

A common application scenario:

A file named my.txt was retrieved from app A, but it could not be opened. Therefore, it wanted to open it with the help of other apps. At this time, it needed to tell other apps the path of the file to be opened.

Assuming application B can open my.txt, how can application A pass the path to application B? This involves interprocess communication. We know that Binder is the main means of communication between Android processes, and the communication between the four components is also dependent on Binder, so we can rely on the four components for the transfer path between applications.

It can be seen that the Activity/Service/Broadcast can pass Intent, and ContentProvider transmit Uri, actually carrying the Uri in the Intent variables, so the four components can be passed between Uri, and the path can be kept in the Uri.

2. Application and principle of FileProvider

Using other applications to open files as an example, explain the differences before and after Android 7.0.

Used before Android 7.0

Pass paths can be passed through urIs to see how they are used:

private void openByOtherForN() { Intent intent = new Intent(); Intent.setaction (intent.action_view); Uri Uri = uri.fromfile (new File(external_filePath)); // Set the Intent with the Uri intent.setData(Uri); Intent startActivity(Intent); }Copy the code

Among them

  • external_filePath=”/storage/emulated/0/fish/myTxt.txt”
  • After the structure of uri uriString = “file:///storage/emulated/0/fish/myTxt.txt”

As you can see, the file path is preceded by “file:///” string. After receiving the Intent, the receiver picks up the Uri and passes:

FilePath = uri.getencodedPath () reads and writes files after retrieving the original path sent by the sender.

However, this method of constructing uris is deprecated in Android7.0 and beyond, and will throw an exception if it is used:

We can see the disadvantages of the uri. fromFile construction:

1. The receiver is fully aware of the file path transmitted by the sender, which is clear at a glance and without security guarantee. 2. The receiver may not have read permission on the file path transferred by the sender, which results in receiving exceptions.

Use after Android 7.0(inclusive)

Think about it for a moment. How do we get around these two problems if we do it ourselves? For the first problem: you can replace the specific path with another string, similar to the old password book, for example: “/ storage/emulated / 0 / fish/myTxt. TXT” replaced by “myfile/TXT. TXT”, so that the receiver receive the file path is how completely don’t know the original file path.

But this introduces an additional problem: how can the receiver read the file without knowing the real path?

In view of the second question, since it is uncertain whether the receiver has the permission to open the file, should the sender open it and then pass it to the receiver?

Android 7.0 and Inclusive introduced FileProvider to address both of these issues.

FileProvider application

Let’s start by looking at how to use FileProvider to pass paths. Broken down into four steps:

Define a FileProvider subclass

public class MyFileProvider extends FileProvider {

}
Copy the code

Define an empty class that inherits from FileProvider, which inherits from ContentProvider. Note: FileProvider needs to be introduced on AndroidX

AndroidManifest declares the FileProvider

Androidmanifest.xml: androidManifest.xml: androidManifest.xml: androidManifest.xml: androidManifest.xml

        <provider
            android:authorities="com.fish.fileprovider"
            android:name=".fileprovider.MyFileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_path">
            </meta-data>
        </provider>
Copy the code

The fields are described as follows:

1, Android :authorities identifies ContentProvider as being completely unique. Android: Name refers to the previously defined FileProvider subclass. 3, Android: Exported =”false” to restrict other applications to obtain providers. 4, android: grantUriPermissions = “true” Uri to other application access. Meta -data includes the alias application table. Android :name is fixed to indicate that file_path is being resolved. 5.2 android: Resource Self-defined implementation of the mapping table

Three: Path mapping table

As you can see, the FileProvider needs to read the mapping table. Create the XML folder under /res/, and then create the corresponding mapping table (XML). The final path is /res/ XML /file_path.xml. As follows:

<? The XML version = "1.0" encoding = "utf-8"? > <paths> <root-path name="myroot" path="." /> <external-path name="external_file" path="fish" /> <external-files-path name="external_app_file" path="myfile" /> <external-cache-path name="external_app_cache" path="mycache/doc/" /> <files-path name="inner_app_file" path="." /> <cache-path name="inner_app_cache" path="." /> </paths>Copy the code

The fields are described as follows:

1. The root-path tag indicates that the subdirectories in the root directory are aliases (including internal storage, built-in external storage, and extended external storage, which are all represented by “/”). The path attribute indicates the name of the directory to be changed. 2, if there is a path to the file: / storage/emulated / 0 / fish/myTxt. TXT, and we only configure the root – path tag, so eventually the file path is replaced with: / myroot/storage/emulated / 0 / fish/myTxt. TXT.

As you can see, since path=”.”, myroot is appended to any directory.

The directories of the remaining external-path labels are as follows:

1, the external – path – > Environment. External.getexternalstoragedirectory (), Such as/storage/emulated / 0 / fish 2, external files – path – > ContextCompat. GetExternalFilesDirs (context, null). 3, external cache – the path – > ContextCompat. GetExternalCacheDirs (context). 4, the files – path – > context. GetFilesDir (). 5, the cache – the path – > context. GetCacheDir ().

As you may have noticed, the directories represented by these labels overlap. How do I choose to replace the aliases? The answer is: choose the longest match. Suppose we only define root-path and external-path in the mapping table.

Root – path > / external – path – > / storage/emulated / 0 / to transfer the file path for now: / storage/emulated / 0 / fish/myTxt. TXT. The directory where the file resides needs to be aliased, so the mapping table is traversed to find the label that longest matches the directory. Obviously, /storage/emulated/0/ represented by external-path best matches the file directory, so the file path is replaced with: /external_file/myTxt.txt

Four: Use FileProvider to construct paths

Once the mapping table is established, the path needs to be constructed.

Private void openByOther() {String extension = external_filePath. Substring (external_filePath. LastIndexOf (".")) + 1); / / find the mimeType through extension String mimeType. = MimeTypeMap getSingleton () getMimeTypeFromExtension (extension); Intent Intent = new Intent(); / / to read and write access intent. AddFlags (intent. FLAG_GRANT_READ_URI_PERMISSION | intent. FLAG_GRANT_WRITE_URI_PERMISSION); Intent.setaction (intent.action_view); intent.action_view (intent.action_view); File file = new File(external_filePath); // The second argument indicates which ContentProvider to use. This unique value is defined in androidmanifest.xml. Can be used directly FileProvider alternative Uri Uri = MyFileProvider. GetUriForFile (this, "com. Fish. FileProvider", the file). Intent.setdataandtype (uri, mimeType); intent.setDataAndType(uri, mimeType); StartActivity (intent); } catch (Exception e) {// If no other application can accept open this mimeType, Toast.maketext (this, LLDB etLocalizedMessage(), toast.length_short).show(); }}Copy the code

/ storage/emulated / 0 / fish/myTxt. TXT eventually structure as follows: the content: / / com. Fish. Fileprovider external_file/myTxt. TXT

For private directory: / data/user / 0 / com. Example. Androiddemo/files/myTxt. TXT eventually structure as follows: content://com.fish.fileprovider/inner_app_file/myTxt.txt

You can see that:

Content as scheme;

Com.fish. fileProvider is the authorities we define as host;

After the Uri is constructed in this way, the third-party application cannot see the real path we passed from the path, which solves the first problem: the receiver is fully aware of the file path passed by the sender, and there is no security guarantee.

FileProvider Uri construction and resolution

The Uri constructs the input stream

The sender hands the Uri to the system, which finds an application capable of handling it. TXT file. Assuming that application B has the ability to open text files and is willing to accept the path passed by others, then it needs to declare the following in the AndroidManifest:

<activity android:name="com.fish.fileprovider.ReceiveActivity"> <intent-filter> <action android:name="android.intent.action.VIEW"></action> <category android:name="android.intent.category.DEFAULT"/> <category  android:name="android.intent.category.BROWSABLE"/> <data android:scheme="content"/> <data android:scheme="file"/> <data  android:scheme="http"/> <data android:mimeType="text/*"></data> </intent-filter> </activity>Copy the code

Android. Intent. Action. The VIEW say receive other applications to open the file request. Android :mimeType indicates that it has the ability to open a certain file, and text/* indicates that only text type open requests are received. After declaring the above content, the application will appear in the system’s selection box. When the user clicks the application in the box, the ReceiveActivity will be invoked. As we know, the passed Uri is wrapped in the Intent, so ReceiveActivity needs to handle the Intent.

private void handleIntent() { Intent intent = getIntent(); if (intent ! = null) {if (intent.getAction().equals(intent.action_view)) {if (intent.getAction(). String content = handleUri(uri); if (! Textutils.isempty (content)) {tvContent.settext (" Open the file: "+ content); } } } } private String handleUri(Uri uri) { if (uri == null) return null; String scheme = uri.getScheme(); if (! IsEmpty (scheme)) {if (scheme.equals("content")) {try {// Construct the stream InputStream InputStream = from the URI getContentResolver().openInputStream(uri); Byte [] content = new byte[inputStream.available()]; byte[] content = new byte[inputStream.available()]; inputStream.read(content); return new String(content); } catch (IOException e) { e.printStackTrace(); } } catch (FileNotFoundException e) { e.printStackTrace(); } } } return null; }Copy the code

It takes the Uri from the Intent, constructs the input stream from the Uri, and finally reads the file content from the input stream. At this point, application A can pass the file to application B through the FileProvider in any path it can access, and application B can read the file and display it. The second problem has not been solved: the sender may pass a file path that the receiver does not have access to, resulting in a receiving exception. GetContentResolver ().openInputStream(URI) :

#ContentResolver.java public final @Nullable InputStream openInputStream(@NonNull Uri uri) throws FileNotFoundException { Preconditions.checkNotNull(uri, "uri"); String scheme = uri.getScheme(); if (SCHEME_ANDROID_RESOURCE.equals(scheme)) { ... } else if (scheme_file.equals (scheme)) {//file starts} else {//content starts this AssetFileDescriptor fd = openAssetFileDescriptor(uri, "r", null); Try {// Get input stream from file descriptor return fd! = null ? fd.createInputStream() : null; } catch (IOException e) { throw new FileNotFoundException("Unable to create stream"); } } } public final @Nullable AssetFileDescriptor openAssetFileDescriptor(@NonNull Uri uri, @NonNull String mode, @Nullable CancellationSignal cancellationSignal) throws FileNotFoundException { ... String scheme = uri.getScheme(); String scheme = uri.getScheme(); If (SCHEME_FILE. Equals (scheme)) {if (SCHEME_FILE. Equals (scheme)) {if (SCHEME_FILE. / / the content begins the if (" r ". The equals (mode)) {return openTypedAssetFileDescriptor (uri, "* / *", null, cancellationSignal); } else { ... } } } public final @Nullable AssetFileDescriptor openTypedAssetFileDescriptor(@NonNull Uri uri, @NonNull String mimeType, @Nullable Bundle opts, @Nullable CancellationSignal cancellationSignal) throws FileNotFoundException { ... // Find FileProvider IPC call IContentProvider unstableProvider = acquireUnstableProvider(URI); //IPC call, Returns the file descriptor fd = unstableProvider. OpenTypedAssetFile (mPackageName, uri, mimeType, opts, remoteCancellationSignal); if (fd == null) { // The provider will be released by the finally{} clause return null; } } catch (DeadObjectException e) { ... }... // Construct AssetFileDescriptor return new AssetFileDescriptor(PFD, fd.getStarToffSet (), fd.getDeclaredLength()); } catch (RemoteException e) { ... }}Copy the code

The above is the call process of application B, and finally gets the FileProvider of application A. After getting the FileProvider, IPC can be called.

Application B initiates IPC. Let’s see how application A responds to this action:

# ContentProviderNative. Java / / Binder this method is called public Boolean onTransact (int code, Parcel data, Parcel reply, int flags) throws RemoteException { case OPEN_TYPED_ASSET_FILE_TRANSACTION: { ... fd = openTypedAssetFile(callingPkg, url, mimeType, opts, signal); } } #ContentProvider.java @Override public AssetFileDescriptor openTypedAssetFile(String callingPkg, Uri uri, String mimeType, Bundle opts, ICancellationSignal cancellationSignal) throws FileNotFoundException { ... try { return mInterface.openTypedAssetFile( uri, mimeType, opts, CancellationSignal.fromTransport(cancellationSignal)); } catch (RemoteException e) { ... } finally { ... } } public @Nullable AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException { ParcelFileDescriptor fd = openFile(uri, mode); return fd ! = null ? new AssetFileDescriptor(fd, 0, -1) : null; }Copy the code

As you can see, the openFile() method is called and the FileProvider overrides it:

#ParcelFileDescriptor.java @Override public ParcelFileDescriptor openFile(@NonNull Uri uri, @nonNULL String mode) throws FileNotFoundException {final File File = mStrategy.getFileForURI (URI); final int fileMode = modeToMode(mode); / / structure ParcelFileDescriptor return ParcelFileDescriptor. Open (file, fileMode); }Copy the code

ParcelFileDescriptor holds FileDescriptor, which can be transferred across processes. Mstrategy.getfileforuri (URI), how to find path through URI, the code is very simple, I will not paste, just use graph.

The following articles can be read about IPC and its four major components:

Android communication core of four components

Binder foundation for Android IPC

The Uri and Path transfer each other

Transfer Path Uri

Back to how path was constructed as A Uri in the original application:

When application A starts, it scans the FileProvider in androidmanifest.xml and reads the mapping table to construct A Map:



The Map Key is the alias in the Map table, and the Value corresponds to the directory to be replaced.

Or in the/storage/emulated / 0 / fish/myTxt. TXT, for example:

When calling MyFileProvider. GetUriForFile (xx), traversing the Map, find the best matching items, the best match for external_file namely. So will substitute external_file/storage/emulated / 0 / fish /, eventually forming the Uri of the as follows: the content: / / com. Fish. Fileprovider external_file/myTxt. TXT

Application B constructs the input stream through the Uri. Application A constructs the input stream through the Uri. Therefore, A needs to convert the Uri to Path:

A Uri first isolated external_file/myTxt. TXT, then through external_file find corresponding to the Value from the Map for: / storage/emulated / 0 / fish /, finally will myTxt. TXT, forming path is: /storage/emulated/0/fish/myTxt.txt

As you can see, the Uri was successfully converted to Path.

Now to comb through the process:

1. Application A uses the FileProvider to convert the Path to A Uri through A Map and send the Path to application B through IPC. 2. Application B uses the Uri to obtain the FileProvider of application A through IPC. 3. Application A uses the FileProvider to convert the Uri to Path through the mapping table and construct the file descriptor. 4. Application A returns the file descriptor to application B. Then application B can read the file sent by application A.

As can be seen from the above, no matter whether Application B has the storage permission or not, as long as application A has the storage permission, because the file access is completed through application A, which answers the second question: the file path transmitted by the sender may not be read by the receiver, resulting in receiving exceptions.

The above example illustrates the application of FileProvider by taking opening files as an example. In fact, sharing files is a similar process.

Of course, as you can see from the above, FileProvider construction requires several steps and needs to distinguish the differences between Android versions. Therefore, these steps are abstracted into a simple library, and the corresponding methods can be directly called externally. Steps of storage introduction:

Project build.gradle add: AllProjects {repositories {... Maven {url 'https://jitpack.io'}}} add dependencies to the module build.gradle. / / introduce EasyStorage library implementation 'com. Making. Fishforest: EasyStorage:' 1.0.1} 3, way of use:  EasyFileProvider.fillIntent(this, new File(filePath), intent, true);Copy the code

The last line of code is done.

The effect is as follows:

This article is based on Android 10.0 demo code and library source code if you help, give Github a thumbs up ~

If you like, please like, pay attention to your encouragement is my motivation to move forward

Continue to update, with me step by step system, in-depth study of Android/Java