AndroidQ(10)文件适配

1.文件存储方式
Android开发中,简单的概括
有四种方式存储数据和文件:内部存储、外部存储、SharedPreferences 和数据库
内部存储:
/data/user/0/包名/ 目录。
内部存储用来存储应用的私有文件,且通常是应用的功能相关的必要文件,其他应用(包括非root的用户自己)无权直接访问该文件夹,为程序私有。
一般使用getFilesDir() 或 getCacheDir() 方法获取本应用的内部储存路径,读写该路径下的文件不需要申请储存空间读写权限,且卸载应用时会自动删除。
外部储存:
/storage 或 /mnt 目录,/storage/Android/data/<包名>/files 由于系统版本和各个手机厂商的区别,外部储存的路径有些诧异。 外部存储用来存储与app有关的不那么重要的文件,比如一些缓存,图片,文件等。 一般使用getExternalStorageDirectory()方法获取的路径来存取文件。

2.AndroidN(7)动态读写权限
AndroidN 开始引入了动态权限,在程序引入了WRITE_EXTERNAL_STORAGE权限之后,开发者就可以在外部储存随意新建文件。

3.AndroidQ(10)的Scoped Storage方案
AndroidQ开始,官方引出了Scoped Storage(分区存储),规范了开发者对储存方面的权限。
Environment.getExternalStorageDirectory 弃用
Environment.getExternalStoragePublicDirectory 弃用
这两个方法原本是获取手机储存公共部分的缓存地址
现在Android推荐使用该方法
context.getExternalFilesDir();
context.getExternalCacheDir();
路径为:
/storage/Android/data/包名/files,
该目录不需要动态申请STORAGE权限,可以直接读写数据。同样该目录在app卸载的时候,里面的文件也会一同删除。同时,getExternalStorageDirectory已经不好使了,即使有STORAGE权限,也无法创建文件。
这样一来,各个app必须在自己的一亩三分地进行储存数据。

4.AndroidQ的临时适配
AndroidQ的Scoped Storage不是马上强制执行,可通过以下办法临时适配
(是否需要遗留的储存方案)
targetSDK = 29, 默认开启 Scoped Storage, 但可通过在 manifest 里添加 requestLegacyExternalStorage = true 关闭
targetSDK < 29, 默认不开启 Scoped Storage, 但可通过在 manifest 里添加requestLegacyExternalStorage = false 打开

5.适配AndroidQ的图片下载
由于以上原因,app的图片只能储存到各个app的外部储存之中,而所谓的app外部储存无法被外界访问,即下载的图片无法显示在相册之中,目前常用的解决办法是:
api>=29 使用getExternalFilesDir获取路径,将下载完成的图片文件,复制到系统相册数据库(复制完成会自动刷新,不需要发送通知)
api<29,使用getExternalStorageDirectory,直接下载到该文件夹,下载完成刷新相册
当然api<29的情况,也可以直接使用api>=29的那一套方案,看个人情况。
代码如下:

/**
     * 适配 anroid10
     * 通过MediaStore保存,兼容AndroidQ,保存成功自动添加到相册数据库,无需再发送广播告诉系统插入相册
     *
     * @param context     context
     * @param sourceFile  源文件
     *                    saveFileName 保存的文件名
     * @param saveDirName picture子目录
     * @return 成功或者失败
     */
    public static void saveFile2SdCard(Context context, File sourceFile, String saveDirName) {
        String mimeType = getMimeType(sourceFile);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            String fileName = sourceFile.getName();
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.Media.DESCRIPTION, "This is an image");
            values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
            values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + saveDirName);
            ContentResolver contentResolver = context.getContentResolver();
            Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            if (uri == null) {
                ToastUtils.showShort("保存失败");
                return;
            }
            OutputStream out = null;
            FileInputStream fis = null;
            try {
                out = contentResolver.openOutputStream(uri);
                fis = new FileInputStream(sourceFile);
                if (out != null) {
                    FileUtils.copy(fis, out);
                    ToastUtils.showShort("已保存至相册");
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                closeIo(out, fis);
            }
        } else {
            MediaScannerConnection.scanFile(context, new String[]{sourceFile.getPath()},
                    new String[]{mimeType}, new MediaScannerConnection.OnScanCompletedListener() {
                        @Override
                        public void onScanCompleted(String path, Uri uri) {
                        }
                    });
            ToastUtils.showShort("已保存至相册");
        }
    }
private static String getMimeType(File file) {
        FileNameMap fileNameMap = URLConnection.getFileNameMap();
        return fileNameMap.getContentTypeFor(file.getName());
    }
private static void closeIo(Closeable... closeables) {
        if (closeables == null) {
            return;
        }
        for (Closeable closeable : closeables) {
            if (closeable != null) {
                try {
                    closeable.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }