其他分享
首页 > 其他分享> > Android7.0版本后 Uri和文件路径互相转换封装类,实现系统分享功能及 FileProvider详细解析和踩坑指南,与fileprovider生成的Uri无法被识别

Android7.0版本后 Uri和文件路径互相转换封装类,实现系统分享功能及 FileProvider详细解析和踩坑指南,与fileprovider生成的Uri无法被识别

作者:互联网

在调用系统相机、相册时,经常需要进行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会有一些风险,比如:

因此,google提供了FileProvider 类,使用它可以生成content://Uri来替代file://Uri,所以要在应用间共享文件,应发送一项 content:// URI,并授予 URI 临时访问权限。

FileProvider是android support v4包提供的,是ContentProvider的子类,便于将自己app的数据提供给其他app访问。
        在app开发过程中需要用到FileProvider的主要有

使用content://Uri的优点:

file://到content://的转换规则:

a.替换前缀:把file://替换成content://${android:authorities}。
b.匹配和替换

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() 获取到的目录。

TAG Value Path
TAG_ROOT_PATH root-path /
TAG_FILES_PATH files-path /data/data/<包名>/files
TAG_CACHE_PATH cache-path /data/data/<包名>/cache
TAG_EXTERNAL external-path /storage/emulate/0
TAG_EXTERNAL_FILES external-files-path /storage/emulate/0/Android/data/<包名>/files
TAG_EXTERNAL_CACHE external-cache-path /storage/emulate/0/Android/data/<包名>/cache

首先介绍些基础知识: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;
    }

 

接下来以系统分享功能为例,解决“获取资源失败”和fileprovider生成的uri地址,应用不能识别问题,是要把uri地址转换一下。

要调用 Android 系统内建的分享功能,主要有三步流程:

知道大致的实现流程后,其实只要解决下面几个问题后就可以具体实施了。

确定要分享的内容类型

这其实是直接决定了最终的实现形态,我们知道常见的使用场景中,只是为了在应用间分享图片和一些文件,那对于那些只是分享文本的产品而言,两者实现起来要考虑的问题完全不同。

所以为了解决这个问题,我们可以预先定好支持的分享内容类型,针对不同类型可以进行不同的处理。

@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;
    }
}

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

标签:cursor,String,uri,MediaStore,Uri,Android7.0,null,fileprovider
来源: https://blog.csdn.net/P876643136/article/details/88077803