美文网首页
Android R 如何访问Android/data目录?

Android R 如何访问Android/data目录?

作者: GrayMonkey | 来源:发表于2021-02-27 23:34 被阅读0次

    前言

    Android R上分区存储的限制得到进一步加强,无论APP的targetsdkversion是多少,都将无法访问Android/data和Android/obb这二个应用私有目录。这无疑对会部分APP的业务场景及用户体验造成冲击,典型的如下

    • 文件管理类软件:微信、QQ传输的文件无法展示给用户以便捷使用
    • 垃圾清理类软件:清理缓存功能受阻

    “你有你的张良计,我有我的过墙梯”,现市面上文件管理类软件(如MT管理器)已解决上述系统限制,本文将浅析其实现方案,并主要分析以下2个问题:

    • SAF是通过何种方式访问文件系统的,MediaStore API ? File API ? Native Code ?
    • SAF为何能访问Android/data目录

    实现方案

    其实现方案很简单,就是通过Intent ACTION_OPEN_DOCUMENT_TREE,启动SAF让用户授权访问Android/data目录,属于官方公开的方法。
    前提是APP的targetsdkversion要小于30

    摘自官方文档 摘自官方文档

    文档链接:
    文档访问限制
    授予对目录内容的访问权限

    基本使用

    1. 通过Intent启动SAF授权界面,注意URI的百分号编解码(%3A和%2F),别随意替换,否则SAF无法导航到Android/data目录
         @TargetApi(26)
        private void requestAccessAndroidData(Activity activity){
            try {
                Uri uri = Uri.parse("content://com.android.externalstorage.documents/document/primary%3AAndroid%2Fdata");
                Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
                intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri);
                //flag看实际业务需要可再补充
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                                | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
                activity.startActivityForResult(intent, 6666);
            } catch (Exception e) {
                e.printStackTrace();
            }
        } 
    
       /**
         * 值必须为document uri 或者是带document id的document tree uri
         * eg.
         * document uri:
         * "content://com.android.externalstorage.documents/document/primary%3AAndroid%2Fdata"
         *
         * document tree uri with document id:
         * content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata%2Ffoo
         */
        public static final String EXTRA_INITIAL_URI = "android.provider.extra.INITIAL_URI";
    
    授权申请
    1. 在用户同意授权后,持久化uri权限(否则关机重启或授权界面finish后,APP就无权限访问了),并只能通过DocumentFile进行业务操作,File API操作是无效的,此授权只是授权uri操作,并未授权文件系统,后续章节有说明。
     implementation "androidx.documentfile:documentfile:1.0.1"
    
      @Override
        protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            switch (requestCode) {
                case 6666:
                    if (resultCode == Activity.RESULT_OK) {
                        //persist uri 
                        getContentResolver().takePersistableUriPermission(data.getData(),
                                Intent.FLAG_GRANT_READ_URI_PERMISSION
                                        | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    
                        //now use DocumentFile to do some file op
                        DocumentFile documentFile = DocumentFile
                                .fromTreeUri(this, data.getData());
                        DocumentFile[] files = documentFile.listFiles();
                       //补充说明下授权文件夹后,文件夹中的子文件的uri格式如下,可自行按格式拼接直接访问子文件:
                       //content://com.android.externalstorage.documents/tree/primary%3ATest%2Ftest/document/primary%3ATest%2Ftest%2F666.mp3
                        ......
                    }
                    break;
                default:
                    break;
            }
        }
    
    1. 注意这个授权用户是可以撤回的,通过点击应用信息界面的存储,就会看到撤回界面,所以业务需要去动态判断
     public boolean isGrantAndroidData(Context context) {
            for (UriPermission persistedUriPermission : context.getContentResolver().getPersistedUriPermissions()) {
                if (persistedUriPermission.getUri().toString().
                        equals("content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata")) {
                    return true;
                }
            }
            return false;
        }
    
    授权撤回

    拓展

    通过前面二个章节,已经介绍了实现方案的基本使用,下面就该分析本文的亮点内容了

    • SAF是通过何种方式访问文件系统的,MediaStore API ? File API ? Native Code ?
    • SAF为何能访问Android/data目录
    存储访问框架(SAF)简介

    为方便后续讲解,先简单回顾下SAF

    SAF架构

    APP:
    com.example.photos就是我们自己的APP

    System UI:
    com.google.android.documentsui,一般称作DoucmentUI,就是上文中启动的授权界面APP,它只是个UI壳子

    DocumentProvider:
    DocumentUI中数据的提供者,这个Provider可以有很多
    com.android.externalstorage,是本地文件系统的Provider

    关于SAF更详细介绍,请参考官方存储访问框架
    经过SAF的简单介绍,分析目标很明确,那就是com.android.externalstorage

    SAF是通过何种方式访问文件系统的

    先安利几个AOSP源码查看网址:
    官方的Android Code Search
    国内的AOSP XREF

    PS:后文源码链接都用的是XREF,方便国内查看

    从DocumentFile#listFile入手,经过源码跟踪会发现最终会调用 DocumentsProvider#queryChildDocuments方法

    public abstract class DocumentsProvider extends ContentProvider {
     .......
     @Override
        public final Cursor query(
                Uri uri, String[] projection, Bundle queryArgs, CancellationSignal cancellationSignal) {
           switch (mMatcher.match(uri)) {
                    ......
                    case MATCH_CHILDREN:
                    case MATCH_CHILDREN_TREE:
                            .......
                            return queryChildDocuments(getDocumentId(uri), projection, queryArgs);
                            ......
                    default:
                        throw new UnsupportedOperationException("Unsupported Uri " + uri);
                }
            } catch (FileNotFoundException e) {
                Log.w(TAG, "Failed during query", e);
                return null;
            }      
       }
     ......
    }
    

    接下来看看com.android.externalstorage中DocumentProvider的实现类
    ExternalStorageProvider
    frameworks/base/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java

    import com.android.internal.content.FileSystemProvider;
    public class ExternalStorageProvider extends FileSystemProvider 
    

    queryChildDocuments的实现位于其父类 FileSystemProvider

    public abstract class FileSystemProvider extends DocumentsProvider {
      ......
      private Cursor queryChildDocuments(
                String parentDocumentId, String[] projection, String sortOrder,
                @NonNull Predicate<File> filter) throws FileNotFoundException {
            final File parent = getFileForDocId(parentDocumentId);
            final MatrixCursor result = new DirectoryCursor(
                    resolveProjection(projection), parentDocumentId, parent);
            if (parent.isDirectory()) {
                //重点是这行
                for (File file : FileUtils.listFilesOrEmpty(parent)) {
                    if (filter.test(file)) {
                        includeFile(result, null, file);
                    }
                }
            } else {
                Log.w(TAG, "parentDocumentId '" + parentDocumentId + "' is not Directory");
            }
            return result;
        }
     ......
    }
    

    FileUtils#listFilesOrEmpty

        /** {@hide} */
        public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
            return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles())
                    : ArrayUtils.EMPTY_FILE;
        }
    

    至此,第一个问题,已经理清:
    SAF的ExternalStorageProvider最终也是通过File API来访问文件系统的

    那么第二个问题,就很自然的来了,都是File API操作,为何我们的APP就不能访问呢?

    SAF为何能访问Android/data目录

    既然,SAF和我们的APP都是File API操作,那我们就去看看com.android.externalstorage属于哪些用户组。
    adb shell 查查com.android.externalstorage进程的用户组

    #查进程ID
    generic_x86_arm:/ $ ps -A|grep com.android.external
    u0_a64        16233    296 1256792  85960 0                   0 S com.android.externalstorage
    #查进程所属的用户组
    generic_x86_arm:/ $ cat /proc/16233/status
    Name:   externalstorage
    Umask:  0077
    State:  S (sleeping)
    Tgid:   16233
    Ngid:   0
    Pid:    16233
    PPid:   296
    TracerPid:      0
    Uid:    10064   10064   10064   10064
    Gid:    10064   10064   10064   10064
    FDSize: 64
    #重点关注这行输出
    Groups: 1015 1077 1078 1079 9997 20064 50064
    

    拿着这些神秘的GID在前面介绍的网址中一搜,就会很容易的发现GID的定义类
    android_filesystem_config.h

    #define AID_SDCARD_RW 1015       /* external storage write access */
    #define AID_EXTERNAL_STORAGE 1077 /* Full external storage access including USB OTG volumes */
    #define AID_EXT_DATA_RW 1078      /* GID for app-private data directories on external storage */
    #define AID_EXT_OBB_RW 1079       /* GID for OBB directories on external storage */
    #define AID_EVERYBODY 9997        /* shared between all apps in the same profile */
    

    其中1078和1079分别对应Android/data和Android/obb的访问权限
    如果我们APP能通过某种方式获取到1078和1079的用户组权限,岂不妙哉?
    遗憾的是,对于三方APP这是不可能的,除非是手机厂商的预置的系统APP

    总结

    • Android R上可通过SAF获得访问Android/data和Android/obb目录的权限,前提是APP targetsdkversion 小于30
    • SAF的底层实现ExternalStorageProvider也是通过File API来访问文件系统的
    • SAF之所以能访问Android/data和Android/obb是因为ExternalStorageProvider
      进程具有GID 1078 和1079,三方APP是不可能拥有这些GID的

    相关文章

      网友评论

          本文标题:Android R 如何访问Android/data目录?

          本文链接:https://www.haomeiwen.com/subject/uqkifltx.html