關於 Android 7.0 適配中 FileProvider 部分的總結

來源:互聯網
上載者:User

標籤:裝置   class   clip   opp   完成   ext.get   media   ntp   string   

原文地址:http://yifeng.studio/2017/05/03/android-7-0-compat-fileprovider/

 

由於 Android 7.0 或更高版本的系統在國內手機市場上的佔比不是很高,很多 Android 開發人員並沒有做 7.0 適配工作,同時測試人員也容易忽視這方面的相容問題。這導致 7.0 及以上版本的手機使用者在使用到應用部分功能時可能出現 App 崩潰閃退。其中,大部分原因都是由項目中使用到 file:// 類型的 URI 所引發的。本文我們便來一探究竟。

 

Android 7.0 許可權變更

為了提高私人目錄的安全性,防止應用資訊的泄漏,從 Android 7.0 開始,應用私人目錄的存取權限被做限制。具體表現為,開發人員不能夠再簡單地通過 file:// URI 訪問其他應用的私人目錄檔案或者讓其他應用訪問自己的私人目錄檔案。

備忘:如果你對應用私人目錄不太清楚的話,可以閱讀我的這篇文章:瞭解 Android 應用的檔案儲存體目錄,掌握持久化資料的正確姿勢。

同時,也是從 7.0 開始,Android SDK 中的 StrictMode 策略禁止開發人員在應用外部公開 file:// URI。具體表現為,當我們在應用中使用包含 file:// URI 的 Intent 離開自己的應用時,程式會發生故障。

開發中,如果我們在使用 file:// URI 時忽視了這兩條規定,將導致使用者在 7.0 及更高版本系統的裝置中使用到相關功能時,出現 FileUriExposedException 異常,導致應用出現崩潰閃退問題。而這兩個過程的替代解決方案便是使用 FileProvider

FileProvider

作為四大組件之一的 ContentProvider,一直扮演著應用間共用資源的角色。這裡我們要使用到的 FileProvider,就是 ContentProvider 的一個特殊子類,協助我們將訪問受限的 file:// URI 轉化為可以授權共用的 content:// URI。

第一步,註冊一個 FileProvider

作為系統四大組件之一的 ContentProvider,其子類FileProvider,也同樣需要使用 元素在 Manifest 檔案中添加註冊資訊,並按照要求設定相關屬性值。

1234567891011 <application>...<provider android:name="android.support.v4.content.FileProvider" android:authorities="${applicationId}.yourname" android:exported="false" android:grantUriPermissions="true">...</provider>...</application>

其中,android:authorities 屬性值是一個由 build.gradle 檔案中的 applicationId 值和自訂的名稱組成的 Uri 字串(這樣寫是約定俗成的)。其他屬性值使用如上固定值即可。

第二步,添加共用目錄

在 res/xml 目錄下建立一個 xml 檔案,用於存放應用需要共用的目錄檔案。這個 xml 檔案的內容類別似這樣:

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

元素必須包含一到多個子項目。這些子項目用於指定共用檔案的目錄路徑,必須是這些元素之一:

  • <files-path>:內部儲存空間應用私人目錄下的 files/ 目錄,等同於 Context.getFilesDir() 所擷取的目錄路徑;

  • <cache-path>:內部儲存空間應用私人目錄下的 cache/ 目錄,等同於 Context.getCacheDir() 所擷取的目錄路徑;

  • <external-path>:外部儲存空間根目錄,等同於 Environment.getExternalStorageDirectory() 所擷取的目錄路徑;

  • <external-files-path>:外部儲存空間應用私人目錄下的 files/ 目錄,等同於 Context.getExternalFilesDir(null) 所擷取的目錄路徑;

  • <external-cache-path>:外部儲存空間應用私人目錄下的 cache/ 目錄,等同於 Context.getExternalCacheDir();

可以看出,這五種子項目基本涵蓋內外儲存空間所有目錄路徑,包含應用私人目錄。同時,每個子項目都擁有 name 和 path 兩個屬性。

其中,path 屬性用於指定當前子項目所代表目錄下需要共用的子目錄名稱。注意:path 屬性值不能使用具體的獨立檔案名稱,只能是目錄名。

而 name 屬性用於給 path 屬性所指定的子目錄名稱取一個別名。後續產生 content:// URI 時,會使用這個別名代替真實目錄名。這樣做的目的,很顯然是為了提高安全性。

如果我們需要分享的檔案位於同層級目錄下不同的子目錄中,就需要添加多個子項目逐一指定要分享的檔案目錄,或者共用他們通用的父目錄也行。

添加完共用目錄後,再在 <provider> 元素中使用 <meta-data> 元素將 res/xml 中的 path 檔案與註冊的 FileProvider 連結起來:

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

第三步,產生 Content URI

在 Android 7.0 出現之前,我們通常使用 Uri.fromFile() 方法產生一個 File URI。這裡,我們需要使用 FileProvider 類提供的公有靜態方法 getUriForFile 產生 Content URI。比如:

12 Uri contentUri = FileProvider.getUriForFile(this,BuildConfig.APPLICATION_ID + ".myprovider", myFile);

需要傳遞三個參數。第二個參數便是 Manifest 檔案中註冊 FileProvider 時設定的 authorities 屬性值,第三個參數為要共用的檔案,並且這個檔案一定位於第二步我們在 path 檔案中添加的子目錄裡面。

舉個例子:

1234567 String filePath = Environment.getExternalStorageDirectory() + "/images/"+System.currentTimeMillis()+".jpg";File outputFile = new File(filePath);if (!outputFile.getParentFile().exists()) {outputFile.getParentFile().mkdir();}Uri contentUri = FileProvider.getUriForFile(this,BuildConfig.APPLICATION_ID + ".myprovider", outputFile);

產生的 Content URI 是這樣的:

1 content://com.yifeng.samples.myprovider/my_images/1493715330339.jpg

其中,構成 URI 的 host 部分為 <provider> 元素的 authorities 屬性值(applicationId + customname),path 片段 my_images 為 res/xml 檔案中指定的子目錄別名(真實目錄名為:images)。

第四步,授予 Content URI 存取權限

產生 Content URI 對象後,需要對其授權存取權限。授權方式有兩種:

第一種方式,使用 Context 提供的 grantUriPermission(package, Uri, mode_flags) 方法向其他應用授權訪問 URI 對象。三個參數分別表示授權訪問 URI 對象的其他應用程式套件名,授權訪問的 Uri 對象,和授與類型。其中,授與類型為 Intent 類提供的讀寫類型常量:

  • FLAG_GRANT_READ_URI_PERMISSION

  • FLAG_GRANT_WRITE_URI_PERMISSION

或者二者同時授權。這種形式的授權方式,許可權有效期間截止至發生裝置重啟或者手動調用 revokeUriPermission() 方法撤銷授權時。

第二種方式,配合 Intent 使用。通過 setData() 方法向 intent 對象添加 Content URI。然後使用 setFlags() 或者 addFlags() 方法設定讀寫權限,可選常量值同上。這種形式的授權方式,許可權有效期間截止至其它應用所處的堆棧銷毀,並且一旦授權給某一個組件後,該應用的其它組件擁有相同的存取權限。

第五步,提供 Content URI 給其它應用

擁有授予許可權的 Content URI 後,便可以通過 startActivity() 或者 setResult() 方法啟動其他應用並傳遞授權過的 Content URI 資料。當然,也有其他方式提供服務。

如果你需要一次性傳遞多個 URI 對象,可以使用 intent 對象提供的 setClipData() 方法,並且 setFlags() 方法設定的許可權適用於所有 Content URIs。

常見使用情境

前面介紹的內容都是理論部分,在 開發人員官方 FileProvider 部分 都有所介紹。接下來我們看看,實際開發一款應用的過程中,會經常遇見哪些 FileProvider 的使用情境。

自動安裝檔案

版本更新完成時開啟新版本 apk 檔案實現自動安裝的功能,應該是最常見的使用情境,也是每個應用必備功能之一。常見操作為,通知欄顯示下載新版本完畢,使用者點擊或者監聽下載過程自動開啟新版本 apk 檔案。適配 Android 7.0 版本之前,我們代碼可能是這樣:

123456 File apkFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_sample.apk"); Intent installIntent = new Intent(Intent.ACTION_VIEW);installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);installIntent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");startActivity(installIntent);

現在為了適配 7.0 及以上版本的系統,必須使用 Content URI 代替 File URI。

在 res/xml 目錄下建立一個 file_provider_paths.xml 檔案(檔案名稱自由定義),並添加子目錄路徑資訊:

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

然後在 Manifest 檔案中註冊 FileProvider 對象,並連結上面的 path 路徑檔案:

1234567891011 <provider android:name="android.support.v4.content.FileProvider" android:authorities="com.yifeng.samples.myprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_provider_paths"/> </provider>

修改 java 代碼,根據 File 對象產生 Content URI 對象,並授權訪問:

123456789 File apkFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_sample.apk");Uri apkUri = FileProvider.getUriForFile(this,BuildConfig.APPLICATION_ID+".myprovider", apkFile); Intent installIntent = new Intent(Intent.ACTION_VIEW);installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive");startActivity(installIntent);

如此這般,便完成了應用中調用系統功能開啟 apk 檔案的 7.0 適配工作。

調用系統拍照

調用系統拍照功能時也需要傳遞一個 Uri 對象,用於儲存圖片至指定目錄,這裡也需要適配 7.0 版本。其他步驟不再贅述,核心 java 代碼如下(路徑不同,注意添加 res/xml 中的 path 檔案子目錄):

1234567891011 String filePath = Environment.getExternalStorageDirectory() + "/images/"+System.currentTimeMillis()+".jpg";File outputFile = new File(filePath);if (!outputFile.getParentFile().exists()) {outputFile.getParentFile().mkdir();}Uri contentUri = FileProvider.getUriForFile(this,BuildConfig.APPLICATION_ID + ".myprovider", outputFile); Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);startActivityForResult(intent, REQUEST_TAKE_PICTURE);

調用系統裁剪

調用系統裁剪的過程中涉及到兩個 Uri 對象:inputUri 和 outputUri,較為複雜一些。通常,調用系統裁剪的來源為調用系統拍照或選擇系統相簿。前者返回的是一個 File URI 對象,後者返回的是一個 Content URI 對象。作為裁剪源,我們要做的就是對其做進一步處理。但是不能像上面那樣使用 getUriForFile() 方法,這個並不難理解,因為如果是選擇系統相簿所得的圖片,本身也不一定屬於我們自己的應用。正確處理方式是這樣:

123456789101112131415 private Uri getImageContentUri(String path){Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,new String[]{MediaStore.Images.Media._ID},MediaStore.Images.Media.DATA + "=? ",new String[]{path}, null);if (cursor != null && cursor.moveToFirst()) {int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));Uri baseUri = Uri.parse("content://media/external/images/media");return Uri.withAppendedPath(baseUri, ""+id);}else {ContentValues contentValues = new ContentValues(1);contentValues.put(MediaStore.Images.Media.DATA, path);return getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);}}

拿到正確的 Content URI 後,作為 inputUri,傳遞給 Intent 對象:

12345678 Intent intent = new Intent("com.android.camera.action.CROP");intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.setDataAndType(inputUri, "image/*");intent.putExtra("crop", "true");intent.putExtra("aspectX", 1);intent.putExtra("aspectY", 1);intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile));startActivityForResult(intent, REQUEST_PICK);

注意:這裡的 outputUri 並沒有改變,仍然使用的是 Uri.fromFile() 方法擷取的 File URI 類型!這是很奇怪的一點,但是不得不這麼做。事實上,使用這種方式調用系統裁剪功能本身就是有問題的!常見問題如:在部分機型上,調用系統裁剪並返回前一個頁面時,在 onActivityResult() 方法中得到的 resultCode 值不等於 RESULT_OK。Crop Intent 在官方文檔中本來就無跡可尋,本身就是一種不推薦的用法!取而代之的是,我們可以使用 GitHub 上的一些開源庫實現應用內的圖片裁剪功能,比如 uCrop、cropper 等。

曆史版本問題

說了這麼多,還有一個大家比較關心的問題就是:哪些已經上線的舊版本應用沒有做 7.0 適配工作怎麼辦?關於這個問題,Google 已經提前幫我們想好解決方案啦。

還記得 6.0 運行時許可權問題嗎?如果你不想處理運行時許可權事宜的話,只需要在 build.gradle 檔案中將 targetSdkVersion 的值設為 23 以下即可。

同樣的,只要 targetSdkVersion 值小於 24,File URI 的使用依舊可以出現在 7.0 及以上版本的裝置中。不過需要注意的是,如前面所述,調用系統裁剪功能比較特殊,可能會出現一些問題。

雖然 Google 在每次發布新版 Android 系統時,都提供這種設定 targetSdkVersion 的方式相容舊版本,但只是一種臨時解決方案,並不推薦大家使用這種技巧繞開新版本的適配問題。要知道,新出現的 API 改變一定是在解決過去存在的系統問題,是一種進步的表現。遵循規範,是我們每個開發人員開發時都應銘記於心的格言。

補充:就在完成這篇文章之後的一個月,無意間發現部落格大神「鴻洋」也針對 7.0 FileProvider 問題著有一篇。一番觀摩之後,發現該文章的細節分析更加到位,值得後續寫技術類部落格時反思改進。部落格地址:

Android 7.0 行為變更 通過FileProvider在應用間共用檔案吧

本文由 亦楓 創作並首發於 亦楓的個人部落格 ,同步推送公眾號:安卓筆記俠(NiaoTech)。

歡迎各種形式地交流與轉載,註明作者及出處即可。

本文標題為: 關於 Android 7.0 適配中 FileProvider 部分的總結

本文連結為:http://yifeng.studio/2017/05/03/android-7-0-compat-fileprovider/

● 科學上網,FQ推薦:http://buytizi.com/?r=225bb619613ec09e

關於 Android 7.0 適配中 FileProvider 部分的總結

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.