FileProvider of Android Climbing Journey (Failed to find configured root that contains)

Original address: https://www.jianshu.com/p/ac5fe346a5b7


When testing FileProvider-related functions recently, the program suddenly crashed when selecting a picture from a custom album to obtain the content uri through FileProvider, and reported

Failed to find configured root that contains xxxx

At first, I thought it was my configuration error, but referring to the official document to change it still has no effect. After racking my brains, I finally found the cause of the error and found the correct solution. Before the solution, let's make a brief understanding and review of FileProvider.


Introduction to FileProvider

I have known FileProvider for a long time, but I have rarely used it in actual projects I have done before. Read it many times, but never thought to use it.

But with the arrival of Android 7.0, in order to further improve the security of private files, Android will no longer allow developers to relax the access rights of private files. Before, we have always used the "file:///" absolute path to pass the file address. It is easy to trigger a SecurityException exception when the receiver accesses.

Therefore, in order to better adapt to Android 7.0, FileProvider is used in places that involve file address transfer, such as camera taking pictures, and FileProvider is also better in everyone's field of vision.

In fact, FileProvider is a special subclass of ContentProvider, which is essentially based on the implementation of ContentProvider. FileProvider converts the path of "file:///" into a specific content uri in the form of "content://", and the receiver passes this uri. Then use ContentResolver to query and parse the media library.


How to use FileProvider:

  1. Declare Provider in AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.example.myapp"> 
      <application        
          ...>  
          <provider     
              android:name="android.support.v4.content.FileProvider" //指向v4包里的FileProviderandroid:authorities="com.example.myapp.fileprovider" //对应你的content uri的基础域名,生成的uri将以content://com.example.myapp.fileprovider作为开头
              android:grantUriPermissions="true" //设置允许获取访问uri的临时权限
              android:exported="false"//设置不允许导出,我们的FileProvider应该是私有的
          >            
          <meta-data
              android:name="android.support.FILE_PROVIDER_PATHS" 
              android:resource="@xml/filepaths" //用于设置FileProvider的文件访问路径
           />        
          </provider>        
          ...    
      </application>
</manifest>

2. Configure the path of the FileProvider file share

Next, create an xml directory in our res directory, and create a new filepaths.xml in the xml directory. The name of this xml can be taken according to the specific situation of the project, corresponding to the configuration of the FileProvider path in the mainifest configuration in the first step. specified file

<paths xmlns:android="http://schemas.android.com/apk/res/android"> 
    <files-path name="my_images" path="images/"/> 
    ...
</paths>

In the <paths> tag, we must configure at least one or more path sub-elements, and the
path sub-elements are used to define the path directory corresponding to the content uri.
Here is an example of <files-path>:

  • name attribute: indicates the part that FileProvider needs to add in the content uri, where the name is my_images, so the corresponding content uri is
content://com.example.myapp.fileprovider/my_images
  • path attribute: The path address corresponding to the <files-path> tag is the path address returned by Context.getFilesDir()](), and the value of the path attribute is the sub-path of the path, where the path value is "images/", That combined path looks like this:
Content.getFilesDir() + "/images/"
  • The name attribute corresponds to the path attribute one-to-one. According to the above configuration, when the file is accessed
content://com.example.myapp.fileprovider/my_images/xxx.jpg

You will find the path configured here

Content.getFilesDir() + "/images/"

and look for the xxx.jpg file

For the configuration of the corresponding path, the official documentation lists the following configuration items:

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

The path address corresponding to Context.getFilesDir()

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

Corresponds to the path obtained by getCacheDir()

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

Corresponds to the path address of Environment.getExternalStorageDirectory()

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

Corresponds to the path address returned by Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)

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

Corresponds to the address returned by Context.getExternalCacheDir()
.
By analogy, we can configure different paths according to the directories we need to share.

3. After configuring the shared address, obtain the value of the content uri, which is the uri address provided to the third party for access

File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);

4. Grant a temporary permission to a uri and pass the value to the receiver app.
We assume that the receiver app uses startActivityForResult to request the app's image resources,
then the requester obtains the request according to the above code.

Intent intent = new Intent()
intent.setDataAndType(fileUri,getContentResolver().getType(fileUri));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
setResult(intent);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

Used to grant the recipient temporary permissions for file operations, which can be set to FLAG_GRANT_READ_URI_PERMISSION
FLAG_GRANT_WRITE_URI_PERMISSION
or both

5. The receiver queries data according to the obtained content uri in onActivityResult

Uri returnUri = returnIntent.getData();    
Cursor returnCursor =  getContentResolver().query(returnUri, null, null, null, null);    
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);   
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();    
TextView nameView = (TextView) findViewById(R.id.filename_text); 
TextView sizeView = (TextView) findViewById(R.id.filesize_text); 
nameView.setText(returnCursor.getString(nameIndex)); 
sizeView.setText(Long.toString(returnCursor.getLong(sizeIndex)));

Now the entire usage process of our FileProvider is complete


FileProvider的坑(Failed to find configured root that contains)

failed to find configured root that contains

After further understanding and consolidation of FileProvider, it is finally time to uncover the cause of the error

It turned out that the photo album of this Redmi phone in hand chose the storage location on the external SD card, so the storage address of some album photos is actually on the external SD card.
Below is my path configuration

<paths xmlns:android="http://schemas.android.com/apk/res/android>
    <external-path name="external_files" path="."/>
</paths>

At first glance, there seems to be no problem. The path is set to the external root path, which corresponds to Environment.getExternalStorageDirectory() .
However, this method only obtains the path of the built-in SD card, so when the selected album contains pictures from the external SD card At that time, the image address could not be found, so the error failed to find configured root that contains was thrown.

How to solve it? The configuration items given in the official documents do not correspond to external addresses, so we can only start from the source code of FileProvider to see if there is any solution.

Because FileProvider limits the scope of the path through the configuration of the path, we can analyze it through the parsing of the path as the entry point,
so we found the method parsePathStrategy() ,
which is the xml specially used to parse our path. file.

private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";

private static final File DEVICE_ROOT = new File("/");

private static PathStrategy parsePathStrategy(Context context, String authority)        
          throws IOException, XmlPullParserException {    
       ...
      while ((type = in.next()) != END_DOCUMENT) {        
          if (type == START_TAG) {            
              //<external-path>等根标签
              final String tag = in.getName();
              //标签中的name属性
              final String name = in.getAttributeValue(null, ATTR_NAME);
              //标签中的path属性  
              String path = in.getAttributeValue(null, ATTR_PATH);            
              File target = null;            
              //标签对比
              if (TAG_ROOT_PATH.equals(tag)) {                
                  target = buildPath(DEVICE_ROOT, path);            
              } else if (TAG_FILES_PATH.equals(tag)) {
                  target = buildPath(context.getFilesDir(), path);            
              } else if (TAG_CACHE_PATH.equals(tag)) {                
                  target = buildPath(context.getCacheDir(), path);            
              } else if (TAG_EXTERNAL.equals(tag)) {                
                  target = buildPath(Environment.getExternalStorageDirectory(), path);            
            }            
          if (target != null) {                
              strat.addRoot(name, target);            
          }        
    }    
}   
 return strat;
}


private static File buildPath(File base, String... segments) {    
    File cur = base;    //根文件
    for (String segment : segments) {        
        if (segment != null) {            
            //创建以cur为根文件,segment为子目录的文件
            cur = new File(cur, segment);       
        }    
    }    
    return cur;
}

Here I have excerpted some key codes. From these key codes, we can see that
after the xml is parsed to the corresponding tags, the buildPath() method will be executed to convert the root tags (<files-path><external-path> etc. ) The corresponding path is used as the root path of the file,
and the path specified by the path attribute in the label is used as a sub-path to create the corresponding File object, and finally a cur file object is generated as the target file, thereby restricting the file access path of the FileProvider to be located in cur under the path path.

One of the pieces of code made me find something strange

 if (TAG_ROOT_PATH.equals(tag)) {                
        target = buildPath(DEVICE_ROOT, path);            
 }

TAG_ROOT_PATH corresponds to the constant "root-path",
while DEVICE_ROOT corresponds to new File("/");
yes, although the official document is not written, there is a root tag of <root-path> in the code. Its path corresponds to the root path of the entire storage pointed to by DEVICE_ROOT.

So according to the logic of the code, the configuration file of our path was adjusted and
changed to

<paths xmlns:android="http://schemas.android.com/apk/res/android>
      <root-path name="name" path="" />
</paths>

At this time, if you run the program, you will find that FileProvider will no longer report an error, because the search path of the file has become the entire root path of our client. In this way, our problem has finally been successfully solved!

Through the source code analysis of FileProvider, as mentioned at the beginning, FileProvider inherits from ContentProvider, so there is another way to inherit ContentProvider and then customize Provider to handle it, but the code of FileProvider itself is well written, so it is generally not necessary. , here I give a simple code for reference only:
also configure our provider in AndroidManifest

<provider    
      android:name="org.test.img.MyProvider"//指向自定义的Provider
      android:authorities="com.test.img" 
/>

Then customize a MyProvider

public class MyProvider extends ContentProvider {    

  private String mAuthority;

  @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");    
      }    
      mAuthority = info.authority;
  }

  @Override    
  public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {        
        File file = new File(uri.getPath());        
        ParcelFileDescriptor parcel = ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY);       
         return parcel;    
  }    

  @Override    
  public boolean onCreate() {        
        return true;    
  }   
 
  @Override    
  public int delete(Uri uri, String s, String[] as) {        
      throw new UnsupportedOperationException("Not supported by this provider");   
   }  
  
  @Override    
  public String getType(Uri uri) {        
      throw new UnsupportedOperationException("Not supported by this provider");    
  }
    
  @Override    
  public Uri insert(Uri uri, ContentValues contentvalues) {        
      throw new UnsupportedOperationException("Not supported by this provider");    
  }    

  @Override    
  public Cursor query(Uri uri, String[] as, String s, String[] as1, String s1) {        throw new UnsupportedOperationException("Not supported by this provider");   
   }    

  @Override    
  public int update(Uri uri, ContentValues contentvalues, String s, String[] as) {        
      throw new UnsupportedOperationException("Not supported by this provider");    
  }

  public static Uri getUriForFile(File file) {    
    return new Uri.Builder().scheme("content").authority(mAuthority).encodedPath(file.getPath).build();
  }
}

In this way, our code can get the content uri through the custom getUriForFile



Guess you like

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