谷歌在Android7.0(API 24)做了一些權(quán)限的更改,對用戶私有目錄或私有文件的訪問和共享做了限制,具體可看官網(wǎng)描述權(quán)限更改
這里簡單解釋一下,就是涉及傳遞file://URI在7.0+上就會拋FileUriExposedException異常。下面舉兩個常見的開發(fā)場景來解釋一下傳遞file://URI具體什么操作(下面例子不涉及具體實(shí)現(xiàn),只貼出關(guān)鍵代碼做解釋)。
拍照
當(dāng)實(shí)現(xiàn)拍照需要設(shè)置拍照后照片存放目錄,一般會涉及如下代碼
Uri uri = Uri.fromFile(new File("指定你要保存的目錄"));
//設(shè)置拍照后保存目錄
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
上面代碼最終會轉(zhuǎn)為file:///你指定的目錄/指定的照片名,在7.0以下運(yùn)行正常,7.0及以上會拋出上面所說的FileUriExposedException
應(yīng)用更新
應(yīng)用更新下載完安裝包啟動系統(tǒng)安裝界面涉及下面一句代碼
intent.setDataAndType(Uri.fromFile(new File("安裝包路徑")), "application/vnd.android.package-archive");
這里跟上面拍照例子也用到了Uri.fromFile();這個api,同樣在7.0下運(yùn)行正常,7.0及以上依舊拋FileUriExposedException異常
小結(jié)
凡是涉及Uri.fromUri(),又跟Intent相關(guān)的,如在安卓7.0及以上不做適配,就會拋異常。下面具體講解如何適配
聲明Provider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.myapplication.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
將以上復(fù)制粘貼到清單文件<application>節(jié)點(diǎn)下再修改下即可
幾點(diǎn)說明:
1.其中下面這一行中的com.example.myapplication必須改為自己項(xiàng)目的包名,包名在Module下build.gradle下
android:authorities="com.example.myapplication.fileprovider"

2.倒數(shù)第二行涉及一個xml文件聲明,這個在下一個點(diǎn)介紹
android:resource="@xml/file_paths"
編寫xml
Android Studio可以鼠標(biāo)點(diǎn)一個上面那個@xml/file_paths,然后按快捷鍵Alt+Enter,就會提示創(chuàng)建文件夾和文件,按回車,再點(diǎn)擊OK即可自動在項(xiàng)目res目錄下新建一個xml目錄和一個名為file_paths的xml文件(不會快捷鍵的按照目錄結(jié)構(gòu)逐個創(chuàng)建即可)


然后打開xml文件
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="" path=""/>
<cache-path name="" path="" />
<external-path name="" path="" />
<external-files-path name="" path="" />
<external-cache-path name="" path="" />
<external-media-path name="" path="" />
</paths>
標(biāo)簽說明
<paths>為頂層標(biāo)簽,它下面可添加任意個(0,1或多個)上面出現(xiàn)的標(biāo)簽
<paths>子標(biāo)簽解釋
可以看到上面每個標(biāo)簽都含有一個name和path屬性
name:這個可以隨意寫(用于隱藏真實(shí)的目錄,后面再演示)
path:要共享的目錄,只能是目錄,不能是某個具體的文件
<files-path> 代表Context.getFilesDir()所指向的目錄
<cache-path>代表Context.getCacheDir()所指向的目錄
<external-path>代表Environment.getExternalStorageDirectory()所指向的目錄
<external-files-path>代表Context.getExternalFilesDir()所指向的目錄
<external-cache-path>代表Context.getExternalCacheDir()所指向的目錄
<external-media-path>代表Context.getExternalMediaDirs()所指向的目錄(API21+設(shè)備才能使用,所以一般不用)
以上解釋可能有的小伙伴還是一臉懵逼,下面舉個具體的下載例子說一下(不涉及代碼)
比如我現(xiàn)在把新版本安裝包下載在SD卡根目錄下的Download文件夾下,那么我的xml如下設(shè)計(jì)即可
其中path就得指定為Download,name的值可隨意寫

關(guān)于name和path最終去向這里盜用博客里面一張圖(如涉及侵權(quán)麻煩博主找我撤回)
由下圖可知最終我們將原本的file://URI轉(zhuǎn)為了content://URI,這個正是安卓7.0要求的正確傳遞方式,而我們上面的name最終傳遞到了URI的后面作為一個隱藏的目錄,隱藏了原文件的真實(shí)目錄

使用
上面說了這么多,都只是熱身,單純配置而已,還沒涉及一行代碼,接下來就得開始用代碼實(shí)現(xiàn)我們的功能了,這里也主要針對上面提到的兩個例子(其它例子看完可舉一反三,下面只展示修改部分,不涉及全部代碼)這里得用到一個FileProvider類,它繼承自ContentProvider,這也是為什么我們第一步得在清單文件注冊provider的原因。
拍照
//創(chuàng)建圖片存放file
File imgFile = new File("照片存放目錄");
Uri uri;
//根據(jù)當(dāng)前系統(tǒng)版本決定使用哪個api,N是Android7.0的代號
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//第一個參數(shù)是上下文,第二個參數(shù)來自清單文件,必須完全一樣,第三個參數(shù)為上面創(chuàng)建的照片file
uri = FileProvider.getUriForFile(this, "com.example.myapplication.fileprovider", imgFile);
} else {
//Android7.0還用原先的api
uri = Uri.fromFile(imgFile);
}
//設(shè)置拍照后保存目錄
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
應(yīng)用更新
//創(chuàng)建安裝包file
File apkFile = new File("安裝包路徑");
Uri uri;
//根據(jù)當(dāng)前系統(tǒng)版本決定使用哪個api,N是Android7.0的代號
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//第一個參數(shù)是上下文,第二個參數(shù)來自清單文件,必須完全一樣,第三個參數(shù)為上面創(chuàng)建的安裝包file
uri = FileProvider.getUriForFile(context, "com.example.myapplication.fileprovider", apkFile);
} else {
//Android7.0還用原先的api
uri = Uri.fromFile(apkFile);
}
//當(dāng)前代碼在Activity里則下面這句可省略
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//授權(quán)Intent讀取URI和寫URI的權(quán)限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//設(shè)置拍照后保存目錄
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
intent.setDataAndType(uri,"application/vnd.android.package-archive");
context.startActivity(intent);
幾點(diǎn)說明
(1)不管是拍照還是應(yīng)用更新,Android7.0及以上都使用FileProvider提供的API獲取到最終的uri,這里要特別強(qiáng)調(diào),也是上面第二個參數(shù)必須和清單文件設(shè)置的完全一樣,即應(yīng)用包名+fileprovider,也即如下紅框中字符串

(2)眼尖的小伙伴肯定注意到應(yīng)用更新中比拍照多了一行授權(quán)的代碼。這是因?yàn)镕ileProvider內(nèi)部進(jìn)行了一個授權(quán)的判斷,如果未授權(quán),則會拋出異常,所以才需要加上那一行授權(quán)代碼的。具體看下圖代碼和注釋

到這里有的小伙伴又納悶了,那都拋異常了,怎么拍照不也跟隨潮流設(shè)置一波呢?那是因?yàn)榕恼赵趧?chuàng)建Intent時傳入的Action為ACTION_IMAGE_CAPTURE。添加這個值之后startActivity的時候會調(diào)用到Intent的一個方法migrateExtraStreamToClipData()方法,方法最后有段代碼是進(jìn)行判斷Action是否為我們設(shè)置的這個值,如果是,會自動為我們添加上面的權(quán)限,所以我們才不用再次手動添加權(quán)限,具體如下圖所示。由于安裝apk我們只用到讀取的權(quán)限即可,所以上面就沒多添加寫的權(quán)限

最后小伙伴就可以愉快地適配Android7.0關(guān)于file://URI的異常啦,不知道我舉的兩個例子能否讓你能夠舉一反三呢?如發(fā)現(xiàn)內(nèi)容有誤,請指正,在此小弟先謝過了。如果不懂可留言,看到我會及時回復(fù)。
然后貼出官網(wǎng)的適配(原滋原味來一波) 適配步驟