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

前言

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的
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容