TargetSdk 30 的存储记录

  • 前言
  • 一、分区存储是什么?
  • 二、应用分区存储的文件访问规定
  • 1.应用专属目录
  • 2.其他应用的专属目录
  • 3.媒体资源文件
  • 4.文件和目录访问限制
  • 4.1.访问目录
  • 4.2.访问文件
  • 4.3.SAF的限制性
  • 三、全部文件的访问的限制性
  • 测试环境
  • 参考地址



前言


在 Android 10 的时候,Google 为开发者考虑,并没有强制开启分区存储。可以在targetSdkVersion = 29应用中,设置android:requestLegacyExternalStorage="true",这样就可以不启动分区存储,让以前的文件读取正常使用。这给予开发者更多的适配时间。 但是现在已经 targetSdkVersion = 30 了,分区存储被强制开启了 但是依然有android:preserveLegacyExternalStorage="true"可以暂时关闭分区存储,卸载重装后失效。


一、分区存储是什么?

首先,我们要知道对于开发者来说,android应用存储空间从逻辑上划分为内部存储和外部存储。而分区存储就是对外部存储的进一步严格要求。

分区存储机制会让APP在外部存储空间有属于自己的专属存储区域,这块区域是私有的,因此,这块区域非常适用于保存用户的私有数据。一般情况下这块私有区域不会暴露给其他应用。

二、应用分区存储的文件访问规定

1.应用专属目录

每个应用向自己的私有目录读写文件,不需要读写权限。
应用即使获取了读写权限,也无法访问其他应用的私有目录。
私有文件目录具体路径:storage/emulated/0/android/data/packageName/…

访问方式:
this.getExternalMediaDirs() ==[/storage/emulated/0/Android/media/com.yoshin.tspsdk]
this.getExternalCacheDir() ==/storage/emulated/0/Android/data/com.yoshin.tspsdk/cache
this.getExternalFilesDir(Environment.DIRECTORY_SCREENSHOTS) ==/storage/emulated/0/Android/data/com.yoshin.tspsdk/files/Screenshots

其中,这几个方法在调用的时候,如果还没有对应文件夹,都会进行创建。
Environment.DIRECTORY_SCREENSHOTS 是可以被替他参数代替的,就会访问其他文件夹了,然后低版本会有异常,不建议这么用。

2.其他应用的专属目录

访问其他应用的专属目录并不需要权限,但是需要其他应用共享,使用:
ParcelFileDescriptor 与 FileDescriptor 就可以实现

3.媒体资源文件

Android 11 访问自己的媒体文件并不需要权限,但是其他应用的媒体文件就需要READ_EXTERNAL_STORAGE权限,否则查询不到其他媒体文件。
Android 低版本查询媒体文件就需要权限,所以最好一直设置READ_EXTERNAL_STORAGE权限
访问方式:MediaStore API 接口定义地址:

https://developer.android.google.cn/reference/android/provider/MediaStore

如何获取音频文件:

ContentResolver contentResolver = this.getContentResolver();

        Cursor cursor = contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                null, null, null,   null);
        if (cursor != null) {

            if (cursor.getCount() >0){
                LogUtils.i(TAG, "audio > 0");
            }

            while (cursor.moveToNext()) {
                long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
                Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
                LogUtils.i(TAG, "audio uri is  = " + uri);
            }
            cursor.close();
        }

Log

StroageActivity: audio > 0
StroageActivity: audio uri is  = content://media/external/audio/media/432
StroageActivity: audio uri is  = content://media/external/audio/media/434
StroageActivity: audio uri is  = content://media/external/audio/media/441

使用uri即可播放音频文件

图片:MediaStore.Images.Media.EXTERNAL_CONTENT_URI
视频:MediaStore.Video.Media.EXTERNAL_CONTENT_URI
音乐:MediaStore.Audio.Media.EXTERNAL_CONTENT_URI

如果是查询图片,使用MediaStore.Images.Media.EXTERNAL_CONTENT_URI 查询即可,若要使用查询出来uri,需要将它转换成io,再转换成bitmap

((ImageView) findViewById(R.id.iv)).setImageBitmap(getBitmapFormUri(StroageActivity.this, uri));

转换和压缩的方法

/**
     * 通过uri获取图片并进行压缩
     *
     * @param uri
     */
    public static Bitmap getBitmapFormUri(Activity ac, Uri uri) throws FileNotFoundException, IOException {
        InputStream input = ac.getContentResolver().openInputStream(uri);
        BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
        onlyBoundsOptions.inJustDecodeBounds = true;
        onlyBoundsOptions.inDither = true;//optional
        onlyBoundsOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
        BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
        input.close();
        int originalWidth = onlyBoundsOptions.outWidth;
        int originalHeight = onlyBoundsOptions.outHeight;
        if ((originalWidth == -1) || (originalHeight == -1))
            return null;
        //图片分辨率以480x800为标准
        float hh = 800f;//这里设置高度为800f
        float ww = 480f;//这里设置宽度为480f
        //缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
        int be = 1;//be=1表示不缩放
        if (originalWidth > originalHeight && originalWidth > ww) {//如果宽度大的话根据宽度固定大小缩放
            be = (int) (originalWidth / ww);
        } else if (originalWidth < originalHeight && originalHeight > hh) {//如果高度高的话根据宽度固定大小缩放
            be = (int) (originalHeight / hh);
        }
        if (be <= 0) {
            be = 1;
        }
        //比例压缩
        BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
        bitmapOptions.inSampleSize = be;//设置缩放比例
        bitmapOptions.inDither = true;//optional
        bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
        input = ac.getContentResolver().openInputStream(uri);
        Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
        input.close();

        return compressImage(bitmap);//再进行质量压缩
    }

    /**
     * 质量压缩方法
     *
     * @param image
     * @return
     */
    public static Bitmap compressImage(Bitmap image) {

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        int options = 100;
        while (baos.toByteArray().length / 1024 > 100) {  //循环判断如果压缩后图片是否大于100kb,大于继续压缩
            baos.reset();//重置baos即清空baos
            //第一个参数 :图片格式 ,第二个参数: 图片质量,100为最高,0为最差  ,第三个参数:保存压缩后的数据的流
            image.compress(Bitmap.CompressFormat.JPEG, options, baos);//这里压缩options%,把压缩后的数据存放到baos中
            options -= 10;//每次都减少10
        }
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中
        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream数据生成图片
        return bitmap;
    }

如果对图片品质要求不高,Bitmap.Config.ARGB_8888 替换成 565,可以节省内存占用。

4.文件和目录访问限制

SAF地址:https://developer.android.google.cn/training/data-storage/shared/documents-files

对文件和目录访问使用 SAF (存储访问框架–Storage Access Framework),SAF访问方式不需要申请权限

4.1.访问目录

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
                startActivityForResult(intent, 101);
@RequiresApi(Build.VERSION_CODES.KITKAT)
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (data == null || resultCode != Activity.RESULT_OK) {
            return;
        }
        if (requestCode == 101) {
            Uri uri = data.getData();
        }
    }

4.2.访问文件

如下是访问图片资源

Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                intent.setType("image/*");
                startActivityForResult(intent, 100);
@RequiresApi(Build.VERSION_CODES.KITKAT)
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (data == null || resultCode != Activity.RESULT_OK) {
            return;
        }
        if (requestCode == 100) {
            Uri uri = data.getData();
            try {
                ((ImageView) findViewById(R.id.iv_2)).setImageBitmap(getBitmapFormUri(StroageActivity.this, uri));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

除此之外,资源文件还有很多种可以访问:

enum class MimeType(val value: String) {
     //todo 发现新的音频文件有aac格式,这里没有
    _png("image/png"),
    _jpeg("image/jpeg"),
    _jpg("image/jpeg"),
    _webp("image/webp"),
    _gif("image/gif"),
    _bmp("image/bmp"),
    _3gp("video/3gpp"),
    _apk("application/vnd.android.package-archive"),
    _asf("video/x-ms-asf"),
    _avi("video/x-msvideo"),
    _bin("application/octet-stream"),
    _c("text/plain"),
    _class("application/octet-stream"),
    _conf("text/plain"),
    _cpp("text/plain"),
    _doc("application/msword"),
    _docx("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
    _xls("application/vnd.ms-excel"),
    _xlsx("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
    _exe("application/octet-stream"),
    _gtar("application/x-gtar"),
    _gz("application/x-gzip"),
    _h("text/plain"),
    _htm("text/html"),
    _html("text/html"),
    _jar("application/java-archive"),
    _java("text/plain"),
    _js("application/x-javascript"),
    _log("text/plain"),
    _m3u("audio/x-mpegurl"),
    _m4a("audio/mp4a-latm"),
    _m4b("audio/mp4a-latm"),
    _m4p("audio/mp4a-latm"),
    _m4u("video/vnd.mpegurl"),
    _m4v("video/x-m4v"),
    _mov("video/quicktime"),
    _mp2("audio/x-mpeg"),
    _mp3("audio/x-mpeg"),
    _mp4("video/mp4"),
    _mpc("application/vnd.mpohun.certificate"),
    _mpe("video/mpeg"),
    _mpeg("video/mpeg"),
    _mpg("video/mpeg"),
    _mpg4("video/mp4"),
    _mpga("audio/mpeg"),
    _msg("application/vnd.ms-outlook"),
    _ogg("audio/ogg"),
    _pdf("application/pdf"),
    _pps("application/vnd.ms-powerpoint"),
    _ppt("application/vnd.ms-powerpoint"),
    _pptx("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
    _prop("text/plain"),
    _rc("text/plain"),
    _rmvb("audio/x-pn-realaudio"),
    _rtf("application/rtf"),
    _sh("text/plain"),
    _tar("application/x-tar"),
    _tgz("application/x-compressed"),
    _txt("text/plain"),
    _wav("audio/x-wav"),
    _wma("audio/x-ms-wma"),
    _wmv("audio/x-ms-wmv"),
    _wps("application/vnd.ms-works"),
    _xml("text/plain"),
    _z("application/x-compress"),
    _zip("application/x-zip-compressed"),
    _0("*/*"),
    ;

    companion object {
        fun isImage(mimeType: String?): Boolean {
            return mimeType?.let {
                _webp.value == mimeType ||
                        _png.value == mimeType ||
                        _jpeg.value == mimeType ||
                        _jpg.value == mimeType ||
                        _bmp.value == mimeType ||
                        _gif.value == mimeType
            } ?: false
        }

        fun isGif(mimeType: String?): Boolean {
            return mimeType?.let {
                _gif.value == mimeType
            } ?: false
        }

        fun isApk(mimeType: String?) = mimeType?.let {
            _apk.value == mimeType
        } ?: false

        fun isVideo(mimeType: String?) = mimeType?.let {
            _m3u.value == mimeType || _avi.value == mimeType
        } ?: false
    }
}

4.3.SAF的限制性

targetSdk = 30,限制了SAF的一些操作

  1. 访问目录
    使用 ACTION_OPEN_DOCUMENT_TREE intent 操做来请求访问目录:
    无法获取 Downloads 根目录
    无法获取根目录
    Android/data/ 目录及其全部子目录不可见,所以更无法获取
    Android/obb/ 目录及其全部子目录不可见,所以更无法获取
  2. 访问文件
    使用 ACTION_OPEN_DOCUMENT intent 操做来请求选择文件:
    Android/data/ 目录及其全部子目录不可见,所以更无法选择
    Android/obb/ 目录及其全部子目录不可见,所以更无法选择

如果您的应用需要访问单个文件,比如文字处理应用,则应该使用 Storage Access Framework (SAF)。

三、全部文件的访问的限制性

像文件管理器、手机助手之类的APP依然会需要访问所有文件,虽然手机要求分区存储,但是依然有提供申请全部文件的权限和方法。当然,肯定还是有些不同的,这里提供的所有文件不再是手机内的全部文件,现在的所有文件是:

  1. 共享的存储空间内全部文件的读写访问权限。
    转载自共享内存空间的组成:https://developer.android.google.cn/training/data-storage/shared

使用共享存储的用户数据,可以或应该被其他应用程序访问,并保存,即使用户卸载你的应用程序。
Android提供api来存储和访问以下类型的可共享数据:
媒体内容:系统为这些类型的文件提供了标准的公共目录,因此用户有一个用于存放所有照片的公共位置,另一个用于存放所有音乐和音频文件的公共位置,等等。你的应用程序可以使用平台的MediaStore API访问这些内容。
文档和其他文件:系统有一个特殊的目录用于包含其他类型的文件,例如使用EPUB格式的PDF文档和书籍。你的应用程序可以使用平台的存储访问框架访问这些文件。
数据集:在Android 11 (API级别30)和更高,系统缓存大的数据集,多个应用程序可能使用。这些数据集可以支持机器学习和媒体回放等用例。应用程序可以使用BlobStoreManager API访问这些共享数据集。

  1. MediaStore.Files 表的内容的访问权限。

申请“全部文件”的方法:

//申请 
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />     

        //有没有全部的权限
        boolean isExternalStorageManager = Environment.isExternalStorageManager();
        
				//没有的话,让用户去授权,跳转到授权页面
                Intent intent = new Intent();
                intent.setAction(ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                startActivity(intent);

当你申请完全部文件的权限,访问的时候,通过:

  1. Media API
  2. 原始文件路径
  3. 存储访问框架SAF

都可以访问上述“所有文件”资源,但是其他应用的私有路径依然不可访问。
我这里理解是即使有所有的权限,4.3.SAF的限制性依然存在

注:对于全部文件的访问权限具体和读写权限差在哪里,不清楚,希望可以留言交流,文章持续更新


测试环境

博客是在环境:
android = [
compileSdkVersion: 30,
buildToolsVersion: “30.0.2”,
minSdkVersion : 21,
targetSdkVersion : 30,
versionCode : 1,
versionName : “1.0”
]
下进行测试和验证。

参考地址

Android11新特性及部分适配
https://www.jianshu.com/p/a228f6a46354

Android11最全适配实践指南–应用端https://www.jianshu.com/p/f5796aead731

https://developer.android.google.cn/about/versions/11/behavior-changes-all