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使用方法:
- 在AndroidManifest.xml裡宣告Provider
<manifes 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" //設定允許獲取訪問檔案的臨時許可權
android:exported="false"//設定不允許匯出,我們的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>
在標籤中我們必須配置至少一個或多個path子元素,
path子元素則用來定義content uri所對應的路徑目錄。
這裡以為例:
* name屬性:指明瞭FileProvider在content uri中需要新增的部分,這裡的name為my_images,所以對應的content uri為
content://com.example.myapp.fileprovider/my_images
- 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*" />
<cache-path name="*name*" path="*path*" />
<external-path name="*name*" path="*path*" />
<external-files-path name="*name*" path="*path*" />
<external-cache-path name="*name*" path="*path*" />
以此類推,我們可以根據自己所需共享的目錄來配置不同的path路徑.
3.配置完共享地址後,獲取content 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);
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() 方法來將根標籤(等)對應的路徑作為檔案根路徑,
同時將標籤中的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(“/”);
沒錯,雖然官方文件沒有寫出來,但是程式碼裡竟然留了個的根標籤,而它的路徑對應的是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"//指向自定義的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了