Android 7.0 設(shè)備已經(jīng)逐漸普及了乔煞。然而和 6.0 系統(tǒng)的 運(yùn)行時(shí)權(quán)限 類似, Google 又針對這個(gè)版本做了一些出于安全性考慮的改動(dòng)劲赠。如果針對 24+ 的 SDK 版本 進(jìn)行開發(fā)逞带,你不得不做一些適配工作陌兑。
FileUriExposedException
我們創(chuàng)建一個(gè)項(xiàng)目铡原,目標(biāo) SDK 設(shè)置為 24 或者更大偷厦。
首先寫一個(gè)布局文件商叹,取名 activity_main
。
很簡單只泼,一個(gè)幀布局剖笙,里面一個(gè)按鈕。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.jin.fileprovidertest.MainActivity">
<Button
android:id="@+id/ac_main_btn"
android:text="click me!"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
對應(yīng)的 MainActivity
请唱,也很簡單弥咪,聲明賦值按鈕,然后添加點(diǎn)擊事件籍滴。
事件里調(diào)用 系統(tǒng)自帶 的相機(jī)應(yīng)用酪夷,這是 Google 推薦的方式,可以不用額外申請權(quán)限孽惰。
因?yàn)閮H僅為了測試,就不寫回調(diào)了鸥印,直接打開系統(tǒng)相機(jī)的活動(dòng)勋功。
package com.example.jin.fileprovidertest;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import java.io.File;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn = (Button) findViewById(R.id.ac_main_btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Uri uri = Uri.fromFile(new File(Environment.getExternalStorageDirectory().getPath()));
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivity(intent);
}
});
}
}
然后在一臺 Android 7.0 設(shè)備上啟動(dòng)之,結(jié)果一點(diǎn)按鈕就崩潰了——
查看 logcat 库说,報(bào)了一個(gè)異常叫 FileUriExposedException
原來這是 Android 7.0 的一處安全性改動(dòng):不允許再直接使用真實(shí)路徑的URI狂鞋。 因此不能再直接調(diào)
Uri.fromFile()
方法了,需要用到本文的主角 FileProvider 潜的。
FileProvider
這是 Android 7.0 新增的一個(gè)類骚揍,位于 v4 包下,繼承自四大組件之一的 ContentProvider
啰挪,因此需要 在清單配置文件里注冊信不。
首先在資源文件夾下新建一個(gè) xml 目錄,里面包含一個(gè)文件 test
——
文件內(nèi)容如下——
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<root-path name="my_image" path=""/>
</paths>
</resources>
外兩層寫法是固定的亡呵,重點(diǎn)在第三層抽活。
首先是節(jié)點(diǎn)名 <root-path>
,它對應(yīng)前面代碼中 Environment.getExternalStorageDirectory()
方法獲取到的存儲(chǔ)路徑锰什。
如果是其它路徑下硕,節(jié)點(diǎn)名要做相應(yīng)改變——
<files-path>
,對應(yīng) Context.getFilesDir()
方法汁胆;
<cache-path>
梭姓,對應(yīng) getCacheDir()
方法;
<external-path>
嫩码,Google 官方文檔和網(wǎng)上多數(shù)文章都說誉尖,是對應(yīng) Environment.getExternalStorageDirectory()
方法,但在現(xiàn)有設(shè)備上親測均無效谢谦,會(huì)報(bào) IllegalArgumentException 異常释牺。
<external-files-path>
萝衩,對應(yīng) Context.getExternalFilesDir()
方法;
<external-cache-path>
没咙,對應(yīng) Context.getExternalCacheDir()
方法猩谊。
然后是 path 屬性,Google 官方文檔和網(wǎng)上多數(shù)文章同樣都說祭刚,傳空牌捷,代表你整個(gè)對應(yīng)路徑都可以用于共享;傳入一個(gè)相對路徑涡驮,代表只有這個(gè)路徑的指向才能用于共享暗甥。但在現(xiàn)有設(shè)備上親測同樣均無效,會(huì)報(bào) IllegalArgumentException 異常捉捅。
以上二處原因不明撤防,請指教。
完成 test.xml
后棒口,我們?nèi)?AndroidManifest
文件里注冊——
<provider
android:authorities="com.example.jin.fileprovidertest.fileprovider"
android:name="android.support.v4.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/test"/>
</provider>
需要注意的就是 authorities 屬性寄月,Google 官方推薦的寫法是「包名 + .fileprovider」。其它屬性寫法固定无牵,resource 的值應(yīng)指向你之前的 xml 文件漾肮。
好了,現(xiàn)在修改我們的 MainAcitivity
茎毁,將 onClick()
方法的內(nèi)容修改為——
final String authority = "com.example.jin.fileprovidertest.fileprovider";
boolean b = Build.VERSION.SDK_INT >= 24;
File file = new File(Environment.getExternalStorageDirectory().getPath());
Uri uri = b
? FileProvider.getUriForFile(MainActivity.this, authority, file)
: Uri.fromFile(file);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
startActivity(intent);
然后啟動(dòng)克懊,完美!
最近看了一些文章七蜘,補(bǔ)充更新:
- 建議自己創(chuàng)建一個(gè)類 MyProvider 繼承 FileProvider(類里面可以什么都不寫)谭溉,然后將 name 屬性的值設(shè)為 .MyProvider。這樣做的好處是可以確保當(dāng)進(jìn)行組件化開發(fā)時(shí)崔梗,不同組件之間的 merge 不會(huì)發(fā)生沖突夜只。
- authority 屬性的值更規(guī)范一點(diǎn)的寫法是 ${applicationId}.fileprovider。相應(yīng)的在代碼中蒜魄,它的賦值為 getPackageName() + ".fileprovider"(需要有上下文環(huán)境)扔亥。
- xml 文件的 <resource> 節(jié)點(diǎn)可以剝掉,直接用 <paths> 做根節(jié)點(diǎn)谈为。
本文結(jié)束旅挤,歡迎拍磚吐槽 and 指教~~