Android爬坑之旅之FileProvider(Failed to find configured root that contains)

最近在測試FileProvider相關功能的時候,在從自定義相冊選擇圖片通過FileProvider來獲取content uri的時候程序突然崩潰了,報出了

Failed to find configured root that contains xxxx

的錯誤,一開始以為是自己的配置出錯了,但是參照官方文檔改來改去仍然沒有任何作用,通過絞盡腦汁地排查,終于發現了錯誤原因,并找到了正確的解決方案,在了解最終的解決方案之前我們先對FileProvider做個簡單的了解和回顧。


FileProvider簡介

很久之前就知道FileProvider了,然而之前做過的實際項目中卻很少用到,一方面自己的項目沒有涉及到相關的場景,一方面也是自己對文件安全沒有太在意,雖然看官方文檔的時候多次讀過,卻從來沒想到去使用它。

但隨著Android 7.0的到來,為了進一步提高私有文件的安全性,Android不再由開發者放寬私有文件的訪問權限,之前我們一直使用"file:///"絕對路徑來傳遞文件地址的方式,在接收方訪問時很容易觸發SecurityException的異常。

因此,為了更好的適配Android 7.0,例如相機拍照這類涉及到文件地址傳遞的地方就用上了FileProvider,FileProvider也更好地進入了大家的視野。

其實FileProvider是ContentProvider的一個特殊子類,本質上還是基于ContentProvider的實現,FileProvider會把"file:///"的路徑轉換為特定的"content://"形式的content uri,接收方通過這個uri再使用ContentResolver去媒體庫查詢解析。


FileProvider使用方法:

  1. 在AndroidManifest.xml里聲明Provider
<manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.example.myapp"> 
      <application        
          ...>  
          <provider     
              android:name="android.support.v4.content.FileProvider" //指向v4包里的FileProvider類
              android:authorities="com.example.myapp.fileprovider" //對應你的content uri的基礎域名,生成的uri將以content://com.example.myapp.fileprovider作為開頭
              android:grantUriPermissions="true" //設置允許獲取訪問uri的臨時權限
              android:exported="false"http://設置不允許導出,我們的FileProvider應該是私有的
          >            
          <meta-data
              android:name="android.support.FILE_PROVIDER_PATHS" 
              android:resource="@xml/filepaths" //用于設置FileProvider的文件訪問路徑
           />        
          </provider>        
          ...    
      </application>
</manifest>

2.配置FileProvider文件共享的路徑

接下來在我們的res目錄下創建一個xml目錄,在xml目錄下新建一個filepaths.xml,這個xml的名字可以根據項目的具體情況來取,對應第一步中mainifest配置里的FileProvider路徑的配置中指定的文件

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

在<paths>標簽中我們必須配置至少一個或多個path子元素,
path子元素則用來定義content uri所對應的路徑目錄。
這里以<files-path>為例:

  • name屬性:指明了FileProvider在content uri中需要添加的部分,這里的name為my_images,所以對應的content uri為
content://com.example.myapp.fileprovider/my_images
  • path屬性:<files-path>標簽對應的路徑地址為Context.getFilesDir()]()返回的路徑地址,而path屬性的值則是該路徑的子路徑,這里的path值為"images/",那組合起來的路徑如下所示:
Content.getFilesDir() + "/images/"
  • name屬性跟path屬性一一對應,根據上面的配置,當訪問到文件
content://com.example.myapp.fileprovider/my_images/xxx.jpg

就會找到這里配置的path路徑

Content.getFilesDir() + "/images/"

并查找xxx.jpg文件

對應路徑的配置,官方文檔列出了如下配置項:

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

對應 Context.getFilesDir() 的路徑地址

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

對應 getCacheDir() 獲取的路徑

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

對應 Environment.getExternalStorageDirectory()的路徑地址

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

對應Context#getExternalFilesDir(String) Context.getExternalFilesDir(null) 返回的路徑地址

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

對應 Context.getExternalCacheDir() 返回的地址
.
以此類推,我們可以根據自己所需共享的目錄來配置不同的path路徑.

3.配置完共享地址后,獲取content uri的值,這個uri即提供給第三方進行訪問的uri地址

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

4.授予一個uri的臨時權限,并將值傳給接收方app
我們假設接收方app使用startActivityForResult來請求app的圖片資源,
則請求方獲取請求后根據上面的代碼獲取

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

用來授予接收方對于文件操作的臨時權限,可以設置為 FLAG_GRANT_READ_URI_PERMISSION
FLAG_GRANT_WRITE_URI_PERMISSION
或者兩者都允許

5.接收方在onActivityResult里根據獲取的content uri來查詢數據

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

這下我們FileProvider的整個使用流程就完整了


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

failed to find configured root that contains

經過對FileProvider的進一步了解和鞏固,終于到了揭開錯誤原因的時候了

原來手上這臺紅米手機的相冊選擇了存儲位置在外置SD卡上,因此部分相冊照片的存放地址實際上是在外置sd卡上的。
下面是我的path配置

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

乍一看貌似沒什么問題,path設置的是external的根路徑,對應Environment.getExternalStorageDirectory()
然而這個方法所獲取的只是內置SD卡的路徑,所以當選擇的相冊中的圖片是外置SD卡的時候,就查找不到圖片地址了,因此便拋出了failed to find configured root that contains的錯誤。

那怎么解決呢,官方文檔給出的配置項并沒有能夠對應外部地址的,那我們只有從FileProvider的源碼入手看看有沒有什么辦法了.

因為FileProvider是通過path的配置來限制路徑范圍的,因此我們可以通過path的解析作為入口點來進行分析,
于是乎便找到了parsePathStrategy()這個方法,
它就是專門用來解析我們的path路徑的xml文件的.

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

這里我把一些關鍵代碼摘抄了出來,通過這些關鍵代碼我們可以看到,
在xml解析到對應的標簽后,會執行 buildPath() 方法來將根標簽(<files-path><external-path>等)對應的路徑作為文件根路徑,
同時將標簽中的path屬性所指定的路徑作為子路徑創建對應的File對象,最終生成了一個cur文件對象來作為目標文件,從而限制FileProvider的文件訪問路徑位于cur的path路徑下。

其中一段代碼讓我發現了一絲異樣

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

TAG_ROOT_PATH對應了常量"root-path",
而DEVICE_ROOT則對應了new File("/");
沒錯,雖然官方文檔沒有寫出來,但是代碼里竟然留了個<root-path>的根標簽,而它的路徑對應的是DEVICE_ROOT指向的整個存儲的根路徑。

于是根據代碼的邏輯,將我們的路徑的配置文件做了調整,
改成了

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

這時運行程序,會發現FileProvider已經不會報錯了,因為文件的搜索路徑已經變成了我們客戶端的整個根路徑,就這樣,我們的問題終于得到了順利地解決!

通過對FileProvider的源碼分析,正如開頭所說的,FileProvider繼承自ContentProvider,因此還有一種方法就是繼承ContentProvider然后自定義Provider來處理,不過FileProvider本身的代碼寫得已經很完善了,所以一般沒有這個必要,這里我給出了一段簡易代碼僅做參考:
同樣在AndroidManifest里配置我們的provider

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

然后自定義一個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();
  }
}

這樣我們的代碼就可以通過自定義的getUriForFile來獲取content uri了

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容