上篇博客 介绍了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/mediatotal 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/emulatedtotal 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/emulatedtotal 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/emulatedtotal 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 ... #define AID_SDCARD_RW 1015 ... #define AID_MEDIA_RW 1023 ... #define AID_EVERYBODY 9997 ...
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 static bool MountEmulatedStorage (uid_t uid, jint mount_mode, bool force_mount_namespace) { 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) { return true ; } 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 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 int VolumeManager::remountUid(uid_t uid, const std ::string & mode) { ... if (!(dir = opendir("/proc" ))) { PLOG(ERROR) << "Failed to opendir" ; return -1 ; } ... 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; } if (sb.st_uid != uid) { goto next; } ... nsFd = openat(pidFd, "ns/mnt" , O_RDONLY); 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 ); } 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 { _exit(0 ); } 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 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 ; } ... 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) { if (uid < AID_APP_START || uid == MY_UID) { return true ; } 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)) { 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 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" , 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 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, 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 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 private void ensureFileColumns (int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath) throws VolumeArgumentException, VolumeNotFoundException { ... 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 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 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 , true ); ... }
最终去到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 , 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)) { 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) { ... if (isCallingPackageManager()) { return true ; } ... }
开启文件管理器权限需要:
在AndroidManifest.xml声明android.permission.MANAGE_EXTERNAL_STORAGE权限
使用Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION启动设置页面让用户手动打开该应用的文件管理权限:
1 2 Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); startActivity(intent);