Android 10、11 存储完全适配

android 11 存储权限,Android 11存储权限强制申请 private static final int REQUEST_CODE = 1024;private void requestPermission() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {// 先判断有没有权限if (Environment.isExternalStorageManager()) {Log.d("权限... 阅读详情
转载: 王朋6     Android 10、11 存储完全适配 - 简书
一、存储基本知识

先来看看存储区域划分:

其中,以下目录无需存储权限即可访问:

1、App自身的内部存储

2、App自身的自带外部存储-私有目录

剩下的都需要申请存储权限,Android 10.0前后对于存储作用域访问的区别就体现在如何访问剩余这些目录内的文件。

重点在自带外部存储之共享存储空间和其它目录

2、Android 10.0 之前访问方式

继续细分为Android 6.0 之前和之后。

Android 6.0 之前访问方式

Android 6.0 之前是无需申请动态权限的,在AndroidManifest.xml 里声明存储权限:

< uses-permissionandroid:name= "android.permission.WRITE_EXTERNAL_STORAGE"/> 

< uses-permissionandroid:name= "android.permission.READ_EXTERNAL_STORAGE"/>

就可以访问共享存储空间、其它目录下的文件了。

Android 6.0 之后的访问方式动态申请权限

Android 6.0 后需要动态申请权限,除了在AndroidManifest.xml 里声明存储权限外,还需要在代码里动态申请。

//检查权限,并返回需要申请的权限列表

 private List checkPermission( Context context, String[] checkList) { 

     List list = new ArrayList();

     for( inti = 0; i < checkList.length; i++) { 

     if( PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(context, checkList[i]))   {                                                           list. add(checkList[i]);

        } 

     } 

     return list; 

}

//申请权限

private void requestPermission( Activity activity, String requestPermissionList[]) {

   ActivityCompat.requestPermissions(activity, requestPermissionList, 100); 

}

//用户作出选择后,返回申请的结果

@ Override

public void onRequestPermissionsResult( intrequestCode, @NonNull String[] permissions, @NonNull int[] grantResults ) { 

if(requestCode == 100) { 

      for( inti = 0; i < permissions.length; i++) {

             if(permissions[i]. equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { 

                   if(grantResults[i] == PackageManager.PERMISSION_GRANTED) { 

                        Toast.makeText(MainActivity. this, "存储权限申请成功", Toast.LENGTH_SHORT).show; 

                    } else{

                        Toast.makeText(MainActivity. this, "存储权限申请失败", Toast.LENGTH_SHORT).show;

                    }

           }

       }

 }

}

//测试申请存储权限

private void testPermission( Activity activity) { 

       String[] checkList = newString[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,                 Manifest.permission.READ_EXTERNAL_STORAGE}; 

        List<String> needRequestList = checkPermission(activity, checkList);

        if(needRequestList.isEmpty()) { 

             Toast.makeText(MainActivity. this, "无需申请权限", Toast.LENGTH_SHORT).show; 

        } else{

              requestPermission(activity, needRequestList.toArray( newString[needRequestList.size])); 

        }

 }

申请权限后,提示用户作出选择:

访问文件

权限申请成功后,即可对自带外部存储之共享存储空间和其它目录进行访问。

分别以共享存储空间和其它目录为例,阐述访问方式:

访问共享存储空间

共享存储空间分为两类文件:媒体文件和文档/其它文件。

访问媒体文件

目的是拿到媒体文件的路径,有两种方式获取路径:

1、直接构造路径

以图片为例,假设图片存储在/sdcard/Pictures/目录下。

private void  testShareMedia( ) { 

 // 获取目录:/storage/emulated/0/

 File rootFile = Environment.getExternalStorageDirectory;

 String imagePath = rootFile.getAbsolutePath + File.separator + Environment.DIRECTORY_PICTURES + File.separator + "myPic.png"; 

 Bitmap bitmap = BitmapFactory.decodeFile(imagePath);

}

如上,myPic.png的路径:/storage/emulated/0/Pictures/myPic.png,拿到路径后就可以解析并获取Bitmap。

2、通过MediaStore获取路径

沿用上篇的demo:

private void getImagePath( Context context) { 

       ContentResolver contentResolver = context.getContentResolver;

       Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null); 

       while(cursor.moveToNext) { 

                String imagePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));

                Bitmap bitmap = BitmapFactory.decodeFile(imagePath);

                break; 

       }

}

同样的,也是拿到图片路径后获取Bitmap。

还有一种不直接通过路径访问的方法:

3、通过MediaStore获取Uri

private void getImagePath( Context context) {

     ContentResolver contentResolver = context.getContentResolver;

     Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null); 

     while(cursor.moveToNext) { 

         //获取唯一的id

          longid = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));

         //通过id构造Uri

          Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);

          openUri(uri);

           break; 

       }

}

与直接拿到路径不同的是,此处拿到的是Uri。图片的信息封装在Uri里,通过Uri构造出InputStream,再进行图片解码拿到Bitmap

访问文档和其它文件

1、直接构造路径

与媒体文件一样,可以直接构造路径访问。

2、通过SAF访问

Storage Access Framework 简称SAF:存储访问框架。相当于系统内置了文件选择器,通过它可以拿到想要访问的文件信息。

同样的以获取图片为例:

private void startSAF{ 

       Intent intent = newIntent(Intent.ACTION_OPEN_DOCUMENT); 

       intent.addCategory(Intent.CATEGORY_OPENABLE);

       //选择图片intent.setType( "image/jpeg"); 

       startActivityForResult(intent, 100); 

}

@Override

protected void  onActivityResult( intrequestCode, intresultCode, @Nullable Intent data) { 

        super.onActivityResult(requestCode, resultCode, data);

       if(requestCode == 100) {  

       //选中返回的图片封装在uri里

        Uri uri =  data.getData;   

       openUri(uri);

        }

}

private void openUri(Uri uri){

       try{ 

            //从uri构造输入流

           InputStream fis = getContentResolver.openInputStream(uri);

           Bitmap bitmap = BitmapFactory.decodeStream(fis);

        } catch(Exception e) {

        }

} 

可以看出,通过SAF并不能直接拿到图片的路径,图片的信息封装在Uri里,通过Uri构造出InputStream,再进行图片解码拿到Bitmap

访问其它目录

有两种方式:

1、直接构造路径

在/sdcard/目录下直接创建目录:

private void testPublicFile( ) {

     File rootFile = Environment.getExternalStorageDirectory();

     String imagePath = rootFile.getAbsolutePath() + File.separator + "myDir"; 

     File myDir = new File(imagePath);

     if(!myDir.exists()) {

          myDir.mkdir();

      }

}

可以看出,/sdcard/myDir/目录创建成功。

二、通过SAF访问

与共享存储空间SAF访问方式一致。

Android 10.0 之前访问方式总结

由上面分析的共享存储空间/其它目录访问方式可知,访问目录/文件可通过如下两个方法:

1、通过路径访问。路径可以直接构造也可以通过MediaStore获取。

2、通过Uri访问。Uri可以通过MediaStore或者SAF获取。

Android 6.0 以下访问共享存储空间/其它目录步骤:

1、AndroidManifest.xml里声明存储权限

2、通过路径或者Uri访问文件

Android 6.0(含)~Android 10.0(不含)访问共享存储空间/其它目录步骤:

1、AndroidManifest.xml里声明存储权限

2、动态申请存储权限

3、通过路径或者Uri访问文件

3、Android 10.0 访问方式变更

为什么要变更

你可能已经发现了上面访问方式的弊端,比如我们能够直接在/sdcard/目录下创建目录/文件。事实上,很多App就是这么干的,看图说话:

可以看出/sdcard/目录下,如淘宝、qq、qq浏览器、微博、支付宝等都自己建了目录。

这么看来,导致目录结构很乱,而且App卸载后,对应的目录并没有删除,于是就是遗留了很多"垃圾"文件,久而久之不处理,用户的存储空间越来越小。

总结弊端如下:

1、在设置里"Clear storage"或者"Clear cache"并不能删除该目录下的文件

2、卸载App也不能删除该目录下的文件

3、App可以随意修改其它目录下的文件,如修改别的App创建的文件等,不安全

你也许会问,为什么要在/sdcard/目录下新建自己的目录呢?

大体有以下两个原因:

1、此处新建的目录不会被设置里的App存储用量统计,让用户"看起来"自己的App占用的存储空间很小

2、方便操作文件

如何变更

面对众多App不讲"码德"随意新建目录/文件的现象,Google在Android 10.0上重拳出击了。

引入Scoped Storage

翻译成中文有好几个版本:作用域存储、分区存储、沙盒存储。

具体中文翻译不重要,下面以分区存储指代。

分区存储原理:

1、App访问自身内部存储空间、访问外部存储空间-App私有目录不需要任何权限(这个与Android 10.0之前一致)

2、外部存储空间-共享存储空间、外部存储空间-其它目录 App无法通过路径直接访问,不能新建、删除、修改目录/文件等

3、外部存储空间-共享存储空间、外部存储空间-其它目录 需要通过Uri访问

分区存储的变更在于第二点、第三点。

为什么Uri能够访问

先来看为什么通过路径无法直接访问。

我们知道访问文件最终是通过构造InputStream/OutputStream来实现的,以InputStream为例,看看其构造方法:

#FileInputStream.java

//文件描述符 

private final FileDeor fd; 

public FileInputStream( File file)  throws  FileNotFoundException { 

      String  name  =  file != null ? file.getPath() : null;

        ...

      //传入name,构造FileDeor

      //没有权限访问,则此处抛出异常

       fd = IoBridge.open(name, O_RDONLY);

       ...

}

可以看出,要想FileInputStream 能读入文件,核心是需要构造FileDeor,而对于Android 10.0,直接通过路径构造FileDeor 会抛出异常。

那么我们自然会想到,有没有通过构造好的FileDeor 来生成FileInputStream对象,进而使用read(xx)方法读取数据。

还真有,请看:通过Uri构造InputStream。

InputStream  fis = getContentResolver.openInputStream(uri);

进入看其源码:

#ContentResolver.java

public final @ Nullable  InputStream openInputStream( @NonNull Uri uri)  throws FileNotFoundException{ 

       ...

       if(SCHEME_ANDROID_RESOURCE. equals(scheme)) {

        ...

        }else if(SCHEME_FILE. equals(scheme)) {

         ...

        } else{

             //通过Uri构造fd是被允许的

             AssetFileDeor fd = openAssetFileDeor(uri, "r", null);

             try{

             //反过来创建 InputStream 

             return fd != null? fd.createInputStream() : null; 

            } catch(IOException e) { 

            throw  newFileNotFoundException( "Unable to create stream"); 

           }

      }

}

AssetFileDeor 持有ParcelFileDeor 引用,而ParcelFileDeor 持有FileDeor 引用。

同理也适用于FileOutputStream。因此,通过Uri能够访问文件。

四、如何适配Android 10.0

从以上分析可知,适配Android 10.0 有点麻烦,问题来了有没有简单的方法绕过检测。

第一种方法

1、Android 10.0 及其以后才会有分区存储功能,只要Android 设备不升级系统到Android 10.0以后,就不会有问题。

2、可能觉得这是句废话,其实不然,有些定制的设备系统一般都不会升级的。

如果不能使用第一种方法,还可以采用第二种方法。

第二种方法

1、Android 一般升级功能的时候都会配合targetSdkVersion使用。只要targetSdkVersion<=28,分区存储功能就不会开启。

有关targetSdkVersion 作用请移步:targetSdkVersion、compileSdkVersion、minSdkVersion作用与区别

如果第二种方法也不能使用,则还有第三种方法。

第三种方法

在AndroidManifest.xml 里application标签下添加:

android:requestLegacyExternalStorage="true" 可禁用分区存储

从长远的角度看,以上三个方法都不是一劳永逸的方法,其中第二种、第三种方法是Google 留给App开发者适配的缓冲时间。

对于第二种方法:

Google 在App上架App Store 时候可能会强制要求升级targetSdkVersion,因此该方法不保险。

对于第三种方法:

在Android 11会忽略该字段,强制开启分区存储,该字段也不怎么靠谱。

因此,最终还是需要老老实实按照Google 的要求适配Android 10.0,下篇将重点分析Android 10.0/11 该如何来适配。

本文基于Android 10.0。

五、MediaStore 基本知识

上篇已经分析得出结论,Android 10.0 存储访问方式变更地方在于:

自带外部存储-共享存储空间和自带外部存储-其它目录

以上两个地方不能通过路径直接访问文件,而是需要通过Uri访问。

共享存储空间

共享存储空间存放的是图片、视频、音频等文件,这些资源是公用的,所有App都能够访问它们。

系统里有external.db数据库,该数据库里有files表,该表里存放着共享文件的诸多信息,如图片有宽高,经纬度、存放路径等,视频宽高、时长、存放路径等。而文件真正存放的地方在于共享存储空间。

1、保存图片到相册

当App1保存图片到相册时,简单流程如下:

1、将路径信息写入数据库里,并获取Uri

2、通过Uri构造输出流

3、将该图片保存在/sdcard/Pictures/目录下

2、从相册获取图片

当App2从相册获取图片时,简单流程如下:

1、先查询数据库,找到对应的图片Cursor

2、从Cursor里构造Uri

3、从Uri构造输入流读取图片

以上以图片为例简单分析了共享存储空间文件的写入与读取,实际上对于视频、音频步骤亦是如此。

MediaStore作用

共享存储空间里存放着图片、视频、音频、下载的文件,App获取或者插入文件的时候怎么区分这些类型呢?

这个时候就需要MediaStore,来看看MediaStore.java

可以看出其内部有Audio、Images等内部类,这些内部类里记录着files表的各个字段名,通过构造这些参数就可以插入相应的字段值以及获取对应的字段值。

MediaStore 实际上就是相当于给各个字段起了别名,我们编码的时候更容易记住与使用:

//列举一些字段:

//图片类型  MediaStore.Images.Media.MIME_TYPE

//音频时长  MediaStore.Audio.Media.DURATION

//视频时长  MediaStore.Video.Media.DURATION

//等等,还有很多

MediaStore和Uri联系

比如想要查询共享存储空间里的图片文件:

Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);

MediaStore.Images.Media.EXTERNAL_CONTENT_URI 意思是指定查询文件的类型是图片,并构造成Uri对象,Uri实现了Parcelable,能够在进程间传递。

接收方(另一个进程收到后),匹配Uri,解析出对应的字段,进行具体的操作。

当然,MediaStore是系统提供的方便操作共享存储空间的类,若是自己写ContentProvider,则也可以自定义类似MediaStore的类用来标记自己的数据库表的字段。

六、通过Uri读取和写入文件

既然不能通过路径直接访问文件,那么来看看如何通过Uri访问文件。在上篇文章里提到过: Uri可以通过MediaStore或者SAF获取。(此处需要注意的是:虽然也可以通过文件路径直接构造Uri,但是此种方式构造的Uri是没有权限访问文件的)

先来看看通过SAF获取Uri。

从Uri读取文件

现在/sdcard/目录下存在一个文件名为:mytest.txt。

该文件内容是:

传统的直接读取mytest.txt方法:

//从文件读取

private  void  readFile( String filePath) { 

     if( TextUtils.isEmpty( filePath) ) return;

    try{ 

           File file = newFile(filePath); 

           FileInputStream fileInputStream = newFileInputStream(file); 

           BufferedInputStream  bis = new  BufferedInputStream(fileInputStream); 

           byte[]  readContent = new byte[ 1024]; 

           int  readLen = 0; 

           while(readLen != -1) {

                readLen = bis.read( readContent, 0, readContent.length);

               if ( readLen > 0) { 

                   String content = newString(readContent); 

                   Log.d( "test", "read content:"+ content.substring( 0, readLen)); 

              }

          }

           fileInputStream.close;

      }  catch(Exception e) {

      }

}

开启分区存储功能后,这种方法是不可取的,会报权限错误。

而mytest.txt不属于共享存储空间的文件,是属于其它目录的,因此不能通过MediaStore获取,只能通过SAF获取,如下:

private void  startSAF{

     Intent intent = new Intent( Intent.ACTION_OPEN_DOCUMENT); 

      intent.addCategory(Intent.CATEGORY_OPENABLE);

      //指定选择文本类型的文件        

      intent.setType( "text/plain");

      startActivityForResult(intent, 100); 

}

@Override

protected void  onActivityResult( intrequestCode, intresultCode, @Nullable Intent data) { 

           super.onActivityResult(requestCode, resultCode, data);

           if(requestCode == 100) { 

            // 选中返回的文件信息封装在Uri 里

            Uri uri = data.getData;

           openUriForRead(uri);

          }

}

拿到Uri后,用来构造输入流读取文件。

private void openUriForRead( Uri uri) { 

     if(uri == null) return;

     try{ 

     //获取输入流

     InputStream inputStream = getContentResolver.openInputStream(uri);

     byte[] readContent = newbyte[ 1024]; 

     int len = 0; 

          do{ 

           //读文件

           len = inputStream.read(readContent);

            if(len != -1) {

            Log.d( "test", "read content:"+ newString(readContent).substring( 0, len)); 

              }

             } while(len != -1);

                inputStream.close;

           } catch(Exception e) {

         Log.d( "test", e.getLocalizedMessage); 

      }

}

最终输出:

由此可以看出,mytest.txt属于"其它目录"下的文件,因此需要通过SAF访问,SAF返回Uri,通过Uri构造InputStream即可读取文件。

从Uri写入文件

继续来看看写的过程,现在需要往mytest.txt写入内容。

同样的,还是需要通过SAF拿到Uri,拿到Uri后构造输出流:

private void  openUriForWrite( Uri uri) { 

         if(uri == null) { return; }

         try{ 

               //从uri构造输出流

               OutputStream outputStream = getContentResolver.openOutputStream(uri);

                //待写入的内容

                String content = "hello world I'm from SAF\n";

                //写入文件

               outputStream.write(content.getBytes);

                outputStream.flush;

                outputStream.close;

             } catch(Exception e) {

              Log.d( "test", e.getLocalizedMessage);

      }

}

最后来看看文件是否写入成功,通过SAF再次读取mytest.txt,发现正好是之前写入的内容,说明写入成功。

七、通过Uri 获取图片和插入相册

上面列举出了其它目录下文件的读写,方法是通过SAF拿到Uri。

SAF好处是:

系统提供了文件选择器,调用者只需要指定想要读写的文件类型,比如文本类型、图片类型、视频类型等,选择器就会过滤出相应文件以供选择。接入方便,选择简单。

想想另一种场景:

想要自己实现相册选择器,那么就需要获得共享存储空间下的文件信息。此种场景下使用SAF是无法做到的。

因此问题的关键是: 如何批量获得共享存储空间下图片/视频的信息?

答案是:ContentResolver+ContentProvider+MediaStore(ContentProvider对于调用者是透明的)。

以图片为例,分析插入与查询方式。

插入相册

来看看图片的插入过程:

//fileName为需要保存到相册的图片名

private void  insert2Album(InputStream inputStream, StringfileName) {

      if(inputStream == null) return;

     ContentValues contentValues = new ContentValues; 

      contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, fileName);

      if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 

              //  RELATIVE_PATH 字段表示相对路径-------->(1)

              contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);

       } else{ 

              Stringd  stPath = Environment.getExternalStorageDirectory + File.separator + Environment.DIRECTORY_PICTURES + File.separator + fileName;

                //DATA字段在Android 10.0 之后已经废弃

              contentValues.put(MediaStore.Images.ImageColumns.DATA, dstPath);

       }

    //插入相册------->(2)

              Uri uri = getContentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);

    //写入文件------->(3)

           write2File(uri, inputStream);

}

重点说明三个点:

(1)

Android 10.0之前,MediaStore.Images.ImageColumns.DATA 字段记录的是图片的绝对路径,而Android 10.0(含)之后,DATA 被废弃,取而代之的是使用MediaStore.Images.ImageColumns.RELATIVE_PATH,表示相对路径。比如指定RELATIVE_PATH为Environment.DIRECTORY_PICTURES,表示之后的图片将会放到Environment.DIRECTORY_PICTURES目录下。

(2)

调用ContentResolver里的方法插入相册。

MediaStore.Images.Media.EXTERNAL_CONTENT_URI 指的是插入图片表。

ContentValues 以Map的形式记录了待写入的字段值。

插入后返回Uri。

(3)

以上两步仅仅只是往数据库里增加一条记录,该记录指向的新文件是空的,需要将图片写入到新文件。

而新文件位于/sdcard/Pictures/目录下,该目录是不能直接通过路径访问的,因此需要通过第二步返回的Uri进行访问。

//  uri 关联着待写入的文件

//inputStream 表示原始的文件流

private void  write2File( Uri uri, InputStream inputStream) { 

      if(uri == null  || inputStream == null) return;

      try{ 

     //从Uri构造输出流

        OutputStream outputStream = getContentResolver.openOutputStream(uri);

        byte[] in= new byte[1024]; 

        int len = 0;

       do{ 

           //从输入流里读取数据

           len = inputStream.read( in); 

            if(len != -1) { 

              outputStream.write( in, 0, len); 

              outputStream.flush;

            }

         } while(len != -1);

         inputStream.close;

         outputStream.close;

    } catch(Exception e) {

        Log.d( "test", e.getLocalizedMessage); 

     }

}

可以看出,目标文件关联的Uri有了,还需要原始的输入文件。

测试上述的插入方法:

private  void  testInsert( ) {

      String picName = "mypic.jpg"; 

      try{

           File  externalFilesDir = getExternalFilesDir( null); 

           File  file = newFile(externalFilesDir, picName); 

           FileInputStream fis = newFileInputStream(file); 

           insert2Album(fis, picName);

        } catch( Exception e) { 

      Log.d( "test", e.getLocalizedMessage); 

       }

}

其中,原始文件(图片)存放于自带外部存储-App私有目录,如下:

需要注意的是:

1、读取原始文件需要权限,上述例子里的原始文件存放在自带外部存储-App私有目录,因此本App可以使用路径直接读取

2、对于其他目录则依然需要构造Uri读取,如通过SAF获取Uri

获取图片

同样的,想要从系统相册中获取图片,也需要通过Uri访问。

private  void  queryImageFromAlbum( ) { 

         Cursor cursor = getContentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);

         if( cursor != null ) { 

              while( cursor.moveToNext() ) {

                     //获取唯一的id  

                      long  id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)); 

                     //通过id构造Uri  

                       Uri uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);

                       //解析uri

                     decodeUriForBitmap(uri);

                 }

           }

 }

private  void   decodeUriForBitmap( Uri uri) { 

         if(uri == null) return;

         try{ 

              //构造输入流

              InputStream  inputStream = getContentResolver.openInputStream(uri);

             //解析Bitmap

              Bitmap bitmap = BitmapFactory.decodeStream(inputStream);

              if( bitmap != null ) Log.d( "test", "bitmap width-width:"+ bitmap.getWidth + "-"+ bitmap.getHeight); 

          } catch(Exception e) { 

               Log.d( "test", e.getLocalizedMessage); 

          }

}

与插入相册过程类似,同样需要拿到Uri,再构造输入流,从输入流读取文件(图片内容)。

以上,通过Uri 获取图片和插入相册分析完毕,共享存储空间的其他文件类型如视频、音频、下载文件也是同样的流程。

需要说明的是上述的ContentResolver .insert(xx)或ContentResolver.query(xx) 的参数取值还可以更丰富,但不是本篇重点,因此忽略了,实际使用过程中具体情况具体分析。

八、Android 11.0 权限申请

通过Uri访问文件似乎已经满足了Android 10.0适配要求,但是仔细想想还是有不足之处:

1、共享存储空间只能通过MediaStore访问,以前流行的访问方式是直接通过路径访问。比如自己做的相册管理器,先遍历相册拿到图片/视频的路径,然后再解析成Bitmap展示,现在需要先拿到Uri,再解析成Bitmap,多少有些不方便。此外,也许你依赖的第三方库是直接通过路径访问文件的,而三方库又没有及时更新适配分区存储,可能就会导致用不了相应的功能。

2、SAF虽然能够访问其它目录的文件,但是每次都需要跳转到新的页面去选择,当想要批量展示文件的时候,比如自己做的文件管理器,就需要列出当前目录下有哪些目录/文件,这个时候需要有权限遍历/sdcard/目录。显然,SAF并不能胜任此工作。

Android 11.0考虑到上面的问题,因此做了新的优化。

共享存储空间-媒体文件访问变更

媒体文件可以通过路径直接访问:

private void  getImagePath( Context context) { 

         ContentResolver contentResolver = context.getContentResolver();

          Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null); 

          while(cursor.moveToNext) {

                 try{

                     //取出路径

                    String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));

                     Bitmap bitmap = BitmapFactory.decodeFile(path);

                   } catch(Exception e) { 

                        Log.d( "test", e.getLocalizedMessage); 

                   }

                  break; 

           }

}

可以看出,之前在Android 10.0上被禁用的访问方式,在Android 11.0上又被允许了,这就解决了上面的第一个问题。

需要注意的是:此种方式只允许读文件,写文件依然不行

Google 官方指导意见是:

虽然可以通过路径直接访问媒体文件,但是这些操作最终是被重定向到MediaStore API的,重定向过程可能会损耗一些性能,并且直接通过路径访问不一定比MediaStore API 访问快。

总之建议非必要的话不要直接使用路径访问。

访问所有文件

假若App开启了分区存储功能,当App运行在Android 10.0的设备上时,是没法遍历/sdcard/目录的。而在Android 11.0上运行时是可以遍历的,需要进行如下几个步骤。

1、声明管理权限

在AndroidManifest.xml添加权限声明

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

2、动态申请所有文件访问权限

private  void  testAllFiles{ 

           //运行设备>=Android 11.0

          if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 

              //检查是否已经有权限

                     if(!Environment.isExternalStorageManager) { 

                     //跳转新页面申请权限             

                     startActivityForResult( new  Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION), 101); 

                    }

           }

}

@Override

protected void  onActivityResult( intrequestCode, intresultCode, @Nullable Intent data) { 

      super.onActivityResult(requestCode, resultCode, data); 

      //申请权限结果

     if(requestCode == 101) { 

            if(Environment.isExternalStorageManager()) { 

                  Toast.makeText(MainActivity. this, "访问所有文件权限申请成功", Toast.LENGTH_SHORT).show();

                 //遍历目录

                 showAllFiles();

             }

       }

}

此处申请权限不是以对话框的形式提示用户,而是跳转到新的页面,说明该权限的管理更严格。

3、遍历目录、读写文件

拥有权限后,就可以进行相应的操作了。

private void showAllFiles(){ 

          File file = Environment.getExternalStorageDirectory();

          File[] list= file.listFiles(); 

          for( inti = 0; i < list.length; i++) { 

              String name = list[i].getName;

              Log.d( "test", "fileName:"+ name); 

         }

 }

文件管理器效果图类似如下:

当然读写文件也不在话下了,比如往/sdcard/目录下写入文件:

private  void  testPublicFile( ) { 

         File rootFile = Environment.getExternalStorageDirectory();

         try{ 

                File file = newFile(rootFile, "mytest.txt"); 

                FileOutputStream fos = newFileOutputStream(file); 

                String content = "hello world\n"; 

                fos.write(content.getBytes);

                fos.flush;

                fos.close;

           } catch(Exception e) {

                  Log.d( "test", e.getLocalizedMessage); 

           }

}

ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 这个权限的名字看起来很唬人,感觉就像是能够操作所有文件的样子,这不就是打破了分区存储的规则了吗?其实不然:

即使拥有了该权限,依然不能访问内部存储和外部存储-App私有目录

需要说明的是:

1、Environment.isExternalStorageManager()、Build.VERSION_CODES.R 等需要编译版本>=30才能编译通过。

2、Google 提示当使用MANAGE_EXTERNAL_STORAGE 申请权限时,并且targetSdkVersion>=30,此种情况下App被禁止上架Google Play的,限制时间最早到2021年。因此,在此时间之前若是申请了MANAGE_EXTERNAL_STORAGE权限,最好不要升级targetSdkVersion到30以上。

九、Android 10/11 存储适配建议

好了,通过分析Android 10/11存储适配方式,了解到了不同的系统需要如何进行适配,此时就需要一个统一的适配方案了。

适配核心

分区存储是核心,App自身产生的文件应该存放在自己的目录下:

/sdcard/Android/data/packagename/ 和/data/data/packagename/

这两个目录本App无需申请访问权限即可申请,其它App无法访问本App的目录。

适配共享存储

共享存储空间里的文件需要通过Uri构造输入输出流访问,Uri获取方式有两种:MediaStore和SAF。

适配其它目录

在Android 11上需要申请访问所有文件的权限。

具体做法

第一步

在AndroidManifest.xml里添加如下字段:

权限声明:

 < uses-permissionandroid:name= "android.permission.WRITE_EXTERNAL_STORAGE"/> 

 < uses-permissionandroid:name= "android.permission.READ_EXTERNAL_STORAGE"/> 

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

在<application/>标签下添加如下字段:

android:requestLegacyExternalStorage= "true"

第二步

如果需要访问共享存储空间,则判断运行设备版本是否大于等于Android6.0,若是则需要申请WRITE_EXTERNAL_STORAGE 权限。拿到权限后,通过Uri访问共享存储空间里的文件。

如果需要访问其它目录,则通过SAF访问

第三步

如果想要做文件管理器、病毒扫描管理器等功能。则判断运行设备版本是否大于等于Android 6.0,若是先需要申请普通的存储权。若运行设备版本为Android 10.0,则可以直接通过路径访问/sdcard/目录下文件(因为禁用了分区存储);若运行设备版本为Android 11.0,则需要申请MANAGE_EXTERNAL_STORAGE 权限。

以上是Android 存储权限适配的全部内容。



作者:王朋6
链接:https://www.jianshu.com/p/b727764d2d70
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Android 10-11适配外部存储方案 Android Api 29 对文件和文件夹进行了重大更改。不允许使用外部存储,如下方法:只能使用内部存储但谷歌官方给了一个后门,在AndroidManifest.xml文件中application节点中加上android:requestLegacyExternalStorage="true"属性才可以访问沙盒路径下的数据原来的项目就要重新适配。 阅读详情

相关推荐

Android系统 应用存储路径与权限

本文学习Android存储路径和权限,分为系统预装应用、特权系统应用、用户安装应用和平台签名应用。根据存储位置和权限级别各不相同。

我其实什么都不会 5811

Android 文件存储系统

作者:自动化BUG制造器 存储的可选项 Android 的文件系统类似于其他平台的基于磁盘的文件系统。包括以下几种存储类别: 1.App 私有的存储空间 2.共享的存储空间 3.Preferences,以 key-value 的形式存储一些原始的内容,如 SharedPreferences。 4.数据库 不同的存储类别扮演了不同的角色,比如 App 的私有存储空间是无法被外部访问的,可以用来存储一些 App 私有的敏感信息;共享的存储空间可以存储一些所有应用都可以访问的文件,例如媒体、文档等可公开的文件.

1 830

从Demo到产品:基于ADAU1452的ANC方案,硬件选型、性能优化与避坑指南

本文详细解析了基于ADAU1452的主动降噪(ANC)方案从Demo到产品的实现路径,涵盖硬件选型、性能优化与实战避坑指南。重点探讨了ADAU1452与ADAU1466的性能差异、编解码器选型策略,以及MFXLMS算法优化技巧,为工程师提供从理论验证到量产落地的系统级解决方案。

weixin_30542079的博客 451

Android13关于获取外部存储文件的相关问题及解决方案记录

但是我们知道,从Android13 (API33) 开始,外部存储权限发生了变化,要想读取外部存储文件,使用原来的权限注册方式已经失效了。左图是系统自带的文件选择器,右图是我开发的文件选择器(我是以系统的选择器作为参照的,因此样子长得很像😄)。也就是说,如果开启了这个权限,就可以获取所有的文件了(包括媒体文件和非媒体文件),那么媒体文件的权限就算不申请也可以获得权限。

qq_45204138的博客 1万+

实战解析:如何通过LTspice仿真优化MOS管缓启动电路,解决上电冲击电流问题

本文详细解析如何通过LTspice仿真优化MOS管缓启动电路,有效解决上电冲击电流问题。文章从问题根源分析入手,介绍LTspice环境搭建、缓启动电路设计原理、参数优化技巧及实际工程问题排查,帮助工程师掌握MOS管电源开关电路的关键设计方法,提升系统可靠性和性能。

weixin_28683689的博客 515

Ghidra逆向工具入门:从安装到实战分析baby.exe(附详细配置截图)

本文详细介绍了Ghidra逆向工具的安装配置与实战应用,通过破解CTF题目baby.exe的全过程展示其强大功能。从Java环境搭建到插件配置,再到反编译技巧与动态调试方法,帮助读者快速掌握这款免费开源的反编译神器,有效替代收费的IDA Pro工具。

weixin_29252171的博客 352

Android-10、11-存储完全适配(上)

前言 上篇文章分析了Android 存储相关的基础知识,说到了各个目录下文件的访问方式。本篇将着重分析Android 系统版本变更对存储访问权限的影响及其适配方法。 通过本篇文章,你将了解到: 1、存储基本知识 2、Android 10.0 之前访问方式 3、Android 10.0 访问方式变更 4、如何不适配Android 10.0 1、存储基本知识 先来看看存储区域划分: 其中,以下目录无需存储权限即可访问: 1、App自身的内部存储 2、App自身的自带外部存储-私有目录 剩下的都需要申请

1096

使用再生龙还原镜像文件到硬盘

1. 把制作好的再生龙光盘插入需要的电脑,开机按F4使用再生龙启动2. 选择默认的:Clonezailla live(Default settings,VGA 800*600),然后等待几分钟 3. 选择语言:简体中文 4. 选择不修改键盘映射 5. 选择使用再生龙          6. 选择硬盘/分区(存到/来自)镜像文件,此选项既可以制作镜像,也可以恢复镜像。 7. 使用本机的分区     ...

weixin_42168893的博客 1万+

Android 10、11 存储完全适配(下)

Android 10、11 存储完全适配(下) 前言 上篇文章分析了Android 10.0版本前后存储访问方式的变更,本篇将着重分析如何来具体适配Android 10.0、11.0。 通过本篇文章,你将了解到: 1、MediaStore 基本知识 2、通过Uri读取和写入文件 3、通过Uri 获取图片和插入相册 4、Android 11.0 权限申请 5、Android 10/11 存储适配建议 1、MediaStore 基本知识 再次回顾存储区域划分: 上篇已经分析得出结论,Android 10.

silencefly的博客 1384

BlockUI专栏目录

BlockUI是一个设计NX对话框的工具,是官方推荐使用的对话框制作方法,能够与NX自身风格相统一,并且在实际开发中经常被使用。BlockUI可以帮助用户快速创建NX对话框,支持多种控件类型,如按钮、文本框、标签等。通过使用BlockUI,用户可以轻松地自定义对话框的布局、样式和交互行为。此外,BlockUI还提供了一些高级功能,如动态更新、条件显示和事件处理等,以帮助用户创建更加复杂和灵活的对话框。本专栏讲述了使用BlockUI制作NX对话框的方法,介绍了每个控件的功能,并且分享一些常用操作的源码。

王牌飞行员_里海的博客 1632

软件设计师考试下午题代码部分详解:23种设计模式全面解析

软件设计师考试的下午题通常会考察设计模式的应用,这部分内容既是重点也是难点。本文将系统地介绍考试中常见的23种设计模式,通过详细代码示例和通俗易懂的讲解,帮助考生掌握这些核心知识点,提高应试能力。概念:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。适用场景:系统要独立于产品的创建、组合和表示时;系统要由多个产品系列中的一个来配置时。@Override@Override@Override@Override// 产品族A@Override。

4万+

Android 10、11 存储完全适配(上)

原文地址 Android 存储基础 Android 10、11 存储完全适配(上) Android 10、11 存储完全适配(下) Android 10、11 存储完全适配(上) 前言 上篇文章分析了Android 存储相关的基础知识,说到了各个目录下文件的访问方式。本篇将着重分析Android 系统版本变更对存储访问权限的影响及其适配方法。 通过本篇文章,你将了解到: 1、存储基本知识 2、Android 10.0 之前访问方式 3、Android 10.0 访问方式变更 4、如何不适配Android

silencefly的博客 1078

uniapp:微信支付报错requestPayment:fail errors,uniAPP调用uni.requestPayment微信支付提示错误(安卓app、ios苹果app、微信小程序支付报错)

uniapp微信支付报错requestPayment:fail errors,uniApp APP端调起微信支付失败报错requestPayment:fail errors,uniapp微信小程序支付的坑 “errMsg“: “requestPayment:fail errors“,uni.requestPayment微信支付,UNI-APP支付报错“errMsg“: “requestPayment:fail errors“ app微信支付,uniapp 安卓app微信支付报错,uni.requestPay

✅ 精选推荐文章 3916

好好管理你应用的文件夹,别再乱用了

为什么要管理文件? Android手机一直以来被人诟病越用越卡,越用存储空间越少,经常有要靠各种清理app清理垃圾,到最后不得对手机进行双清,原因除了硬件老化和Android的底层实现问题之外,开发者对文件管理的忽视制造出大量无法清理的“垃圾”也是造成手机卡慢的原因之一。 Android的开放性给了开发者巨大的自由度,但自由不是让我们滥用权限和随意开发的借口,每一个开发者都应该注重细节,连...

weixin_45365889的博客 920

GEE:基于MODIS数据计算NDVI时间序列,并使用Savatsky-Golay(SG)滤波器对其进行平滑处理

优快云@_养乐多_本文将介绍如何在 Google Earth Engine (GEE) 平台中,使用 MODIS 数据计算 NDVI 时间序列,并对该数据集应用 Savitzky-Golay 滤波器,得到了一个新的平滑后的数据集。Savitzky-Golay 滤波器可以有效地去除信号中的噪声,并平滑信号的变化。该方法可以扩展到基于 MODIS 数据计算的任何指数或者波段。

养乐多的博客 1425

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

Android所有版本的存储权限适配

android.widget.Toast.makeText(this, "权限授予失败", android.widget.Toast.LENGTH_LONG).show();android.widget.Toast.makeText(this, "权限授予失败", android.widget.Toast.LENGTH_LONG).show();android.util.Log.d("debug", "来自安卓6.0到安卓9.9之间的请求权限,打印授予的权限->" + permissions[0]);

HardProgrammer的博客 2475

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Android开发之API29以上Environment.getExternalStoragePublicDirectory 无法访问解决

【代码】Android开发之API29以上Environment.getExternalStoragePublicDirectory 无法访问解决。

u013519290的博客 4370

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

Android系统不同版本存储权限

Android系统不同版本存储权限变更行为

ChinaDragon10的博客 7327

不同版本Android文件权限申请方式

现在的安卓系统基本都是10+的了,之前写的一个app发现找不到文件,才发现10+之后的和以前的不一样,参考网上他人的文章,把文件权限申请的整理了下。

晨石的博客 4304

Android 11 访问外部存储

Android 11 访问外部存储

u010334503的博客 1542

Android 存储权限适配指南

Android权限大致可分为三类: 普通权限:只需要在清单文件中注册即可 危险权限:需要在代码中动态申请,以弹系统 Dialog 的形式进行请求 特殊权限:需要在代码中动态申请,以跳系统 Activity 的形式进行请求 而我们今天要讲的主题,是关于存储权限,在 Android 6.0 之后就变成了危险权限,而到了 Android 11 上面变成了特殊权限,而最明显的区别是一个是通过 Dialog 展示给用户看,另外一个是通过 Activity 展现给用户看。 6.0之后的危险权限 Android

Flying snow 1万+

android11以上SD卡存储权限适配-申请允许管理所有文件权限

存储权限申请

zhayunbiao的博客 4355

安卓11及以上版本读取本地文件的方法

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录前言一、pandas是什么?二、使用步骤1.引入库2.读入数据总结 前言 提示:这里可以添加本文要记录的大概内容: 在安卓10.0及以上,系统新增了沙盒模式,进一步增强了用户的隐私性以及开发的规范性。但是在10.0时,我们可以**在AndroidManifest.xml中添加 android:requestLegacyExternalStorage=“true”**的方式,把沙盒模式关闭继续使用9.0及以下的方式去读取本地文件。.

XL1583135614的博客 1万+

android 11 文件权限 EXTERNAL STORAGE

android AndroidManifest

hoffmannlin的博客 680

Android 11 来袭,一起来看看怎么适配

Android 11为目标平台的应用,从后台发送自定义view的Toast消息系统会进行屏蔽。前台使用不受影响。Toast相应的setView和getView也已经废弃不建议使用。如果要在后台使用,推荐使用默认的toast或Snackbar替代。

程序员徐师兄的博客 3209
上一篇: Ubuntu下载ndk---android-ndk-r17c-linux-x86_64.zip
下一篇: kotlin has private access in
Lian_he
博客等级 码龄14年 14粉丝 2原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值