最近在測試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使用方法:
- 在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了