文件访问权限是 Android 系统中一个非常复杂且重要的内容,它决定了应用程序能够访问和操作哪些文件和目录。Android 系统对文件访问权限有严格的限制和管理。随着 Android 系统版本的不断更新,文件访问权限也有一些变化和变更,需要我们及时了解和学习 以便应对需求。这块内容非常复杂庞大且内容很杂 。。本文简单学习 Android 系统的文件访问权限的概念、分类、申请方法、framework层源码位置和关键函数,以及 Android 11、12 和 13 的文件访问权限的变化和适配方法。
系列文章:
Android系统 理解/sys/目录权限和UID和GID?
参考学习:
Android 11 system_server 读写 SDCARD
解决Android10读取不到/sdcard/、/storage/emulated/0/文件的问题
Android系统 理解/sys/目录权限和UID和GID?
在安卓11/12/13下修改android/data和obb内容的经验记录
Android 13 功能和变更列表 | Android 开发者 | Android Developers
Android11 处理文件 出现 open failed: EACCES (Permission denied) 问题
Android外部存储权限的变化与适配
从Android 10版本开始,普通应用无法直接读取系统/sdcard/、/mnt/下的sdcard和/mnt/下的u盘。这是因为Android 10引入了分区存储机制,旨在防止应用读取其他应用的数据。每个应用都应有自己的存储空间,不能越权访问公共目录。应用在请求数据时需要经过权限检测。
系统应用与权限
应用如果修改为系统平台签名或者设置android:sharedUserId="android.uid.system"
,并添加相应的读写权限,就可以读写系统/sdcard/、/mnt/下的sdcard和/mnt/下的u盘。这是因为这样的应用会与system拥有相同的签名,可以使用android:sharedUserId="android.uid.system"
来共享system的UID,从而访问系统的核心功能和数据。
通过修改 android:sharedUserId="android.uid.system"
,可以让应用的 UID 变为系统的 UID ,从而可以访问到一些目录。这是因为系统的 UID 是 1000 ,具有最高的权限。
可以在Android源码的system/core/include/private/android_filesystem_config.h路径下找到这个文件。
#define AID_ROOT 0 /* traditional unix root user */ /* The following are for LTP and should only be used for testing */ #define AID_DAEMON 1 /* traditional unix daemon owner */ #define AID_BIN 2 /* traditional unix binaries owner */ #define AID_SYSTEM 1000 /* system server */ 。。。。。。。。。。
Android源码与权限检测
文件访问权限是 Android 系统中,它决定了应用程序能够访问和操作哪些文件和目录。Android 系统对文件访问权限有严格的限制和管理,,其中有一些关键的类和方法。
Android 系统的文件访问权限的源码位置和关键函数可以根据不同的功能和模块进行区分,主要有以下几个:
StorageManagerService.java
- xref:
http://aospxref.com/android-13.0.0_r3/xref/frameworks/base/services/core/java/com/android/server/StorageManagerService.java
:这个类负责管理外部存储设备的挂载和卸载,以及分配和回收存储空间。它是一个系统服务,可以通过 Context.getSystemService(Context.STORAGE_SERVICE) 来获取它的实例。它提供了一些关键的方法,如:
mount(String volId):
// 这个方法是 StorageManagerService 类的一个公开方法,用于挂载指定 ID 的外部存储设备卷 2309 @Override 2310 public void mount(String volId) { 2311 // 检查调用者是否有 MOUNT_UNMOUNT_FILESYSTEMS 权限,如果没有,抛出 SecurityException 异常 2312 enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); 2313 2314 // 根据卷的 ID 找到对应的 VolumeInfo 对象,如果找不到,抛出 IllegalArgumentException 异常 2315 final VolumeInfo vol = findVolumeByIdOrThrow(volId); 2316 // 判断是否允许挂载该卷,如果不允许,抛出 SecurityException 异常 2317 if (isMountDisallowed(vol)) { 2318 throw new SecurityException("Mounting " + volId + " restricted by policy"); 2319 } 2320 2321 // 调用私有的 mount 方法,传入 VolumeInfo 对象,用于执行挂载操作 2322 mount(vol); 2323 } 2324 2325 // 这个方法是 StorageManagerService 类的一个私有方法,用于挂载指定的 VolumeInfo 对象 2326 private void mount(VolumeInfo vol) { 2327 try { 2328 // TODO(b/135341433): Remove cautious logging when FUSE is stable 2329 // 记录日志,表示正在挂载卷 2330 Slog.i(TAG, "Mounting volume " + vol); 2331 // 调用 mVold 的 mount 方法,传入卷的 ID、挂载标志、挂载用户 ID 和一个回调对象 2332 mVold.mount(vol.id, vol.mountFlags, vol.mountUserId, new IVoldMountCallback.Stub() { 2333 // 这个回调对象实现了 IVoldMountCallback 接口,它有一个 onVolumeChecking 方法,用于在挂载前对卷进行检查 2334 @Override 2335 public boolean onVolumeChecking(FileDescriptor fd, String path, 2336 String internalPath) { 2337 // 将卷的路径和内部路径设置为传入的参数值 2338 vol.path = path; 2339 vol.internalPath = internalPath; 2340 // 将文件描述符封装成一个 ParcelFileDescriptor 对象 2341 ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd); 2342 try { 2343 // 调用 mStorageSessionController 的 onVolumeMount 方法,传入该对象和卷的信息 2344 mStorageSessionController.onVolumeMount(pfd, vol); 2345 // 如果 onVolumeMount 方法成功返回,表示挂载成功,返回 true 2346 return true; 2347 } catch (ExternalStorageServiceException e) { 2348 // 如果 onVolumeMount 方法抛出 ExternalStorageServiceException 异常,表示挂载失败,返回 false,并在一定时间后重新尝试挂载 2349 Slog.e(TAG, "Failed to mount volume " + vol, e); 2350 2351 int nextResetSeconds = FAILED_MOUNT_RESET_TIMEOUT_SECONDS; 2352 Slog.i(TAG, "Scheduling reset in " + nextResetSeconds + "s"); 2353 mHandler.removeMessages(H_RESET); 2354 mHandler.sendMessageDelayed(mHandler.obtainMessage(H_RESET), 2355 TimeUnit.SECONDS.toMillis(nextResetSeconds)); 2356 return false; 2357 } finally { 2358 // 无论成功或失败,都要关闭文件描述符对象 2359 try { 2360 pfd.close(); 2361 } catch (Exception e) { 2362 Slog.e(TAG, "Failed to close FUSE device fd", e); 2363 } 2364 } 2365 } 2366 }); 2367 // 记录日志,表示已经挂载卷 2368 Slog.i(TAG, "Mounted volume " + vol); 2369 } catch (Exception e) { 2370 // 如果发生其他异常,记录错误日志 2371 Slog.wtf(TAG, e); 2372 } 2373 }
unmount(String volId):
// 这个方法是 StorageManagerService 类的一个公开方法,用于卸载指定 ID 的外部存储设备卷 2377 @Override 2378 public void unmount(String volId) { 2379 // 检查调用者是否有 MOUNT_UNMOUNT_FILESYSTEMS 权限,如果没有,抛出 SecurityException 异常 2380 enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS); 2381 2382 // 根据卷的 ID 找到对应的 VolumeInfo 对象,如果找不到,抛出 IllegalArgumentException 异常 2383 final VolumeInfo vol = findVolumeByIdOrThrow(volId); 2384 // 调用私有的 unmount 方法,传入 VolumeInfo 对象,用于执行卸载操作 2385 unmount(vol); 2386 } 2387 // 这个方法是 StorageManagerService 类的一个私有方法,用于卸载指定的 VolumeInfo 对象 2389 private void unmount(VolumeInfo vol) { 2390 try { 2391 try { 2392 // 判断卷的类型是否为 VolumeInfo.TYPE_PRIVATE,如果是,调用 mInstaller 的 onPrivateVolumeRemoved 方法,传入卷的 UUID,用于移除私有卷的镜像数据 2393 if (vol.type == VolumeInfo.TYPE_PRIVATE) { 2394 mInstaller.onPrivateVolumeRemoved(vol.getFsUuid()); 2395 } 2396 } catch (Installer.InstallerException e) { 2397 // 如果 onPrivateVolumeRemoved 方法抛出 Installer.InstallerException 异常,记录错误日志 2398 Slog.e(TAG, "Failed unmount mirror data", e); 2399 } 2400 // 调用 mVold 的 unmount 方法,传入卷的 ID,用于卸载卷 2401 mVold.unmount(vol.id); 2402 // 调用 mStorageSessionController 的 onVolumeUnmount 方法,传入卷的信息,用于处理卸载后的逻辑 2403 mStorageSessionController.onVolumeUnmount(vol); 2404 } catch (Exception e) { 2405 // 如果发生其他异常,记录错误日志 2406 Slog.wtf(TAG, e); 2407 } 2408 }
getVolumes():
// 这个方法是 StorageManagerService 类的一个公开方法,用于获取所有已挂载的外部存储设备卷的列表 4001 @Override 4002 public VolumeInfo[] getVolumes(int flags) { 4003 // 同步锁定 mLock 对象,用于保证线程安全 4004 synchronized (mLock) { 4005 // 创建一个 VolumeInfo 类型的数组,大小为 mVolumes 集合的大小 4006 final VolumeInfo[] res = new VolumeInfo[mVolumes.size()]; 4007 // 遍历 mVolumes 集合,将每个元素复制到数组中 4008 for (int i = 0; i < mVolumes.size(); i++) { 4009 res[i] = mVolumes.valueAt(i); 4010 } 4011 // 返回数组 4012 return res; 4013 } 4014 }
getAllocatableBytes(String volumeUuid, int flags, String callingPackage):
// 这个方法是 StorageManagerService 类的一个公开方法,用于获取指定 UUID 的外部存储设备卷上可分配给应用程序的字节数 4082 @Override 4083 public long getAllocatableBytes(String volumeUuid, int flags, String callingPackage) { 4084 // 调整分配标志,根据调用者的 UID 和包名,判断是否需要添加 FLAG_ALLOCATE_AGGRESSIVE 标志 4085 flags = adjustAllocateFlags(flags, Binder.getCallingUid(), callingPackage); 4086 4087 // 获取 StorageManager 和 StorageStatsManager 的实例,用于操作存储相关的数据 4088 final StorageManager storage = mContext.getSystemService(StorageManager.class); 4089 final StorageStatsManager stats = mContext.getSystemService(StorageStatsManager.class); 4090 // 保存当前的调用标识,并清除调用者的身份,用于跨进程调用 4091 final long token = Binder.clearCallingIdentity(); 4092 try { 4093 // 一般情况下,应用程序可以分配尽可能多的空间,除非超过了最小缓存空间或低磁盘警告空间的限制。为了避免用户混淆,这个逻辑应该和 getFreeBytes() 方法保持一致 4094 // 根据卷的 UUID 找到对应的文件路径 4095 final File path = storage.findPathForUuid(volumeUuid); 4096 4097 // 初始化可用空间、低保留空间、满保留空间和可清除缓存空间为 0 4098 long usable = 0; 4099 long lowReserved = 0; 4100 long fullReserved = 0; 4101 long cacheClearable = 0; 4102 4103 // 如果没有设置 FLAG_ALLOCATE_CACHE_ONLY 标志,表示可以分配非缓存空间 4104 if ((flags & StorageManager.FLAG_ALLOCATE_CACHE_ONLY) == 0) { 4105 // 获取文件路径上的可用空间大小 4106 usable = path.getUsableSpace(); 4107 // 获取文件路径上的低磁盘警告空间大小 4108 lowReserved = storage.getStorageLowBytes(path); 4109 // 获取文件路径上的磁盘满空间大小 4110 fullReserved = storage.getStorageFullBytes(path); 4111 } 4112 4113 // 如果没有设置 FLAG_ALLOCATE_NON_CACHE_ONLY 标志,并且卷支持配额功能,表示可以分配缓存空间 4114 if ((flags & StorageManager.FLAG_ALLOCATE_NON_CACHE_ONLY) == 0 4115 && stats.isQuotaSupported(volumeUuid)) { 4116 // 获取卷上的缓存总大小 4117 final long cacheTotal = stats.getCacheBytes(volumeUuid); 4118 // 获取卷上的缓存保留大小 4119 final long cacheReserved = storage.getStorageCacheBytes(path, flags); 4120 // 计算卷上的可清除缓存大小,即缓存总大小减去缓存保留大小,如果为负数,则取 0 4121 cacheClearable = Math.max(0, cacheTotal - cacheReserved); 4122 } 4123 4124 // 如果设置了 FLAG_ALLOCATE_AGGRESSIVE 标志,表示可以分配更多的空间,但可能会影响系统性能或稳定性 4125 if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) { 4126 // 返回可用空间加上可清除缓存空间再减去磁盘满空间大小,如果为负数,则取 0 4127 return Math.max(0, (usable + cacheClearable) - fullReserved); 4128 } else { 4129 // 否则,返回可用空间加上可清除缓存空间再减去低磁盘警告空间大小,如果为负数,则取 0 4130 return Math.max(0, (usable + cacheClearable) - lowReserved); 4131 } 4132 } catch (IOException e) { 4133 // 如果发生 IOException 异常,将其封装成一个 ParcelableException 对象,并抛出 4134 throw new ParcelableException(e); 4135 } finally { 4136 // 无论成功或失败,都要恢复之前的调用标识 4137 Binder.restoreCallingIdentity(token); 4138 } 4139 }
allocateBytes(UUID storageUuid, long bytes):
// 这个方法是 StorageManagerService 类的一个公开方法,用于在指定 UUID 的外部存储设备卷上为应用程序分配指定字节数的空间 4126 @Override 4127 public void allocateBytes(String volumeUuid, long bytes, int flags, String callingPackage) { 4128 // 调整分配标志,根据调用者的 UID 和包名,判断是否需要添加 FLAG_ALLOCATE_AGGRESSIVE 标志 4129 flags = adjustAllocateFlags(flags, Binder.getCallingUid(), callingPackage); 4130 4131 // 获取卷上可分配给应用程序的非缓存空间大小 4132 final long allocatableBytes = getAllocatableBytes(volumeUuid, 4133 flags | StorageManager.FLAG_ALLOCATE_NON_CACHE_ONLY, callingPackage); 4134 // 如果要分配的字节数大于可分配的非缓存空间大小,表示空间不足 4135 if (bytes > allocatableBytes) { 4136 // 如果空间不足,检查卷上可分配给应用程序的缓存空间大小 4137 final long cacheClearable = getAllocatableBytes(volumeUuid, 4138 flags | StorageManager.FLAG_ALLOCATE_CACHE_ONLY, callingPackage); 4139 // 如果要分配的字节数大于可分配的非缓存空间大小加上可分配的缓存空间大小,表示即使清除缓存也无法满足需求,抛出 IOException 异常 4140 if (bytes > allocatableBytes + cacheClearable) { 4141 throw new ParcelableException(new IOException("Failed to allocate " + bytes 4142 + " because only " + (allocatableBytes + cacheClearable) + " allocatable")); 4143 } 4144 } 4145 4146 // 获取 StorageManager 的实例,用于操作存储相关的数据 4147 final StorageManager storage = mContext.getSystemService(StorageManager.class); 4148 // 保存当前的调用标识,并清除调用者的身份,用于跨进程调用 4149 final long token = Binder.clearCallingIdentity(); 4150 try { 4151 // 为了满足分配需求和低磁盘警告空间的限制,释放足够的磁盘空间 4152 final File path = storage.findPathForUuid(volumeUuid); 4153 // 如果设置了 FLAG_ALLOCATE_AGGRESSIVE 标志,表示可以分配更多的空间,但可能会影响系统性能或稳定性,需要加上磁盘满空间大小 4154 if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) { 4155 bytes += storage.getStorageFullBytes(path); 4156 } else { 4157 // 否则,只需要加上低磁盘警告空间大小 4158 bytes += storage.getStorageLowBytes(path); 4159 } 4160 4161 // 调用 mPmInternal 的 freeStorage 方法,传入卷的 UUID、要释放的字节数和分配标志,用于清理不必要的数据 4162 mPmInternal.freeStorage(volumeUuid, bytes, flags); 4163 } catch (IOException e) { 4164 // 如果发生 IOException 异常,将其封装成一个 ParcelableException 对象,并抛出 4165 throw new ParcelableException(e); 4166 } finally { 4167 // 无论成功或失败,都要恢复之前的调用标识 4168 Binder.restoreCallingIdentity(token); 4169 } 4170 }
VolumeInfo.java
http://aospxref.com/android-13.0.0_r3/xref/frameworks/base/core/java/android/os/storage/VolumeInfo.java
:这个类表示一个外部存储设备的卷信息,包括卷 ID、类型、状态、路径等属性。它是一个 Parcelable 对象,可以通过 Intent 或者 Bundle 传递。它提供了一些关键的属性和方法,如:
isMountedReadable():
// 这判断该卷是否已挂载且可读取。 @UnsupportedAppUsage public boolean isMountedReadable() { // 这个方法检查存储卷的状态是否是已挂载或只读挂载 return state == STATE_MOUNTED || state == STATE_MOUNTED_READ_ONLY; }
isMountedWritable():
//判断该卷是否已挂载且可写入。 @UnsupportedAppUsage public boolean isMountedWritable() { // 这个方法检查存储卷的状态是否是已挂载 return state == STATE_MOUNTED; }
FileUtils.java
http://aospxref.com/android-13.0.0_r3/xref/frameworks/base/core/java/android/os/FileUtils.java
:这个类提供了一些文件操作的工具方法,设置文件权限等。,不需要创建实例。它提供了一些关键的方法,如:
setPermissions(File path, int mode, int uid, int gid)
151 @UnsupportedAppUsage 152 public static int setPermissions(File path, int mode, int uid, int gid) { 153 // 调用另一个重载的方法,传入文件的绝对路径作为参数 153 return setPermissions(path.getAbsolutePath(), mode, uid, gid); 154 } 155 156 /** 157 * 设置给定路径的所有者和模式。 158 * 159 * @param mode 通过 {@code chmod} 应用的模式 160 * @param uid 通过 {@code chown} 应用的用户ID,或 -1 表示不改变 161 * @param gid 通过 {@code chown} 应用的组ID,或 -1 表示不改变 162 * @return 成功时返回 0,否则返回错误码。 163 * @hide 164 */ 165 @UnsupportedAppUsage 166 public static int setPermissions(String path, int mode, int uid, int gid) { 167 try { 168 // 使用 Os 类的 chmod 方法修改文件的权限 168 Os.chmod(path, mode); 169 } catch (ErrnoException e) { 170 // 如果出现异常,打印警告信息,并返回错误码 170 Slog.w(TAG, "Failed to chmod(" + path + "): " + e); 171 return e.errno; 172 } 173 174 // 如果 uid 或 gid 不为 -1,表示需要修改文件的所有者 174 if (uid >= 0 || gid >= 0) { 175 try { 176 // 使用 Os 类的 chown 方法修改文件的所有者 176 Os.chown(path, uid, gid); 177 } catch (ErrnoException e) { 178 // 如果出现异常,打印警告信息,并返回错误码 178 Slog.w(TAG, "Failed to chown(" + path + "): " + e); 179 return e.errno; 180 } 181 } 182 183 // 如果没有异常,返回 0 表示成功 183 return 0; 184 } 185 186 /** 187 * 设置给定 {@link FileDescriptor} 的所有者和模式。 188 * 189 * @param mode 通过 {@code chmod} 应用的模式 190 * @param uid 通过 {@code chown} 应用的用户ID,或 -1 表示不改变 191 * @param gid 通过 {@code chown} 应用的组ID,或 -1 表示不改变 192 * @return 成功时返回 0,否则返回错误码。 193 * @hide 194 */ 195 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 196 public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) { 197 try { 198 // 使用 Os 类的 fchmod 方法修改文件描述符的权限 198 Os.fchmod(fd, mode); 199 } catch (ErrnoException e) { 200 // 如果出现异常,打印警告信息,并返回错误码 200 Slog.w(TAG, "Failed to fchmod(): " + e); 201 return e.errno; 202 } 203 204 // 如果 uid 或 gid 不为 -1,表示需要修改文件描述符的所有者 204 if (uid >= 0 || gid >= 0) { 205 try { 206 // 使用 Os 类的 fchown 方法修改文件描述符的所有者 206 Os.fchown(fd, uid, gid); 207 } catch (ErrnoException e) { 208 // 如果出现异常,打印警告信息,并返回错误码 208 Slog.w(TAG, "Failed to fchown(): " + e); 209 return e.errno; 210 } 211 } 212 213 // 如果没有异常,返回 0 表示成功 213 return 0; 214 }
SELinux.java
http://aospxref.com/android-13.0.0_r3/xref/frameworks/base/core/java/android/os/SELinux.java
:这个类提供了一些 SELinux 相关的方法,包括检查文件是否有 SELinux 上下文标签等。SELinux 是一种安全增强型 Linux 系统,它可以对文件和进程进行更细致的访问控制。它是一个静态类,不需要创建实例。它提供了一些关键的方法,如:
- isSELinuxEnabled():判断当前系统是否启用了 SELinux 机制。
- getFileContext(String path):获取指定路径的文件的 SELinux 上下文标签。
- setFileContext(String path, String context):设置指定路径的文件的 SELinux 上下文标签。
51 /** 52 * 判断 SELinux 是否被禁用或启用。 53 * @return 表示 SELinux 是否启用。 54 */ 55 @UnsupportedAppUsage 56 // 声明一个本地方法,使用 native 关键字,不需要提供方法体 56 public static final native boolean isSELinuxEnabled(); 72 /** 73 * 改变已存在的文件对象的安全上下文。 74 * @param path 表示要重新标记的文件对象的路径。 75 * @param context 新的安全上下文,以字符串形式给出。 76 * @return 表示操作是否成功。 77 */ 78 // 声明一个本地方法,使用 native 关键字,不需要提供方法体 78 public static final native boolean setFileContext(String path, String context); 80 /** 81 * 获取文件对象的安全上下文。 82 * @param path 文件对象的路径名。 83 * @return 表示安全上下文。 84 */ 85 @UnsupportedAppUsage 86 // 声明一个本地方法,使用 native 关键字,不需要提供方法体 86 public static final native String getFileContext(String path); 95 /** 96 * 获取文件描述符对应文件的安全上下文。 97 * @param fd 文件的文件描述符。 98 * @return 表示文件描述符的安全上下文。 99 */ 100 // 声明一个本地方法,使用 native 关键字,不需要提供方法体 100 public static final native String getFileContext(FileDescriptor fd);
文件与目录的访问权限
判断系统的某个目录,某个文件可以被哪些UID或者组访问,有以下几种方法:
- 使用
ls -l
命令查看文件或目录的权限和所有者。例如:
- 如果一个文件的权限是
-rw-r--r--
,表示该文件可以被其所有者(第一个rw
)读写,可以被其所属组(第二个r
)读取,可以被其他用户(第三个r
)读取。 - 如果一个目录的权限是
drwxr-x---
,表示该目录可以被其所有者(第一个rwx
)读写执行,可以被其所属组(第二个rx
)读取执行,不能被其他用户(第三个-
)访问。
- 使用
stat
命令查看文件或目录的详细信息。例如:
- 如果一个文件的信息是
Access: (0644/-rw-r--r--) Uid: ( 1000/ system) Gid: ( 0/ root)
,表示该文件的权限是0644
,可以用八进制数表示为-rw-r--r--
,该文件的所有者是 UID 为0
的用户root
,该文件的所属组是 GID 为0
的组root
。
- 使用
find
命令根据用户的属主、属组、UID、GID等条件查找文件或目录。例如:
- 如果要查找
/data/data
目录下属于 UID 为1000
的用户的所有文件,可以使用find /data/data -uid 1000
命令。
# 查看 /etc/passwd文件的权限和所有者 rk3568_t:/etc # ls -ll passwd -rw-r--r-- 1 root root 0 2023-07-31 06:49:44.000000000 +0000 passwd # 查看 /etc/passwd 文件的详细信息 rk3568_t:/etc # stat passwd File: passwd Size: 0 Blocks: 0 IO Blocks: 512 regular empty file Device: fd00h/64768d Inode: 1782 Links: 1 Device type: 0,0 Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2023-07-31 06:49:44.000000000 +0000 Modify: 2023-07-31 06:49:44.000000000 +0000 Change: 2023-07-31 06:49:44.000000000 +0000 # 查找 /data/data 目录下属于 system 用户或者 system 组的所有目录 rk3568_t:/data/data # find . -user system -o -group system -type d . ./com.android.dynsystem ./com.android.dynsystem/cache ./com.android.dynsystem/code_cache
Android 11、12 和 13 的文件访问权限的变化和适配方法
从 Android 11 开始,Google 对应用访问外部存储设备的权限做了一些限制和变化,以保护用户的隐私和数据安全。这些限制和变化可能会影响到一些需要读写文件的应用的功能和兼容性。需要了解这些权限的含义和用法,并根据自己的应用需求进行相应的适配。下表列出了三种与外部存储设备相关的权限,以及它们的说明、级别和适用版本。
权限名称 | 权限说明 | 权限级别 | 适用版本 |
android.permission.READ_EXTERNAL_STORAGE | 允许应用从外部存储设备(如 SD 卡或 U 盘)读取文件,包括图片、音频、视频等媒体文件。 | 危险权限,需要在运行时向用户请求授权。 | Android 4.1(API 级别 16)及以上。 |
android.permission.WRITE_EXTERNAL_STORAGE | 允许应用向外部存储设备(如 SD 卡或 U 盘)写入文件,包括图片、音频、视频等媒体文件。该权限也包含了 READ_EXTERNAL_STORAGE 权限的功能。 | 危险权限,需要在运行时向用户请求授权。 | Android 4.1(API 级别 16)及以上。 |
android.permission.MANAGE_EXTERNAL_STORAGE | 允许应用访问和修改外部存储设备上的所有文件,包括系统和其他应用的文件 。该权限也包含了 WRITE_EXTERNAL_STORAGE 权限的功能 。 | 特殊权限,需要在系统设置中向用户请求授权 。 | Android 11(API 级别 30)及以上 。 |
下面是对这些权限的变化和适配方法的简要介绍:
- 在 Android 10 及以下版本中,如果应用声明了 WRITE_EXTERNAL_STORAGE 权限,并且获得了用户的授权,那么它就可以访问外部存储设备上的所有文件,包括公共目录(如 Pictures、Music 等)和其他应用的私有目录(如 Android/data 和 Android/obb 等)。这种模式被称为“传统存储”模式。
- 在 Android 11 中,Google 引入了一种新的模式,称为“分区存储”模式。在这种模式下,应用只能访问自己的私有目录和公共目录中属于自己类型(如图片、音频、视频等)的媒体文件。如果应用想要访问其他类型或其他应用的文件,就需要声明 MANAGE_EXTERNAL_STORAGE 权限,并且引导用户在系统设置中授予“所有文件访问权限”。这种权限是一种特殊权限,需要有充分的理由才能使用,否则可能会被 Google Play 拒绝上架 。另外,应用也可以使用 MediaStore API 或系统文件选择器 SAF 来访问或共享媒体文件或非媒体文件 ,而不需要申请额外的权限。
- 在 Android 12 中,Google 对分区存储模式做了一些改进和优化。例如,增加了一些 MediaStore API 的方法和常量 ,以便应用更方便地查询、插入、更新或删除媒体文件;增加了一种新的运行时权限 POST_NOTIFICATIONS ,以便用户更好地控制哪些应用可以发送通知;增加了一种新的机制 前台服务 (FGS) 任务管理器 ,以便用户更好地管理哪些应用可以在后台运行前台服务等。
- 在 Android 13 中,Google 继续对分区存储模式进行了一些调整和完善。例如,增加了一种新的 API 来确定音频的路由方式 ,以便应用更好地适配不同的音频设备;增加了一种新的权限 BODY_SENSORS_BACKGROUND ,以便用户更好地控制哪些应用可以在后台访问身体传感器信息;增加了一种新的 API 来设置或获取用户在每个应用中的首选语言 ,以便应用更好地适配不同的语言环境等。
从 Android 11 开始,应用访问外部存储设备的权限变得更加严格和细化,需要根据自己的应用需求和功能,选择合适的权限和 API 来进行适配。