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

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

Failed to find configured root that contains xxxx

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


FileProvider簡介

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

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

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

其實FileProvider是ContentProvider的一個特殊子類,本質(zhì)上還是基于ContentProvider的實現(xiàn),F(xiàn)ileProvider會把"file:///"的路徑轉(zhuǎn)換為特定的"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" //對應(yīng)你的content uri的基礎(chǔ)域名,生成的uri將以content://com.example.myapp.fileprovider作為開頭
              android:grantUriPermissions="true" //設(shè)置允許獲取訪問uri的臨時權(quán)限
              android:exported="false"http://設(shè)置不允許導(dǎo)出,我們的FileProvider應(yīng)該是私有的
          >            
          <meta-data
              android:name="android.support.FILE_PROVIDER_PATHS" 
              android:resource="@xml/filepaths" //用于設(shè)置FileProvider的文件訪問路徑
           />        
          </provider>        
          ...    
      </application>
</manifest>

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

接下來在我們的res目錄下創(chuàng)建一個xml目錄,在xml目錄下新建一個filepaths.xml,這個xml的名字可以根據(jù)項目的具體情況來取,對應(yīng)第一步中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所對應(yīng)的路徑目錄。
這里以<files-path>為例:

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

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

Content.getFilesDir() + "/images/"

并查找xxx.jpg文件

對應(yīng)路徑的配置,官方文檔列出了如下配置項:

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

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

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

對應(yīng) getCacheDir() 獲取的路徑

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

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

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

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

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

對應(yīng) Context.getExternalCacheDir() 返回的地址
.
以此類推,我們可以根據(jù)自己所需共享的目錄來配置不同的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的臨時權(quán)限,并將值傳給接收方app
我們假設(shè)接收方app使用startActivityForResult來請求app的圖片資源,
則請求方獲取請求后根據(jù)上面的代碼獲取

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

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

5.接收方在onActivityResult里根據(jù)獲取的content uri來查詢數(shù)據(jù)

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

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

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

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

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

那怎么解決呢,官方文檔給出的配置項并沒有能夠?qū)?yīng)外部地址的,那我們只有從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) {            
            //創(chuàng)建以cur為根文件,segment為子目錄的文件
            cur = new File(cur, segment);       
        }    
    }    
    return cur;
}

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

其中一段代碼讓我發(fā)現(xiàn)了一絲異樣

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

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

于是根據(jù)代碼的邏輯,將我們的路徑的配置文件做了調(diào)整,
改成了

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

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

通過對FileProvider的源碼分析,正如開頭所說的,F(xiàn)ileProvider繼承自ContentProvider,因此還有一種方法就是繼承ContentProvider然后自定義Provider來處理,不過FileProvider本身的代碼寫得已經(jīng)很完善了,所以一般沒有這個必要,這里我給出了一段簡易代碼僅做參考:
同樣在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了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內(nèi)容