盒子
盒子
文章目录
  1. 内部储存
    1. SharedUserId
  2. 外部存储
    1. Android 11以前
      1. /mnt/runtime目录
        1. group
        2. mask
      2. 外部存储读写权限原理
      3. 间接挂载
      4. 运行时权限
      5. 缺点
    2. Android 11以后
      1. FUSE
      2. FUSE daemon
      3. 目录隔离
      4. 媒体数据库
      5. 文件隔离

安卓存储权限原理

上篇博客介绍了FileProvider是如何跨应用访问文件的。这篇博客我们来讲讲安卓是如何控制文件的访问权限的。

内部储存

由于安卓基于Linux,所以最简单的文件访问权限控制方法就是使用Linux的文件权限机制.例如应用的私有目录就是这么实现的。

安卓系统为每个安卓的应用都分配了一个用户和用户组,我们可以通过ps命令查看运行中的应用对应的用户:

1
2
3
4
USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME
...
u0_a66 2685 1085 3914640 70688 SyS_epoll_wait 0 S me.linw.demo
...

这里的u0_a66指的是应用的user name,它表示该应用是user 0(这里指的是安卓多用户模式下的主用户,和前面讲的Linux用户不是同一个概念)下面的应用id是66.由于通应用程序的user id都是从10000开始,所以这个应用的user id是10066.可以从/data/system/packages.list文件中确认:

1
me.linw.demo 10066 1 /data/user/0/me.linw.demo default:targetSdkVersion=30 3003

应用的私有目录为/data/data/${包名}/,可以看到安卓系统给应用创建了一个权限为700的目录,文件的owner和group都只属于这个应用,这样就保证了每个应用的私有目录只有自己可以访问:

1
2
# ls -l /data/data/ | grep me.linw.demo
drwx------ 5 u0_a66 u0_a66 4096 2023-03-07 19:32 me.linw.demo

SharedUserId

当然也可以在AndroidManifest.xml里面配置android:sharedUserId让他们是用同一个User:

1
2
3
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.linw.demo2"
android:sharedUserId="test.same.user">
1
2
3
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.linw.demo"
android:sharedUserId="test.same.user">

这样的话两个应用的user就是一样的,就能相互访问私有目录了:

1
2
drwx------ 4 u0_a66 u0_a66 4096 2023-03-10 17:07 me.linw.demo2
drwx------ 5 u0_a66 u0_a66 4096 2023-03-10 16:53 me.linw.demo

外部存储

外部存储的文件系统几经变更。从早期的FUSE到Android 8改为性能更优的SDCardFS,再到Android 11上为了更细的管理文件权限又换回FUSE。各个安卓版本的实现细节也稍有差异,过于老旧的版本也没有学习的必要,这里只拿比较有代表性的Android 8和Android 11进行源码分析。

Android 11以前

安卓11以前的外部存储权限控制做的比较粗糙。应用申请了WREAD_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE就可以对外部存储进行读写。

这个外部存储一般指的是/storage/emulated/目录,它为每个用户分配了一个子目录。例如0子目录就是user 0(主用户)的外部存储目录.

这里我们用一个shellExec在进程里面执行命令协助我们理解外部存储的管理原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void shellExec(String shell) throws IOException {
InputStream is = Runtime.getRuntime().exec(shell).getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
char[] buff = new char[1024];
int ch;
while ((ch = reader.read(buff)) != -1) {
sb.append(buff, 0, ch);
}
reader.close();
Log.d("ExecShell", shell);
Log.d("ExecShell", sb.toString());
}

申请READ_EXTERNAL_STORAGE权限之后执行ls -l /storage/emulated/0/就可以看到熟悉的外部存储目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shellExec("ls -l /storage/emulated/0/");


03-11 17:02:26.861 3411 3411 D ExecShell: ls -l /storage/emulated/0/
03-11 17:02:26.861 3411 3411 D ExecShell: total 40
03-11 17:02:26.861 3411 3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Alarms
03-11 17:02:26.861 3411 3411 D ExecShell: drwxr-x--- 3 root everybody 4096 2023-03-08 14:13 Android
03-11 17:02:26.861 3411 3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 DCIM
03-11 17:02:26.861 3411 3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2023-03-07 19:49 Download
03-11 17:02:26.861 3411 3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Movies
03-11 17:02:26.861 3411 3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Music
03-11 17:02:26.861 3411 3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Notifications
03-11 17:02:26.861 3411 3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2023-03-07 19:46 Pictures
03-11 17:02:26.861 3411 3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Podcasts
03-11 17:02:26.861 3411 3411 D ExecShell: drwxr-x--- 2 root everybody 4096 2022-04-24 20:25 Ringtones

这里可以看到虽然这些目录的user是root,但是所属的group是everybody,即所有人对这些目录都有r-x的权限可读可进入文件夹。

而如果申请了WRITE_EXTERNAL_STORAGE权限之后再执行ls -l /storage/emulated/0/就会看见group的权限变成了rwx可读可写可进入文件夹。

1
2
3
4
5
03-11 17:10:44.146  3646  3646 D ExecShell: ls -l /storage/emulated/0/
03-11 17:10:44.146 3646 3646 D ExecShell: total 40
03-11 17:10:44.146 3646 3646 D ExecShell: drwxrwx--- 2 root everybody 4096 2022-04-24 20:25 Alarms
03-11 17:10:44.146 3646 3646 D ExecShell: drwxrwx--- 3 root everybody 4096 2023-03-08 14:13 Android
...

也就是说不同的权限下应用看到/storage/emulated/0/的文件权限是不一样的,这一点又是怎么做的的呢?

/mnt/runtime目录

这里先介绍/mnt/runtime下的三个目录:

1
2
3
4
mount | grep /mnt/runtime
/data/media on /mnt/runtime/default/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,multiuser,mask=6,derive_gid)
/data/media on /mnt/runtime/read/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=23,derive_gid)
/data/media on /mnt/runtime/write/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=7,derive_gid)

可以看到/mnt/runtime/default/emulated/mnt/runtime/read/emulated/mnt/runtime/write/emulated都挂载了/data/media。只不过他们的gid、和mask不尽相同。

group

其实这三个目录都是通过bind mount机制(普通的mount只能挂载设备,但是bind mount可以挂载目录)挂载的/data/media目录,gid指的是挂载之后修改文件系统下文件的group:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ls -l /data/media
total 8
drwxrwx--- 12 media_rw media_rw 4096 2023-03-11 16:51 0
drwxrwxr-x 2 media_rw media_rw 4096 1970-01-01 08:00 obb

# ls -l /mnt/runtime/default/emulated
total 8
drwxrwx--x 12 root sdcard_rw 4096 2023-03-11 16:51 0
drwxrwx--x 2 root sdcard_rw 4096 1970-01-01 08:00 obb

# ls -l /mnt/runtime/read/emulated
total 8
drwxr-x--- 12 root everybody 4096 2023-03-11 16:51 0
drwxr-x--- 2 root everybody 4096 1970-01-01 08:00 obb

# ls -l /mnt/runtime/write/emulated
total 8
drwxrwx--- 12 root everybody 4096 2023-03-11 16:51 0
drwxrwx--- 2 root everybody 4096 1970-01-01 08:00 obb

可以看到原本/data/media下的文件group是media_rw(id=1023),但挂载之后/mnt/runtime/default/emulated的group是sdcard_rw(id=1015),/mnt/runtime/read/emulated/mnt/runtime/write/emulated的group是everybody(id=9997)。

这些group的id可以在android_filesystem_config.h看到:

1
2
3
4
5
6
7
8
9
// http://androidxref.com/8.0.0_r4/xref/system/core/include/private/android_filesystem_config.h

...
#define AID_SDCARD_RW 1015 /* external storage write access */
...
#define AID_MEDIA_RW 1023 /* internal media storage write access */
...
#define AID_EVERYBODY 9997 /* shared between all apps in the same profile */
...

mask

而mask则是用来重新定义文件的rwx权限的,挂载后文件的权限通过0775 & ~mask计算得到(注意这里的0775指定是8进制的775,即十进制的509):

1
2
3
4
5
6
7
// https://android.googlesource.com/kernel/common.git/+/experimental/android-4.9/fs/sdcardfs/sdcardfs.h

static inline int get_mode(struct vfsmount *mnt, struct sdcardfs_inode_info *info) {
...
int visible_mode = 0775 & ~opts->mask;
...
}

所以:

/mnt/runtime/default/emulated的权限为0775 & ~6:

1
2
3
4
0775 =  111111101 = 111111101
~6 = ~000000110 = 111111001
------------------------------
111111001 = rwxrwx--x

/mnt/runtime/read/emulated的权限为0775 & ~23:

1
2
3
4
0775 =  111111101 = 111111101
~23 = ~000010111 = 111101000
------------------------------
111101000 = rwxr-x---

/mnt/runtime/default/emulated的权限为0775 & ~7:

1
2
3
4
0775 =  111111101 = 111111101
~7 = ~000000111 = 111111000
------------------------------
111111000 = rwxrwx---

综上所述:

  • /mnt/runtime/default/emulated : 普通应用由于不在media_rw组,只有进入子目录的权限,并不能读写。
  • /mnt/runtime/read/emulated : 普通应用属于everybody组,有r-x权限
  • /mnt/runtime/default/emulated : 普通应用属于everybody组,有rwx权限

外部存储读写权限原理

实际上外部存储路径/storage/emulated是通过挂载前面所说的三个目录去实现不同的访问权限的。

在Zygote进程fork应用进程的时候会通过Linux的bind mount机制为应用在私有挂载空间挂载/storage目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// https://cs.android.com/android/platform/superproject/+/android-8.0.0_r1:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

// Create a private mount namespace and bind mount appropriate emulated
// storage for the given user.
static bool MountEmulatedStorage(uid_t uid, jint mount_mode,
bool force_mount_namespace) {
// See storage config details at http://source.android.com/tech/storage/

String8 storageSource;
if (mount_mode == MOUNT_EXTERNAL_DEFAULT) {
storageSource = "/mnt/runtime/default";
} else if (mount_mode == MOUNT_EXTERNAL_READ) {
storageSource = "/mnt/runtime/read";
} else if (mount_mode == MOUNT_EXTERNAL_WRITE) {
storageSource = "/mnt/runtime/write";
} else if (!force_mount_namespace) {
// Sane default of no storage visible
return true;
}

// Create a second private mount namespace for our process
if (unshare(CLONE_NEWNS) == -1) {
ALOGW("Failed to unshare(): %s", strerror(errno));
return false;
}

...

if (TEMP_FAILURE_RETRY(mount(storageSource.string(), "/storage",
NULL, MS_BIND | MS_REC | MS_SLAVE, NULL)) == -1) {
ALOGW("Failed to mount %s to /storage: %s", storageSource.string(), strerror(errno));
return false;
}

...
}

系统根据应用的外部存储权限传入不同的mount_mode:

  • 没有权限挂载/mnt/runtime/default
  • 有READ_EXTERNAL_STORAGE权限挂载/mnt/runtime/read
  • 有WRITE_EXTERNAL_STORAGE权限挂载/mnt/runtime/write

由于使用了unshare所以挂载的/storage实际是在应用的私有挂载空间,即每个应用挂载的/storage是仅自己可见其他应用不可见的。

而这里使用了MS_REC参数,所以会递归挂载子目录,即:/mnt/runtime/default挂载到/storage的同时/mnt/runtime/default/emulated也会挂载到/storage/emulated

间接挂载

不过通过mount命令可以看到/storage/emulated实际上也是挂载了/data/media,而不是前面说的三个目录:

1
2
3
4
03-11 17:13:36.495  3778  3778 D ExecShell: mount
...
03-11 17:13:36.495 3778 3778 D ExecShell: /data/media on /storage/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=7,derive_gid)
...

这是由于bind mount的特性,并不能看到间接挂载的过程。例如我们可以将/mnt/runtime/default/emulated通过bind mount挂载到/data/test/,然后用mount命令可以看到/data/test也是挂载了/data/media:

1
2
3
# mount --bind /mnt/runtime/default/emulated  /data/test
# mount | grep /data/test
/data/media on /data/test type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,multiuser,mask=6,derive_gid)

运行时权限

Android 6之后导入了运行时权限,READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE需要在运行时申请.

所以应用在第一次启动的时候还没有外部存储的权限,挂载的是/mnt/runtime/default.

当运行时权限申请成功之后就会触发StorageManagerInternalImpl.onExternalStoragePolicyChanged然后去给这个应用重新挂载/storage/emulated:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// https://cs.android.com/android/platform/superproject/+/android-8.0.0_r1:frameworks/base/services/core/java/com/android/server/StorageManagerService.java
private final class StorageManagerInternalImpl extends StorageManagerInternal {
...
@Override
public void onExternalStoragePolicyChanged(int uid, String packageName) {
final int mountMode = getExternalStorageMountMode(uid, packageName);
remountUidExternalStorage(uid, mountMode);
}
...
}

private void remountUidExternalStorage(int uid, int mode) {
waitForReady();

String modeName = "none";
switch (mode) {
case Zygote.MOUNT_EXTERNAL_DEFAULT: {
modeName = "default";
} break;

case Zygote.MOUNT_EXTERNAL_READ: {
modeName = "read";
} break;

case Zygote.MOUNT_EXTERNAL_WRITE: {
modeName = "write";
} break;
}

try {
mConnector.execute("volume", "remount_uid", uid, modeName);
} catch (NativeDaemonConnectorException e) {
Slog.w(TAG, "Failed to remount UID " + uid + " as " + modeName + ": " + e);
}
}

最终会调用到VolumeManager::remountUid从proc查找应用进程对应的私有挂载空间,重新根据权限挂载/storage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// https://cs.android.com/android/platform/superproject/+/android-8.0.0_r1:system/vold/VolumeManager.cpp

int VolumeManager::remountUid(uid_t uid, const std::string& mode) {
...

if (!(dir = opendir("/proc"))) {
PLOG(ERROR) << "Failed to opendir";
return -1;
}

...

// 遍历/proc的子目录
while ((de = readdir(dir))) {
pidFd = -1;
nsFd = -1;

pidFd = openat(dirfd(dir), de->d_name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (pidFd < 0) {
goto next;
}
if (fstat(pidFd, &sb) != 0) {
PLOG(WARNING) << "Failed to stat " << de->d_name;
goto next;
}

// 对比uid,查找uid对应的进程目录
if (sb.st_uid != uid) {
goto next;
}

...

// 读取私有挂载空间的id
nsFd = openat(pidFd, "ns/mnt", O_RDONLY); // not O_CLOEXEC
if (nsFd < 0) {
PLOG(WARNING) << "Failed to open namespace for " << de->d_name;
goto next;
}

// 开启子进程实现并发
if (!(child = fork())) {
// 进入应用进程的私有挂载空间
if (setns(nsFd, CLONE_NEWNS) != 0) {
PLOG(ERROR) << "Failed to setns for " << de->d_name;
_exit(1);
}

// 解除/storage挂载
unmount_tree("/storage");

// 根据权限挂载对应目录
std::string storageSource;
if (mode == "default") {
storageSource = "/mnt/runtime/default";
} else if (mode == "read") {
storageSource = "/mnt/runtime/read";
} else if (mode == "write") {
storageSource = "/mnt/runtime/write";
} else {
// Sane default of no storage visible
_exit(0);
}

//重新挂载/storage
if (TEMP_FAILURE_RETRY(mount(storageSource.c_str(), "/storage",
NULL, MS_BIND | MS_REC, NULL)) == -1) {
PLOG(ERROR) << "Failed to mount " << storageSource << " for "
<< de->d_name;
_exit(1);
}
...

_exit(0);
}

if (child == -1) {
PLOG(ERROR) << "Failed to fork";
goto next;
} else {
TEMP_FAILURE_RETRY(waitpid(child, nullptr, 0));
}

next:
close(nsFd);
close(pidFd);
}
closedir(dir);
return 0;
}

缺点

这种权限管理的方式比较粗犷,一旦获取了读写的权限就能对外部存储的任意目录进行读写,例如应用的外部存储路径/storage/emulated/0/Android/data/${包名}/:

1
2
3
4
5
shellExec("ls -l /storage/emulated/0/Android/data");

03-12 18:48:12.809 2934 2934 D ExecShell: ls -l /storage/emulated/0/Android/data
03-12 18:48:12.809 2934 2934 D ExecShell: total 4
03-12 18:48:12.809 2934 2934 D ExecShell: drwxrwx--- 3 u0_a15 everybody 4096 2023-03-12 18:47 com.android.launcher3

获取到读取权限之后就能对其他应用的外部存储路径进行读写了。因此一些敏感的信息一般不会写入到下面方法获取出来的路径:

1
2
3
4
5
public File getExternalFilesDir(String type)
public File[] getExternalFilesDirs(String type)
public File getExternalCacheDir()
public File[] getExternalCacheDirs()
public File[] getExternalMediaDirs()

Android 11以后

安卓11为了更好的管控外部存储的权限,废弃了READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE,使用分区存储(Scoped Storage)的去管理外部存储:

1
2
3
4
5
6
使用分区存储的应用可具有以下访问权限级别(实际访问权限因实现而异)。

- 对自己的文件拥有读取和写入访问权限(没有权限限制)
- 对其他应用的媒体文件拥有读取访问权限(需要具备 READ_EXTERNAL_STORAGE 权限)
- 只有在用户直接同意的情况下,才允许对其他应用的媒体文件拥有写入访问权限(系统图库以及符合“所有文件访问权限”获取条件的应用除外)
- 对其他应用的外部应用数据目录没有读取或写入访问权限

应用端具体的适配方法在网上有很多文章有提及,无非是通过MediaStore或者SAF去访问外部存储,我这边就不做介绍了。这篇博客主要介绍系统端是如何实现外部存储的权限管理的。

FUSE

为了实现分区存储,前面的bind mount机制是无法做到这么细致的管理的。所以在Android 11谷歌又废弃了Android 8导入的SDCardFS,回归FUSE机制。

FUSE是由Linux Kernel提供的一种文件系统。它的框架图如下:

Linux为了支持多种文件系统(如EXT4, NTFS, FAT等)抽象了一个虚拟文件系统层(VFS),FUSE就是其中的一种.

从上面的框架图可以看到,在用户空间会有一个FUSE daemon进程监听对FUSE文件系统的操作,然后对其进行转发给到其他的文件系统。

由于是在FUSE是kernel提供的机制,所以无论应用是通过java还是native方法去操作的文件,安卓都可以在FUSE daemon对文件的操作请求进行权限鉴别和拦截。

FUSE daemon

例如使用FileOutputStream在外部存储创建文件的时候会回调到FuseDaemon的pf_create:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/jni/FuseDaemon.cpp
static void pf_create(fuse_req_t req,
fuse_ino_t parent,
const char* name,
mode_t mode,
struct fuse_file_info* fi) {
...
if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
fuse_reply_err(req, ENOENT);
return;
}

TRACE_NODE(parent_node, req);

const string child_path = parent_path + "/" + name;

int mp_return_code = fuse->mp->InsertFile(child_path.c_str(), req->ctx.uid);
if (mp_return_code) {
fuse_reply_err(req, mp_return_code);
return;
}

...
// Let MediaProvider know we've created a new file
fuse->mp->OnFileCreated(child_path);
...
}

从代码上我们看到首先它会调用is_app_accessible_path去判断应用的访问权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const std::regex PATTERN_OWNED_PATH(
"^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)/([^/]+)(/?.*)?",
std::regex_constants::icase);

static bool is_app_accessible_path(MediaProviderWrapper* mp, const string& path, uid_t uid) {
// 系统权限的应用会被允许访问, FuseDaemon进程自己也允许访问
if (uid < AID_APP_START || uid == MY_UID) {
return true;
}

//应用不能直接访问/storage/emulated,只能访问它的子目录,例如/storage/emulated/0
if (path == "/storage/emulated") {
return false;
}

std::smatch match;
if (std::regex_match(path, match, PATTERN_OWNED_PATH)) {
const std::string& pkg = match[1];
...
if (!mp->IsUidForPackage(pkg, uid)) {
// /storage/emulated/0/Andrdoi/data/${包名} 这样的目录不允许其他应用访问
PLOG(WARNING) << "Invalid other package file access from " << pkg << "(: " << path;
return false;
}
}
return true;
}

然后会调用fuse->mp->InsertFile去通过jni回调到java层的MediaProvider.insertFileIfNecessaryForFuse去插入文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/jni/MediaProviderWrapper.cpp
int MediaProviderWrapper::InsertFile(const string& path, uid_t uid) {
...
return insertFileInternal(env, media_provider_object_, mid_insert_file_, path, uid);
}

int insertFileInternal(JNIEnv* env, jobject media_provider_object, jmethodID mid_insert_file,
const string& path, uid_t uid) {
ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
int res = env->CallIntMethod(media_provider_object, mid_insert_file, j_path.get(), uid);
...
}

MediaProviderWrapper::MediaProviderWrapper(JNIEnv* env, jobject media_provider) {
...
media_provider_class_ = env->FindClass("com/android/providers/media/MediaProvider");
...
mid_insert_file_ = CacheMethod(env, "insertFileIfNecessary", "(Ljava/lang/String;I)I",
/*is_static*/ false);
...
}

jmethodID MediaProviderWrapper::CacheMethod(JNIEnv* env, const char method_name[],
const char signature[], bool is_static) {
jmethodID mid;
string actual_method_name(method_name);
actual_method_name.append("ForFuse");
if (is_static) {
mid = env->GetStaticMethodID(media_provider_class_, actual_method_name.c_str(), signature);
} else {
mid = env->GetMethodID(media_provider_class_, actual_method_name.c_str(), signature);
}
...
}

目录隔离

insertFileIfNecessaryForFuse会通过文件的后缀解析出mimeType(例如.jpg就是图片类型,.mp4就是视频类型),然后创建contentUri调用insertFileForFuse:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) {
...
final String mimeType = MimeUtils.resolveMimeType(new File(path));
...
final Uri contentUri = getContentUriForFile(path, mimeType);
final Uri item = insertFileForFuse(path, contentUri, mimeType, /*useData*/ false);
if (item == null) {
return OsConstants.EPERM;
}
...
}

private Uri insertFileForFuse(@NonNull String path, @NonNull Uri uri, @NonNull String mimeType,
boolean useData) {
ContentValues values = new ContentValues();
values.put(FileColumns.OWNER_PACKAGE_NAME, getCallingPackageOrSelf());
values.put(MediaColumns.MIME_TYPE, mimeType);
values.put(FileColumns.IS_PENDING, 1);

if (useData) {
values.put(FileColumns.DATA, path);
} else {
values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
}
return insert(uri, values, Bundle.EMPTY);
}

insert里面会对文件类型和存放的路径做校验,也就是说外部存储公共目录下只能存放特定类型的文件,例如Movies下只能放视频文件、Music下只能放音频文件、Pictures下只能放图片文件等。你不能将png的图片放到/storage/emulated/0/Movies下:

1
2
03-14 19:48:04.683  1774  2181 E MediaProvider: java.lang.IllegalArgumentException: MIME type image/png cannot be inserted into content://media
/external_primary/video/media; expected MIME type under video/*

这个校验是在insert里面调用ensureFileColumns方法去检查的:

1
2
3
4
5
6
7
8
9
10
11
12
13
// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
@NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)
throws VolumeArgumentException, VolumeNotFoundException {
...
else if (defaultMediaType != actualMediaType) {
final String[] split = defaultMimeType.split("/");
throw new IllegalArgumentException(
"MIME type " + mimeType + " cannot be inserted into " + uri
+ "; expected MIME type under " + split[0] + "/*");
}
...
}

而像/storage/emulated/0/Android/media/${包名}这样的外部媒体私有路径也会被拦截下来:

1
03-14 20:11:49.541  1774  2038 E MediaProvider: java.lang.IllegalArgumentException: Primary directory Android not allowed for content://media/external_primary/file; allowed directories are [Download, Documents]

它同样是在ensureFileColumns里面拦截的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
@NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)
throws VolumeArgumentException, VolumeNotFoundException {
...
// Consider allowing external media directory of calling package
if (!validPath) {
final String pathOwnerPackage = extractPathOwnerPackageName(res.getAbsolutePath());
if (pathOwnerPackage != null) {
validPath = isExternalMediaDirectory(res.getAbsolutePath()) &&
isCallingIdentitySharedPackageName(pathOwnerPackage);
}
}
...
if (!validPath) {
throw new IllegalArgumentException(
"Primary directory " + primary + " not allowed for " + uri
+ "; allowed directories are " + allowedPrimary);
}
...
}

private boolean isExternalMediaDirectory(@NonNull String path) {
final String relativePath = extractRelativePath(path);
if (relativePath != null) {
return relativePath.startsWith("Android/media");
}
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/src/com/android/providers/media/util/FileUtils.java
public static final Pattern PATTERN_OWNED_PATH = Pattern.compile(
"(?i)^/storage/[^/]+/(?:[0-9]+/)?"
+ PROP_CROSS_USER_ROOT_PATTERN
+ "Android/(?:data|media|obb)/([^/]+)(/?.*)?");

public static @Nullable String extractPathOwnerPackageName(@Nullable String path) {
if (path == null) return null;
final Matcher m = PATTERN_OWNED_PATH.matcher(path);
if (m.matches()) {
return m.group(1);
}
return null;
}

从上面的错误日志可以看出来Download, Documents是公共目录。实际上这两个目录不会检查文件类型,可以存放所有类型的文件。

媒体数据库

另外我们看到insertFileForFuse里面会创建ContentValues去调用insert,这里的代码其实和应用层使用MediaStore去访问外部存储基本一致了。

insert的意思实际上是插入到MediaProvider数据库,所以我们可以从MediaProvider数据库通过文件类型查找文件(例如音乐播放器可以通过MediaProvider查找到手机上的所有音频文件):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// https://cs.android.com/android/platform/superproject/+/android-mainline-11.0.0_r1:packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java
public @Nullable Uri insert(@NonNull Uri uri, @Nullable ContentValues values,
@Nullable Bundle extras) {
...
return insertInternal(uri, values, extras);
...
}

private @Nullable Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
@Nullable Bundle extras) throws FallbackException {
...
final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null);
...
switch (match) {
case IMAGES_MEDIA: {
..
newUri = insertFile(qb, helper, match, uri, extras, initialValues,
FileColumns.MEDIA_TYPE_IMAGE);
break;
}

case IMAGES_THUMBNAILS: {
...

rowId = qb.insert(helper, initialValues);
if (rowId > 0) {
newUri = ContentUris.withAppendedId(Images.Thumbnails.
getContentUri(originalVolumeName), rowId);
}
break;
}

case VIDEO_THUMBNAILS: {
...

rowId = qb.insert(helper, initialValues);
if (rowId > 0) {
newUri = ContentUris.withAppendedId(Video.Thumbnails.
getContentUri(originalVolumeName), rowId);
}
break;
}

case AUDIO_MEDIA: {
...
newUri = insertFile(qb, helper, match, uri, extras, initialValues,
FileColumns.MEDIA_TYPE_AUDIO);
break;
}
...
}
...
}

private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper,
int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
int mediaType) throws VolumeArgumentException, VolumeNotFoundException {
...
rowId = insertAllowingUpsert(qb, helper, values, path);
...
}


private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb,
@NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)
throws SQLiteConstraintException {
return helper.runWithTransaction((db) -> {
...
return qb.insert(helper, values);
...
}
}

文件隔离

虽然前面讲到Download, Document是公共目录,谁都可以往里面写入文件。但是正常情况下普通应用只能读取自己写入的问题,没有权限读取其他应用写入的文件:

1
1774  2181 E MediaProvider: Permission to access file: //storage/emulated/0/Download/OtherAppFile.txt is denied

这是因为打开文件的时候会触发到FuseDaemon的pf_open:

1
2
3
4
5
6
7
static void pf_open(fuse_req_t req, fuse_ino_t ino, struct fuse_file_info* fi) {
...
std::unique_ptr<FileOpenResult> result = fuse->mp->OnFileOpen(
build_path, io_path, ctx->uid, ctx->pid, node->GetTransformsReason(), for_write,
!for_write /* redact */, true /* log_transforms_metrics */);
...
}

最终去到MediaProvider.onFileOpenForFuse在里面调用checkAccess检查访问权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public FileOpenResult onFileOpenForFuse(String path, String ioPath, int uid, int tid,
int transformsReason, boolean forWrite, boolean redact, boolean logTransformsMetrics) {
...
try {
...
checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
...
} catch (IllegalStateException | SecurityException e) {
Log.e(TAG, "Permission to access file: " + path + " is denied");
return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
mediaCapabilitiesUid, new long[0]);
}
...
}

checkAccess最终最一堆的权限检查,如果没有符合的就抛出SecurityException异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void checkAccess(@NonNull Uri uri, @NonNull Bundle extras, @NonNull File file,
boolean isWrite) throws FileNotFoundException {
enforceCallingPermission(uri, extras, isWrite);
...
}

private void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras,
boolean forWrite) {
...
enforceCallingPermissionInternal(uri, extras, forWrite);
...
}

private void enforceCallingPermissionInternal(@NonNull Uri uri, @NonNull Bundle extras,
boolean forWrite) {
...
if (checkCallingPermissionGlobal(uri, forWrite)) {
// Access allowed, yay!
return;
}
...
throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri);
}

其中checkCallingPermissionGlobal会检测android.permission.MANAGE_EXTERNAL_STORAGE权限,也就是文件管理器可以读取外部存储的所有公有文件的原理(例如Android/data/${包名}下的文件在前面的判断里面会跳出所以还是不能访问):

1
2
3
4
5
6
7
8
private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
...
// Apps that have permission to manage external storage can work with all files
if (isCallingPackageManager()) {
return true;
}
...
}

开启文件管理器权限需要:

  1. 在AndroidManifest.xml声明android.permission.MANAGE_EXTERNAL_STORAGE权限
  2. 使用Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION启动设置页面让用户手动打开该应用的文件管理权限:
1
2
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);