Android11 文件选择兼容

一.储存

首先,我们需要对Android的储存有所了解
Android储存器可分为内部储存外部储存,这里的内部储存和外部储存不是说有两个物理储存器而是系统在硬盘上划分了两个专用目录用作内部储存和外部储存。简单来说,我们通过系统文件管理器看到的目录都属于外部储存,外部储存又可分为三类目录,私有目录公共目录其它目录,而内部储存对于用户是隐藏的,如数据库、SharedPreferences等文件都放在内部储存中。

内部储存
//内部储存的文件目录获取方法,打印路径:/data/user/0/{应用包名}/files
//
Context.getFilesDir()

//内部储存的缓存目录获取方法,打印路径:/data/user/0/{应用包名}/cache
Context.getCacheDir()

内部储存对应的目录为/data/user/0/{应用包名},该目录下应用有权限进行文件操作,目录对外不可见,应用删除对应的目录也会被删除。

外部储存
1.私有目录
//私有目录的文件目录获取方法,打印路径:/storage/emulated/0/Android/data/{应用包名}/files
//方法参数可选,例如传入Environment.DIRECTORY_PICTURES拿到的目录为/storage/emulated/0/Android/data/com.example.android11/files/Pictures
Context.getExternalFilesDir(null)

//私有目录的缓存目录获取方法,打印路径:/storage/emulated/0/Android/data/{应用包名}/cache
Context.getExternalCacheDir()

私有目录获取和内部储存获取方式类似,都有file和cache目录,且该目录下应用有权限进行文件操作,目录对外可见,应用删除对应的目录也会被删除。
从Android11开始,私有目录不能被外部访问,即使获取了“所有文件管理”权限也不行(当然也是有其它方式可以实现Data目录的访问,不过目前看来并不完美)

2.公共目录

Downloads、Documents、Pictures 、DCIM、Movies、Music、Ringtones等目录都是公共目录,Android11前可以通过文件路径直接访问,Android11后需要通过MediaStore来进行访问。

3.其它目录

外部储存中除了私有目录和公共目录外都是其它目录,Android11后不能直接对其它目录进行访问。

二.分区存储

Android10中已经加入了分区储存机制,不过是非强制的,适配Android10只需在AndroidManifest.xml中添加 android:requestLegacyExternalStorage="true"即可。而在Android11已经强制应用使用分区储存。

三.兼容方式

1.MANAGE_EXTERNAL_STORAGE(不推荐)
<!-- manifest中注册 -->
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />

开启授权页面

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageEmulated()) {
            val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            startActivity(intent)
        }

获取“所有文件管理”的权限可以读写除私有目录外的所有文件,但是这种权限一般为文件管理类的软件才需要申请。一般APP申请此类权限若上架Google,华为等应用市场大概率被拒。

2.SAF(推荐)

应用如果有做文件选择上传类的功能可以使用此方式,通过启动一个系统的文件浏览页面,选择需要的文件后返回一个uri,之后将uri转成流上传或者将通过uri复制文件到私有目录再操作复制后的文件进行上传,这里切记不能直接将uri转成File去进行操作。

//启动SAF文件选择
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");//这里以打开PDF选择为例
startActivityForResult(intent, 10086);
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == 10086 && resultCode == getActivity().RESULT_OK) {
                if (data.getData() != null) {
                  uriToFileApiQ(this, data.getData())
                }
        }
    }
    //将uri对应的文件复制一份到私有目录,之后就可以操作复制后的File了
    @RequiresApi(Build.VERSION_CODES.Q)
    public File uriToFileApiQ(Context context, Uri uri) {
        File file = null;
        if (uri == null) return file;
        //android10以上转换
        if (uri.getScheme().equals(ContentResolver.SCHEME_FILE)) {
            file = new File(uri.getPath());
        } else if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
            //把文件复制到沙盒目录
            ContentResolver contentResolver = context.getContentResolver();
            String displayName = "uritofile"
                    + "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(contentResolver.getType(uri));
            InputStream is = null;
            try {
                is = contentResolver.openInputStream(uri);
                File cache = new File(context.getCacheDir().getAbsolutePath(), displayName);
                FileOutputStream fos = new FileOutputStream(cache);
                byte[] b = new byte[1024];
                while ((is.read(b)) != -1) {
                    fos.write(b);// 写入数据
                }
                file = cache;
                fos.close();
                is.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return file;
    }

推荐阅读更多精彩内容