前言
記得剛學java的時候,那個體育老師就告訴我們在java的世界里萬物皆對象。沒錯,我的java是體育老師教的!既然是對象,都會經(jīng)歷不斷的進化,就像人類由最初的猿人慢慢的進化到如今的高等人類。Android的世界亦如此,經(jīng)歷一代又一代的迭代,如今Android已經(jīng)推出了8.0的版本。不過目前大多數(shù)公司應該沒適配8.0吧,國內(nèi)的手機也沒幾個是8.0系統(tǒng)吧(我只是推測,因為我上個月買的華為榮耀V9還是7.0版本,已經(jīng)是很新的手機了)。每一個版本的迭代,必定會帶來一些技術上的變革,當然肯定是朝著更為人性化的方向變革。今天要說的就是Android7.0中的一個新特性——FileProvider。
問題描述
在Android7.0之前,第三方的應用可以訪問我們自身應用的私有路徑。比如做一些第三方分享,通過Intent的方式,分享一張圖片。只需要通過圖片的路徑就可以將圖片找到然后暴露給第三方平臺,以供分享。但是,在Android7.0以后,出于保護用戶隱私的考慮,第三方應用是不可以隨便訪問我們APP的私有路徑的。這里說不能隨便訪問,也就是說還是可以訪問的,只是會變得矜持些,不那么隨便了。
這里就要引入FileProvider這個類了,字面意思就是文件提供者,就是提供圖片或者視頻文件的。那么這個類到底怎么使用呢?凡事靠對比,才能看出二者的差異,下面我就從兩個使用場景來指出在7.0上這兩個場景與之前版本的差異。
FileProvider的使用
一、拍照
1.準備工作:
(1) 既然是適配7.0的系統(tǒng),那么首先需要在build.gradle中配置相應的SDK版本號,我的配置如下:
(2) 在清單文件中,需要加入相機權限和寫權限,如下:
2.調(diào)用相機拍照:
(1) 7.0以前的調(diào)用方式:
private void takePhotoOld() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // 調(diào)起相機的意圖
if (intent.resolveActivity(this.getPackageManager()) != null) {
File file = new File(getCameraSavePath());
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
this.startActivityForResult(intent, 1000); // 為了驗證圖片存儲成功我選擇在onActivityResult方法中
將它設置到一個ImageView上,代碼就不上了,很簡單
}
}
// 照完的圖片存儲路徑(可以自行定義)
private String getCameraSavePath() {
String sdcardPath = null;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
sdcardPath = Environment.getExternalStorageDirectory().getPath();
} else {
sdcardPath = Environment.getDataDirectory().getPath() + "/data/" + "com.demo.android";
}
initDataDirectory(sdcardPath + "/Demo/"); // 注意:這里必須對路徑進行初始化,否則文件無法創(chuàng)建,導致照相之后無法返回Activity
return sdcardPath + "/Demo/Camera.png";
}
// 初始化路徑
private void initDataDirectory(String path) {
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
}
以上就是在7.0之前調(diào)用系統(tǒng)相機拍照并保存圖片的方法,下面再來看一下7.0以后是如何調(diào)用的。在上代碼之前說一下,如果在7.0的手機上也調(diào)用以上方法調(diào)用系統(tǒng)相機拍照,會導致程序崩潰并拋出以下異常。
從字面意思理解就是暴露文件路徑異常,驗證了我開始說的吧,7.0以上系統(tǒng)不再那么隨便的將自身的私有路徑暴露給第三方應用程序了。有人問了,相機也是第三方應用程序嗎,這個答案是這樣,當然是啦!好了,那下面就看一下如何成功的在7.0以上系統(tǒng)調(diào)用系統(tǒng)相機進行拍照。(這里需要說到一點,就是關于6.0動態(tài)權限的問題,在這先忽略,講解完7.0調(diào)用相機的方法之后我會寫上針對目前系統(tǒng)都適配的完整的調(diào)用系統(tǒng)相機的代碼)
(2) 7.0以后的調(diào)用方式:
1.首先,需要在清單文件中聲明一個provider,如下:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.android.demo.fileProvider" // 名字,這個可以自行設置,通常都設置為包名.fileProvider
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/camera_path" /> // 在res下創(chuàng)建xml文件夾
</provider>
之所以需要聲明,是因為FileProvider是身為Android四大組件之一的ContentProvider的子類,因此需要在清單文件中聲明。
2.剛才的聲明的最后一行,看到了一個xml文件。這個文件的作用是為FileProvider提供可以暴露的路徑,一旦一個路徑在文件中被聲明,那么就可以被FileProvider提供。下面看一下這個xml文件中的內(nèi)容:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path path="Demo/" name="Camera" /> // 外部存儲,本文案例用的就是這個,注意這里的path就是共享的圖片路徑(我前面設置的文件路徑就是外部存儲下的Demo文件夾),name代表使用這個字段去訪問真實的文件路徑
// 以下是path可以設置的其他根節(jié)點路徑
<root-path/> // 設備根目錄,等同于直接new File("/")
<files-path/> // 內(nèi)部存儲空間應用私有目錄下的files/ 目錄,等同于Context.getFilesDir() 所獲取的目錄路徑
<cache-path/> // 內(nèi)部存儲空間應用私有目錄下的cache/ 目錄,等同于Context.getCacheDir() 所獲取的目錄路徑
<external-files-path> // 外部存儲空間應用私有目錄下的files/ 目錄,等同于Context.getExternalFilesDir(null) 所獲取的目錄路徑
<external-cache-path> // 外部存儲空間應用私有目錄下的cache/ 目錄,等同于Context.getExternalCacheDir()
</paths>
應該很多朋友好奇為什么要將路徑寫到這么一個xml文件里。因為我們現(xiàn)在使用FileProvider來提供這個文件,而FileProvider是ContentProvider的子類,它用content:// Uri 代替了 file:/// Uri。因此需要通過path以及name一起來供FileProvider來找到文件的位置。這樣也更加安全的向第三方程序提供文件內(nèi)容了。
3.接下來,就來看看FileProvider類是如何幫助我們來調(diào)用系統(tǒng)相機的:
private void takePhoto7() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(this.getPackageManager()) != null) {
File file = new File(getCameraSavePath());
Uri uri = FileProvider.getUriForFile(this, "com.android.demo.fileProvider", file); // 主要就是這行代碼,通過FileProvider獲取文件的uri
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
this.startActivityForResult(intent, 1000);
}
}
getUriFromFile方法中的第二個參數(shù)就是我們在清單文件中聲明的provider的authorities。(一定要一樣)
4.在7.0的手機上運行后,就會發(fā)現(xiàn)成功的調(diào)用了系統(tǒng)的相機并實現(xiàn)了拍照。同時,可以打印出uri。我這個案例中的uri為:
content://com.android.demo.fileProvider/Camera/Camera.png
怎么樣,是不是驗證我上面說的。
(3) 針對全部機型適配后的方法:
1.前面說到了,沒有對6.0機型的動態(tài)權限進行適配。如果沒有這個權限處理的話,那么在6.0以上的機型上進行此操作等待你的將是崩潰,因為啟用照相機需要訪問照相機權限以及寫權限。因此,這里在調(diào)用相機操作之前,應該先處理權限問題。在6.0系統(tǒng)發(fā)布不久后,網(wǎng)上關于動態(tài)權限處理這一塊就出現(xiàn)了好多的開源庫供使用,基本方法都差不多,這里我舉出EasyPermissions這個開源項目做舉例。(關于這個庫的使用可以自行百度,很簡單)
2.這里,我直接上代碼,在代碼中會有注釋,保證每一步都能簡單易懂(我只寫了用到的方法):
public class MainActivity implements EasyPermissions.PermissionCallbacks {
// 權限請求的回調(diào)結果,有成功失敗兩種結果,分別對應granted和denied兩種狀態(tài)。
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
// 在這里將回調(diào)結果賦給該方法,然后此方法會根據(jù)權限請求的成功與否分別調(diào)用下面兩個方法
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
// 當權限獲取成功時,回調(diào)此方法
@Override
public void onPermissionsGranted(int requestCode, List<String> perms) {
// 在這里,再來調(diào)用照相方法
if (requestCode == 1001) {
takePhoto();
}
}
// 當權限獲取失敗時,回調(diào)此方法
@Override
public void onPermissionsDenied(int requestCode, List<String> perms) {
// 這里可以給用戶一些提示信息,比如告訴用戶無此權限無法正常使用相機功能
}
// 檢查權限并調(diào)用相機的方法
private void checkPermissionAndTakePhoto() {
// 判斷是否有相機和寫權限
if (EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// 有的話直接調(diào)用照相方法
takePhoto();
} else {
// 沒有的話去請求權限
EasyPermissions.requestPermissions(this, "請求相機和寫入權限",
1001, // 這個請求碼自定義
Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
}
// 調(diào)用相機拍照的方法
private void takePhoto() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); // 調(diào)用相機的意圖
if (intent.resolveActivity(this.getPackageManager()) != null) {
File photoFile = new File(getCameraSavePath()); // 圖片保存的文件
// 版本判斷
if (Build.VERSION.SDK_INT >= 24) {
Uri uri = FileProvider.getUriForFile(this, "com.android.demo.fileProvider", photoFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
} else {
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile));
}
startActivityForResult(intent, 1000);
}
}
}
3.嗯,以上就是完整的針對目前各個系統(tǒng)都能用的調(diào)用系統(tǒng)相機的方法了。主要就是掌握FileProvider的使用,以及6.0權限檢查機制。
二、向第三方應用分享(圖片或者視頻)
(1) 描述:
不知道大家知不知道國外的一些比較出名的APP, 例如Instagram,Youtube,Twitter等。這些都是一些海外用戶手機必備的一些軟件。就像在國內(nèi)微信,微信視頻,微博一樣非常的火熱。那么通常海外的一些APP為了增加自己產(chǎn)品的知名度,通常會選擇向這些APP渠道上發(fā)布自己產(chǎn)品的一些信息,也就是為我們常說的第三方分享。但是國內(nèi)的一些APP分享一般都是提供相應的SDK以及調(diào)用文檔,我們調(diào)用相應的API即可實現(xiàn)在對應的平臺上分享文字圖片或視頻等。
然而,還有一種分享不需要調(diào)用相比之下比較繁瑣的API接口,只需要通過Intent即可調(diào)起相應的客戶端實現(xiàn)分享。很多產(chǎn)品的分享更多這個功能,就是利用Intent來調(diào)起可以實現(xiàn)相應分享類型的客戶端的。下面,我就來說明一下具體的實現(xiàn)步驟,以及在這個過程中對FileProvider的使用。
(2) 調(diào)用第三方客戶端分享
1.確定Intent的分享類型:
// 1.這里的第一個參數(shù)圖片路徑,一定要和上面相機圖片的存儲路徑一樣 也要在xml文件中聲明。
2.這里用的FileProvider也和上面是一個。
public Intent makeShareIntent(String shareImagePath, String shareContent) {
Intent intent = new Intent(Intent.ACTION_SEND); // 指定Intent用于分享
File file = null;
if (!TextUtils.isEmpty(shareImagePath)) {
file = new File(shareImagePath);
}
if (file != null && file.exists()) {
if (Build.VERSION.SDK_INT >= 24) { // 7.0以上適配
Uri uri = FileProvider.getUriForFile(context, "com.android.demo.fileProvider", file);
intent.putExtra(Intent.EXTRA_STREAM, uri);
} else { // 7.0以下
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse(shareImagePath));
}
intent.setType("image/*"); // 設置分享的類型為分享圖片(視頻為video/*)
} else {
intent.setType("text/plain"); // 當路徑為空將分享類型設為分享文字
}
intent.putExtra(Intent.EXTRA_TEXT, shareContent);
return intent;
}
(這里額外說明一點,其實可以不進行SDK的版本適配,所有的系統(tǒng)都用FileProvider獲取Uri。但這是需要給這個Intent設置一個flag標記如下):
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); (如果不加在7.0以下的機型就會發(fā)生崩潰,具體原因可以自行研究,不多說了!)
2.尋找指定的第三方平臺:
public void startShareActivity(Context context, String pkgName,
String shareImagePath, String shareContent) {
ResolveInfo resolveInfo = null; // APK文件信息類
Intent intent = makeShareIntent(shareImagePath, shareContent); // 指定分享類型的Intent,上面的方法
PackageManager packageManager = context.getPackageManager(); // 應用程序管理類
List<ResolveInfo> apks = packageManager.queryIntentActivities(intent, 0); // 查詢所有能分享此類型Intent的應用,
并為其附一個標記0
int apkNum = apks.size();
for (int i = 0; i < apkNum; i++) {
ResolveInfo info = apks.get(i);
if (pkgName.equals(info.activityInfo.packageName)) { // 查找與指定包名一致的應用
resolveInfo = info;
break;
}
}
ActivityInfo ai = resolveInfo.activityInfo; // 獲取應用的節(jié)點信息
intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name)); // 獲取應用包名和應用程序名
context.startActivity(intent); // 啟動第三方應用進行分享
}
3.彈出所有能分享此類型的應用列表
public void showAllShareActivities(Context context, String shareImagePath, String shareContent) {
Intent intent = makeAllShareIntent(shareImagePath, shareContent); // 確定指定分享類型的Intent
context.startActivity(Intent.createChooser(intent, "Share")); // 調(diào)起所有能分享的應用菜單
}