Android系统 文件访问权限笔记

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Android系统 文件访问权限笔记

文件访问权限是 Android 系统中一个非常复杂且重要的内容,它决定了应用程序能够访问和操作哪些文件和目录。Android 系统对文件访问权限有严格的限制和管理。随着 Android 系统版本的不断更新,文件访问权限也有一些变化和变更,需要我们及时了解和学习 以便应对需求。这块内容非常复杂庞大且内容很杂 。。本文简单学习 Android 系统的文件访问权限的概念、分类、申请方法、framework层源码位置和关键函数,以及 Android 11、12 和 13 的文件访问权限的变化和适配方法。

系列文章:

Android系统 文件访问权限笔记

Android系统 理解/sys/目录权限和UID和GID?

Android系统 应用存储路径与权限

Android系统 自定义系统和应用权限

Android系统 AppOps默认授予应用相应的权限

Android系统 权限组管理和兼容性

参考学习:

Google Android 数据和文件存储概览

Android 11 system_server 读写 SDCARD

解决Android10读取不到/sdcard/、/storage/emulated/0/文件的问题

论Android 9.0 外置sdcard 读写

Android 9.0中sdcard 的权限和挂载问题

Android系统 理解/sys/目录权限和UID和GID?

在安卓11/12/13下修改android/data和obb内容的经验记录

Android 13 功能和变更列表 | Android 开发者 | Android Developers

android 11 申请允许管理所有文件权限

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

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

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

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

  • 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 来进行适配。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
20天前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
45 15
Android 系统缓存扫描与清理方法分析
|
12天前
|
算法 JavaScript Android开发
|
14天前
|
安全 搜索推荐 Android开发
揭秘安卓与iOS系统的差异:技术深度对比
【10月更文挑战第27天】 本文深入探讨了安卓(Android)与iOS两大移动操作系统的技术特点和用户体验差异。通过对比两者的系统架构、应用生态、用户界面、安全性等方面,揭示了为何这两种系统能够在市场中各占一席之地,并为用户提供不同的选择。文章旨在为读者提供一个全面的视角,理解两种系统的优势与局限,从而更好地根据自己的需求做出选择。
36 2
|
22天前
|
安全 搜索推荐 Android开发
揭秘iOS与Android系统的差异:一场技术与哲学的较量
在当今数字化时代,智能手机操作系统的选择成为了用户个性化表达和技术偏好的重要标志。iOS和Android,作为市场上两大主流操作系统,它们之间的竞争不仅仅是技术的比拼,更是设计理念、用户体验和生态系统构建的全面较量。本文将深入探讨iOS与Android在系统架构、应用生态、用户界面及安全性等方面的本质区别,揭示这两种系统背后的哲学思想和市场策略,帮助读者更全面地理解两者的优劣,从而做出更适合自己的选择。
|
13天前
|
安全 搜索推荐 程序员
深入探索Android系统的碎片化问题及其解决方案
在移动操作系统的世界中,Android以其开放性和灵活性赢得了广泛的市场份额。然而,这种开放性也带来了一个众所周知的问题——系统碎片化。本文旨在探讨Android系统碎片化的现状、成因以及可能的解决方案,为开发者和用户提供一种全新的视角来理解这一现象。通过分析不同版本的Android系统分布、硬件多样性以及更新机制的影响,我们提出了一系列针对性的策略,旨在减少碎片化带来的影响,提升用户体验。
|
13天前
|
安全 Android开发 iOS开发
深入探索iOS与Android系统的差异性及优化策略
在当今数字化时代,移动操作系统的竞争尤为激烈,其中iOS和Android作为市场上的两大巨头,各自拥有庞大的用户基础和独特的技术特点。本文旨在通过对比分析iOS与Android的核心差异,探讨各自的优势与局限,并提出针对性的优化策略,以期为用户提供更优质的使用体验和为开发者提供有价值的参考。
|
15天前
|
安全 Android开发 iOS开发
安卓系统与iOS系统的比较####
【10月更文挑战第26天】 本文将深入探讨安卓(Android)和iOS这两大主流移动操作系统的各自特点、优势与不足。通过对比分析,帮助读者更好地理解两者在用户体验、应用生态、系统安全等方面的差异,从而为消费者在选择智能手机时提供参考依据。无论你是技术爱好者还是普通用户,这篇文章都将为你揭示两大系统背后的故事和技术细节。 ####
36 0
|
6月前
|
XML Android开发 数据安全/隐私保护
android 11后文件读写访问权限申请
android 11后文件读写访问权限申请
241 0
|
XML 缓存 API
Android 7.0之访问文件的权限和FileProvider类
转载请标明出处: http://blog.csdn.net/djy1992/article/details/72533310 本文出自:【奥特曼超人的博客】 权限更改 Android 7.0 做了一些权限更改,这些更改可能会影响您的应用。
3702 0
|
存储 Android开发
【Android 性能优化】应用启动优化 ( 方法追踪代码模板 | 示例项目 | SD 卡访问权限 | 示例代码 | 获取 Trace 文件 | Android Studio 查看文件)
【Android 性能优化】应用启动优化 ( 方法追踪代码模板 | 示例项目 | SD 卡访问权限 | 示例代码 | 获取 Trace 文件 | Android Studio 查看文件)
207 0
【Android 性能优化】应用启动优化 ( 方法追踪代码模板 | 示例项目 | SD 卡访问权限 | 示例代码 | 获取 Trace 文件 | Android Studio 查看文件)