Android7.0版本後 Uri和檔案路徑互相轉換封裝類,實現系統分享功能及 FileProvider詳細解析和踩坑...
在呼叫系統相機、相簿時,經常需要進行Uri和File路徑的互相轉換,並且在專案中遇到按照百度查到的處理7.0方法分享檔案到微信的7.0之後版本會檔名字尾被增加了..octet.stream無法解決,最終使用強制轉換方法解決問題。
檔案路徑轉Uri
Android 7.0以下,以檔案路徑建立一個File物件,然後呼叫Uri.fromFile(file)即可獲得相應的Uri。
File photoOutputFile = SDPath.getFile("temp.jpg", SDPath.PHOTO_FILE_STR); Uri photoOutputUri = Uri.fromFile(photoOutputFile);
但是在Android 7.0 (N) 以上,對於面向 Android 7.0 的應用,Android 框架執行的 StrictMode API 政策禁止在應用外部公開 file:// URI,即當把targetSdkVersion指定成24及之上並且在API>=24的裝置上執行時,如果一項包含檔案 URI 的 intent 離開應用(如分享),則應用出現故障,並出現 FileUriExposedException 異常。
android.os.FileUriExposedException: file:///XXX 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文件如下

原因在於使用file://Uri會有一些風險,比如:
檔案是私有的,接收file://Uri的app無法訪問該檔案。
在Android6.0之後引入執行時許可權,如果接收file://Uri的app沒有申請READ_EXTERNAL_STORAGE許可權,在讀取檔案時會引發崩潰。
因此,google提供了FileProvider 類,使用它可以生成content://Uri來替代file://Uri,所以要在應用間共享檔案,應傳送一項 content:// URI,並授予 URI 臨時訪問許可權。
FileProvider是android support v4包提供的,是ContentProvider的子類,便於將自己app的資料提供給其他app訪問。
在app開發過程中需要用到FileProvider的主要有
相機拍照以及圖片裁剪
呼叫系統應用安裝器安裝apk(應用升級)
分享檔案
使用content://Uri的優點:
它可以控制共享檔案的讀寫許可權,只要呼叫Intent.setFlags()就可以設定對方app對共享檔案的訪問許可權,並且該許可權在對方app退出後自動失效。相比之下,使用file://Uri時只能通過修改檔案系統的許可權來實現訪問控制,這樣的話訪問控制是它對所有 app都生效的,不能區分app。
它可以隱藏共享檔案的真實路徑。
file://到content://的轉換規則:
a.替換字首:把file://替換成content://${android:authorities}。
b.匹配和替換遍歷<paths>的子節點,找到最大能匹配上檔案路徑字首的那個子節點。用path的值替換掉檔案路徑裡所匹配的內容。
c.檔案路徑剩餘的部分保持不變.

解決方案
①定義FileProvider。在AndroidManifest.xml中加上自定義許可權的ContentProvider,在<application>節點中新增<provider>如下
<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.php.demo.FileProvider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>
說明:
android:authorities="com.php.demo.FileProvider" 用來標識provider的唯一標識,在同一部手機上一個"authority"串只能被一個app使用,衝突的話會導致app無法安裝。我們可以利用manifest placeholders(包名)來保證authority的唯一性。
android:exported="false" 是否設定為獨立程序,必須設定成false,否則執行時會報錯java.lang.SecurityException: Provider must not be exported。
android:grantUriPermissions="true" 是否擁有共享檔案的臨時許可權,也可以在java程式碼中設定。
android:resource="@xml/external_storage_root" 共享檔案的檔案根目錄,名字可以自定義
②指定路徑和轉換規則。FileProvider會隱藏共享檔案的真實路徑,將它轉換成content://Uri路徑,因此,我們還需要設定轉換的規則。在專案res目錄下建立一個xml資料夾,裡面建立一個file_paths.xml檔案,上一步定義的什麼名稱,這裡就什麼名稱,如圖:


<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_storage_root" path="." />
<files-path name="files-path" path="." />
<cache-path name="cache-path" path="." />
<!--/storage/emulated/0/Android/data/...-->
<external-files-path name="external_file_path" path="." />
<!--代表app 外部儲存區域根目錄下的檔案 Context.getExternalCacheDir目錄下的目錄-->
<external-cache-path name="external_cache_path" path="." />
<!--配置root-path。這樣子可以讀取到sd卡和一些應用分身的目錄,否則微信分身儲存的圖片,就會導致 java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/999/tencent/MicroMsg/WeiXin/export1544062754693.jpg,在小米6的手機上微信分身有這個crash,華為沒有 -->
<root-path name="root-path" path="" /> /paths>
這個配置的標籤參照FileProvider裡面的TAG配置。

root-path 對應DEVICE_ROOT,也就是File DEVICE_ROOT = new File("/"),即根目錄,一般不需要配置。
files-path對應 content.getFileDir() 獲取到的目錄。
cache-path對應 content.getCacheDir() 獲取到的目錄
external-path對應 Environment.getExternalStorageDirectory() 指向的目錄。
external-files-path對應 ContextCompat.getExternalFilesDirs() 獲取到的目錄。
external-cache-path對應 ContextCompat.getExternalCacheDirs() 獲取到的目錄。

首先介紹些基礎知識:Android的檔案系統和MediaStore類的使用
外部儲存的公共目錄
DIRECTORY_MUSIC:音樂型別 /storage/emulate/0/music
DIRECTORY_PICTURES:圖片型別
DIRECTORY_MOVIES:電影型別
DIRECTORY_DCIM:照片型別,相機拍攝的照片視訊都在這個目錄(digital camera in memory) /storage/emulate/0/DCIM
DIRECTORY_DOWNLOADS:下載檔案型別 /storage/emulate/0/downloads
DIRECTORY_DOCUMENTS:文件型別
DIRECTORY_RINGTONES:鈴聲型別
DIRECTORY_ALARMS:鬧鐘提示音型別
DIRECTORY_NOTIFICATIONS:通知提示音型別
DIRECTORY_PODCASTS:播客音訊型別
這些可以通過Environment的getExternalStoragePublicDirectory()來獲取
安卓系統會在每次開機之後掃描所有檔案並分類整理存入資料庫,記錄在MediaStore這個類裡,通過這個類就可以快速的獲得相應型別的檔案。當然這個類只是給你一個uri,提取檔案的操作還是要通過Curosr這個類來完成。獲得Cursor物件例項的方法必須通過Context例項獲得ContextResolver物件,通過這個物件呼叫query方法。
就是這樣 mycontext.getContentResolver().query(uri, columns, selection, null, null);
mycontext通過活動例項獲取,其他的就沒必要說了 說說引數(官方文件裡有詳細說明),第一個就是uri說白了就是地址,第二個是選擇哪些列(列的名字在官方文件裡有需要哪個寫那個就夠了),第三個是選擇指定的行一般都是通過mimetype去選擇(傳入的引數是sql語句的字串),第四個沒用過,第五個就是排序的要求和第三個差不多 注意前三個引數有點問題就會空指標。
下面貼一下通過MediaStore類獲得URI的程式碼
private Uri getContentUri(FileCategory cat) { Uri uri; String volumeName = "external"; switch(cat) { case Theme: case Doc: case Zip: case Apk: uri = Files.getContentUri(volumeName); break; case Music: uri = Audio.Media.getContentUri(volumeName); break; case Video: uri = Video.Media.getContentUri(volumeName); break; case Picture: uri = Images.Media.getContentUri(volumeName); break; default: uri = null; } Log.e(LOG_CURSOR, "getContentUri"); return uri; }
---------------------
作者:peihp_
來源:CSDN
原文:https://blog.csdn.net/P876643136/article/details/88077803
版權宣告:本文為博主原創文章,轉載請附上博文連結!
接下來以系統分享功能為例,解決“獲取資源失敗”和fileprovider生成的uri地址,應用不能識別問題,是要把uri地址轉換一下。
要呼叫 Android 系統內建的分享功能,主要有三步流程:
建立一個 Intent ,指定其 Action 為 Intent.ACTION_SEND,表示要建立一個傳送指定內容的隱式意圖。
然後指定需要傳送的內容和型別,設定分享的文字內容或檔案的Uri,以及檔案的型別,便於是支援該型別內容的應用開啟。
最後向系統傳送隱式意圖,開啟系統分享選擇器,分享完成後收到結果返回。
知道大致的實現流程後,其實只要解決下面幾個問題後就可以具體實施了。
確定要分享的內容型別
這其實是直接決定了最終的實現形態,我們知道常見的使用場景中,只是為了在應用間分享圖片和一些檔案,那對於那些只是分享文字的產品而言,兩者實現起來要考慮的問題完全不同。
所以為了解決這個問題,我們可以預先定好支援的分享內容型別,針對不同型別可以進行不同的處理。
@StringDef({ShareContentType.TEXT, ShareContentType.IMAGE, ShareContentType.AUDIO, ShareContentType.VIDEO, ShareContentType.File}) @Retention(RetentionPolicy.SOURCE) @interface ShareContentType { /** * Share Text */ final String TEXT = "text/plain"; /** * Share Image */ final String IMAGE = "image/*"; /** * Share Audio */ final String AUDIO = "audio/*"; /** * Share Video */ final String VIDEO = "video/*"; /** * Share File */ final String File = "*/*"; }`
上述一共定義了5種類別的分享內容,基本能覆蓋常見的使用場景。在呼叫分享介面時可以直接指定內容型別,比如像文字、圖片、音視訊、及其他各種型別檔案。
確定分享的內容來源
比如呼叫系統相機進行拍照或錄製音視訊,要傳入一個生成目標檔案的Uri
private static final int REQUEST_FILE_SELECT_CODE = 100; /** * 開啟系統相機進行拍照 */ private void openSystemCamera() { //呼叫系統相機 Intent takePhotoIntent = new Intent(); takePhotoIntent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); if (takePhotoIntent.resolveActivity(getPackageManager()) == null) { Toast.makeText(this, "當前系統沒有可用的相機應用", Toast.LENGTH_SHORT).show(); return; } String fileName = "TEMP_" + System.currentTimeMillis() + ".jpg"; File photoFile = new File(FileUtil.getPhotoCacheFolder(), fileName); // 7.0和以上版本的系統要通過 FileProvider 建立一個 content 型別的 Uri if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { currentTakePhotoUri = FileProvider.getUriForFile(this, getPackageName() + ".fileProvider", photoFile); takePhotoIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|); } else { currentTakePhotoUri = Uri.fromFile(photoFile); } //將拍照結果儲存至 outputFile 的Uri中,不保留在相簿中 takePhotoIntent.putExtra(MediaStore.EXTRA_OUTPUT, currentTakePhotoUri); startActivityForResult(takePhotoIntent, TAKE_PHOTO_REQUEST_CODE); } // 呼叫系統相機進行拍照與上面通過檔案選擇器獲得檔案 uri 的方式類似 // 在 onActivityResult 進行回撥處理,此時 Uri 是你 FileProvider 中指定的,注意與檔案選擇器獲取的 Uri 的區別。
分享檔案 Uri 的處理
要對應用進行臨時訪問 Uri 的授權才行,不然會提示許可權缺失。對於要分享系統返回的 Uri 我們可以這樣進行處理:
// 可以對發起分享的 Intent 新增臨時訪問授權 shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 也可以這樣:由於不知道終端使用者會選擇哪個app,所以授予所有應用臨時訪問許可權 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { List<ResolveInfo> resInfoList = activity.getPackageManager().queryIntentActivities(shareIntent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; activity.grantUriPermission(packageName, shareFileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } }
需要注意的是對於自定義 FileProvider 返回 Uri 的處理,即使是設定臨時訪問許可權,但是分享到第三方應用也會無法識別該 Uri
典型的場景就是,我們如果把自定義 FileProvider 的返回的 Uri 設定分享到微信或 QQ 之類的第三方應用,會提示檔案不存在,這是因為他們無法識別該 Uri。
關於這個問題的處理其實跟下面要說的把檔案路徑變成系統返回的 Uri 一樣,我們只需要把自定義 FileProvider 返回的 Uri 變成第三方應用可以識別系統返回的 Uri 就行了。
建立 FileProvider 時需要傳入一個 File 物件,所以直接可以知道檔案路徑,那就把問題都轉換成了:如何通過檔案路徑獲取系統返回的 Uri
本人在專案中獲取本地檔案如下:
Intent share = new Intent(Intent.ACTION_SEND); File file = new File(filePath); Uri contentUri = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { share.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); contentUri = DealFileClass.getFileUri(getActivity(),DealFileClass.ShareContentType.File,file); share.putExtra(Intent.EXTRA_STREAM, contentUri); share.setType("application/pdf");// 此處可傳送多種檔案 } else { share.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)); share.setType("application/pdf");// 此處可傳送多種檔案 } try{ startActivity(Intent.createChooser(share, "Share")); } catch (Exception e) { e.printStackTrace(); }
下面是根據傳入的 File 物件和型別來查詢系統 ContentProvider 來獲取相應的 Uri,已經按照不同檔案型別在不同系統版本下的進行了適配。
其中 forceGetFileUri 方法是通過反射實現的,處理 7.0 以上系統的特殊情況下的相容性,一般情況下不會呼叫到。Android 7.0 開始不允許 file:// Uri 的方式在不同的 App 間共享檔案,但是如果換成 FileProvider 的方式依然是無效的,我們可以通過反射把該檢測幹掉。
public static Uri getFileUri (Context context, @ShareContentType String shareContentType, File file){ if (context == null) { Log.e(TAG,"getFileUri current activity is null."); return null; } if (file == null || !file.exists()) { Log.e(TAG,"getFileUri file is null or not exists."); return null; } Uri uri = null; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { uri = Uri.fromFile(file); } else { if (TextUtils.isEmpty(shareContentType)) { shareContentType = "*/*"; } switch (shareContentType) { case ShareContentType.IMAGE : uri = getImageContentUri(context, file); break; case ShareContentType.VIDEO : uri = getVideoContentUri(context, file); break; case ShareContentType.AUDIO : uri = getAudioContentUri(context, file); break; case ShareContentType.File : uri = getFileContentUri(context, file); break; default: break; } } if (uri == null) { uri = forceGetFileUri(file); } return uri; } private static Uri getFileContentUri(Context context, File file) { String volumeName = "external"; String filePath = file.getAbsolutePath(); String[] projection = new String[]{MediaStore.Files.FileColumns._ID}; Uri uri = null; Cursor cursor = context.getContentResolver().query(MediaStore.Files.getContentUri(volumeName), projection, MediaStore.Images.Media.DATA + "=? ", new String[] { filePath }, null); if (cursor != null) { if (cursor.moveToFirst()) { int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)); uri = MediaStore.Files.getContentUri(volumeName, id); } cursor.close(); } return uri; } private static Uri getImageContentUri(Context context, File imageFile) { String filePath = imageFile.getAbsolutePath(); Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[] { MediaStore.Images.Media._ID }, MediaStore.Images.Media.DATA + "=? ", new String[] { filePath }, null); Uri uri = null; if (cursor != null) { if (cursor.moveToFirst()) { int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); Uri baseUri = Uri.parse("content://media/external/images/media"); uri = Uri.withAppendedPath(baseUri, "" + id); } cursor.close(); } if (uri == null) { ContentValues values = new ContentValues(); values.put(MediaStore.Images.Media.DATA, filePath); uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); } return uri; } private static Uri getVideoContentUri(Context context, File videoFile) { Uri uri = null; String filePath = videoFile.getAbsolutePath(); Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, new String[] { MediaStore.Video.Media._ID }, MediaStore.Video.Media.DATA + "=? ", new String[] { filePath }, null); if (cursor != null) { if (cursor.moveToFirst()) { int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); Uri baseUri = Uri.parse("content://media/external/video/media"); uri = Uri.withAppendedPath(baseUri, "" + id); } cursor.close(); } if (uri == null) { ContentValues values = new ContentValues(); values.put(MediaStore.Video.Media.DATA, filePath); uri = context.getContentResolver().insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values); } return uri; } private static Uri getAudioContentUri(Context context, File audioFile) { Uri uri = null; String filePath = audioFile.getAbsolutePath(); Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, new String[] { MediaStore.Audio.Media._ID }, MediaStore.Audio.Media.DATA + "=? ", new String[] { filePath }, null); if (cursor != null) { if (cursor.moveToFirst()) { int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID)); Uri baseUri = Uri.parse("content://media/external/audio/media"); uri = Uri.withAppendedPath(baseUri, "" + id); } cursor.close(); } if (uri == null) { ContentValues values = new ContentValues(); values.put(MediaStore.Audio.Media.DATA, filePath); uri = context.getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values); } return uri; } private static Uri forceGetFileUri(File shareFile) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { try { @SuppressLint("PrivateApi") Method rMethod = StrictMode.class.getDeclaredMethod("disableDeathOnFileUriExposure"); rMethod.invoke(null); } catch (Exception e) { Log.e(TAG, Log.getStackTraceString(e)); } } return Uri.parse("file://" + shareFile.getAbsolutePath()); }
此外,如若uri要轉換為檔案路徑則可如下處理:
Intent intent = getIntent(); String action = intent.getAction(); if (Intent.ACTION_VIEW.equals(action)) { Uri uri = intent.getData(); String filename = uri.getPath(); if (String.valueOf(uri) != null && String.valueOf(uri).contains("content")) { boolean kkk = false; try{ filename = CommonUtils.getFilePathFromContentUri(uri,this.getContentResolver()); if(CommonUtils.isEmpty(filename)){ kkk = true; } }catch (Exception e){ e.printStackTrace(); kkk = true; } if(kkk){ filename = ProviderUtils.getFPUriToPath(this,uri); } } }
其中,getFilePathFromContentUri如下:
/** * 將uri轉換成真實路徑 * * @param selectedVideoUri * @param contentResolver * @return */ public static String getFilePathFromContentUri(Uri selectedVideoUri, ContentResolver contentResolver) { String filePath = ""; String[] filePathColumn = {MediaColumns.DATA}; Cursor cursor = contentResolver.query(selectedVideoUri, filePathColumn, null, null, null); // 也可用下面的方法拿到cursor // Cursor cursor = this.context.managedQuery(selectedVideoUri, // filePathColumn, null, null, null); // cursor.moveToFirst(); // // int columnIndex = cursor.getColumnIndex(filePathColumn[0]); // filePath = cursor.getString(columnIndex); if (cursor != null) { if (cursor.moveToFirst()) { int id = cursor.getColumnIndex(filePathColumn[0]); if(id > -1) filePath = cursor.getString(id); } cursor.close(); } return filePath; }
ProviderUtils類檔案內容如下:
public class ProviderUtils { public static String getFPUriToPath(Context context, Uri uri) { try { List<PackageInfo> packs = context.getPackageManager().getInstalledPackages(PackageManager.GET_PROVIDERS); if (packs != null) { String fileProviderClassName = FileProvider.class.getName(); for (PackageInfo pack : packs) { ProviderInfo[] providers = pack.providers; if (providers != null) { for (ProviderInfo provider : providers) { if (uri.getAuthority().equals(provider.authority)) { if (provider.name.equalsIgnoreCase(fileProviderClassName)) { Class<FileProvider> fileProviderClass = FileProvider.class; try { Method getPathStrategy = fileProviderClass.getDeclaredMethod("getPathStrategy", Context.class, String.class); getPathStrategy.setAccessible(true); Object invoke = getPathStrategy.invoke(null, context, uri.getAuthority()); if (invoke != null) { String PathStrategyStringClass = FileProvider.class.getName() + "$PathStrategy"; Class<?> PathStrategy = Class.forName(PathStrategyStringClass); Method getFileForUri = PathStrategy.getDeclaredMethod("getFileForUri", Uri.class); getFileForUri.setAccessible(true); Object invoke1 = getFileForUri.invoke(invoke, uri); if (invoke1 instanceof File) { String filePath = ((File) invoke1).getAbsolutePath(); return filePath; } } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } break; } break; } } } } } } catch (Exception e) { e.printStackTrace(); } return null; } }
最後歡迎大家關注我個人公眾號,可以一起交流成長。
