Android 7.0適配

目前在項(xiàng)目中遇到Android 7.0及以上系統(tǒng)調(diào)用相機(jī)拍照時(shí)出現(xiàn)崩潰的情況,分析后發(fā)現(xiàn)是7.0系統(tǒng)的適配問(wèn)題引起的,下面將收集到的7.0適配的相關(guān)資料整理以備忘。

Android 7.0對(duì)于文件共享權(quán)限做了進(jìn)一步的限制,比如我們?cè)谡{(diào)用系統(tǒng)相機(jī)拍照的時(shí)候經(jīng)常會(huì)這樣寫(xiě):

void takePhoto(String cameraPhotoPath) {
    File cameraPhoto = new File(cameraPhotoPath);
    Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(cameraPhoto));
    startActivityForResult(takePhotoIntent, REQUEST_TAKE_PHOTO);
}

這在一般情況下,運(yùn)行沒(méi)有問(wèn)題,但是當(dāng)把targetSdkVersion指定成24及之上并且在api>=24的設(shè)備上運(yùn)行時(shí),就會(huì)拋出異常:

android.os.FileUriExposedException:         
file:///storage/emulated/0/DCIM/IMG_20170125_144112.jpg exposed beyond app through ClipData.Item.getUri()
    at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
    at android.net.Uri.checkFileUriExposed(Uri.java:2346)
    at android.content.ClipData.prepareToLeaveProcess(ClipData.java:832)
    at android.content.Intent.prepareToLeaveProcess(Intent.java:8909)

7.0系統(tǒng)上Android不再允許app中把file://uri暴露給其他app,包括但不局限于通過(guò)Intent或ClipData等方法。原因在于使用file://Uri會(huì)有一些風(fēng)險(xiǎn),比如:

  • 文件是私有的,接收f(shuō)ile://Uri的app無(wú)法訪(fǎng)問(wèn)該文件
  • 在Android6.0之后引入運(yùn)行時(shí)權(quán)限,如果接收f(shuō)ile://Uri的app沒(méi)有申請(qǐng)READ_EXTERNAL_STORAGE權(quán)限,在讀取文件時(shí)會(huì)引發(fā)崩潰
解決辦法如下:

首先在AndroidManifest.xml中添加provider

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths" />
</provider>

res/xml/provider_paths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_files" path="."/>
</paths>

修改調(diào)用相機(jī)拍照的代碼如下:

void takePhoto(String cameraPhotoPath) {
    File cameraPhoto = new File(cameraPhotoPath);
    Intent takePhotoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    Uri photoUri;
    if (Build.VERSION.SDK_INT >= 24) {
        photoUri = FileProvider.getUriForFile(
                this,
                getPackageName() + ".provider",
                cameraPhoto);
    } else {
        photoUri = Uri.from(cameraPhoto);
    }
    takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
    startActivityForResult(takePhotoIntent, REQUEST_TAKE_PHOTO);
}
下面來(lái)具體講講這個(gè)FileProvider

使用content://Uri的優(yōu)點(diǎn):

  • 它可以控制共享文件的讀寫(xiě)權(quán)限,只要調(diào)用Intent.setFlags()就可以設(shè)置對(duì)方app對(duì)共享文件的訪(fǎng)問(wèn)權(quán)限,并且該權(quán)限在對(duì)方app退出后自動(dòng)失效。相比之下,使用file://Uri時(shí)只能通過(guò)修改文件系統(tǒng)的權(quán)限來(lái)實(shí)現(xiàn)訪(fǎng)問(wèn)控制,這樣的話(huà)訪(fǎng)問(wèn)控制對(duì)所有app都生效的,不能區(qū)分app
  • 它可以隱藏共享文件的真實(shí)路徑
定義FileProvider

FileProvider會(huì)隱藏共享文件的真實(shí)路徑,將它轉(zhuǎn)換成content://Uri路徑,因此,我們還需要設(shè)定轉(zhuǎn)換的規(guī)則。android:resource="@xml/provider_paths"這個(gè)屬性指定了規(guī)則所在的文件

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

<paths>中可以定義以下子節(jié)點(diǎn)

子節(jié)點(diǎn) 對(duì)應(yīng)路徑
files-path Context.getFilesDir()
cache-path Context.getCacheDir()
external-path Environment.getExternalStorageDirectory()
external-files-path Context.getExternalFilesDir(null)
external-cache-path Context.getExternalCacheDir()

file://到content://的轉(zhuǎn)換規(guī)則:

  • 替換前綴:把file://替換成content://${android:authorities}
  • 匹配和替換。遍歷的子節(jié)點(diǎn),找到最大能匹配上文件路徑前綴的那個(gè)子節(jié)點(diǎn),用path的值替換掉文件路徑里所匹配的內(nèi)容
  • 文件路徑剩余的部分保持不變

代碼中生成Content Uri

File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "2016/pic.png");
Uri contentUri = getUriForFile(getContext(), getPackageName() + ".fileprovider", newFile);

有兩種設(shè)置文件的訪(fǎng)問(wèn)權(quán)限:

  • 調(diào)用Context.grantUriPermission(package, uri, modeFlags)。這樣設(shè)置的權(quán)限只有在手動(dòng)調(diào)用Context.revokeUriPermission(uri, modeFlags)或系統(tǒng)重啟后才會(huì)失效
  • 調(diào)用Intent.setFlags()來(lái)設(shè)置權(quán)限。權(quán)限失效的時(shí)機(jī):接收Intent的Activity所在的stack銷(xiāo)毀時(shí)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容