美文网首页程序人生Android开发经验谈Android开发
Android7.0版本后 Uri和文件路径互相转换封装类,实现

Android7.0版本后 Uri和文件路径互相转换封装类,实现

作者: 和平_98c2 | 来源:发表于2019-03-03 12:59 被阅读4次

    在调用系统相机、相册时,经常需要进行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; } }

    最后欢迎大家关注我个人公众号,可以一起交流成长。

    相关文章

      网友评论

        本文标题:Android7.0版本后 Uri和文件路径互相转换封装类,实现

        本文链接:https://www.haomeiwen.com/subject/byupuqtx.html