Android系统 文件访问权限笔记

简介: 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日志并进行多维度分析。
相关文章
|
1天前
|
存储 数据库 Android开发
安卓Jetpack Compose+Kotlin,支持从本地添加音频文件到播放列表,支持删除,使用ExoPlayer播放音乐
为了在UI界面添加用于添加和删除本地音乐文件的按钮,以及相关的播放功能,你需要实现以下几个步骤: 1. **集成用户选择本地音乐**:允许用户从设备中选择音乐文件。 2. **创建UI按钮**:在界面中创建添加和删除按钮。 3. **数据库功能**:使用Room数据库来存储音频文件信息。 4. **更新ViewModel**:处理添加、删除和播放音频文件的逻辑。 5. **UI实现**:在UI层支持添加、删除音乐以及播放功能。
|
2天前
|
缓存 Android开发 Kotlin
【安卓app开发】kotlin Jetpack Compose框架 | 先用OKhttp下载远程音频文件再使用ExoPlayer播放
使用 Kotlin 的 Jetpack Compose 开发安卓应用时,可以结合 OkHttp 下载远程音频文件和 ExoPlayer 进行播放。在 `build.gradle` 添加相关依赖后,示例代码展示了如何下载音频并用 ExoPlayer 播放。代码包括添加依赖、下载文件、播放文件及简单的 Compose UI。注意,示例未包含完整错误处理和资源释放,实际应用需补充这些内容。
|
2天前
|
存储 Android开发 Kotlin
开发安卓app OKhttp下载后使用MediaPlayer播放
在Android Jetpack Compose应用程序中,要使用OkHttp下载远程音频文件并在本地播放,你需要完成以下几个步骤: 1. **添加依赖**:确保`build.gradle`文件包含OkHttp和Jetpack Compose的相关依赖。 2. **下载逻辑**:创建一个`suspend`函数,使用OkHttp发起网络请求下载音频文件到本地。 3. **播放逻辑**:利用`MediaPlayer`管理音频播放状态。 4. **Compose UI**:构建用户界面,包含下载和播放音频的按钮。
|
7天前
|
前端开发 Java API
Android系统中读写和显示图片
Android系统中读写和显示图片
7 0
|
8天前
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的安卓的微博客系统附带文章和源代码部署视频讲解等
基于ssm+vue.js+uniapp小程序的安卓的微博客系统附带文章和源代码部署视频讲解等
20 2
|
10天前
|
Java 机器人 Linux
01. 【Android教程】系统背景及结构概述
01. 【Android教程】系统背景及结构概述
8 0
|
15天前
|
运维 监控 Android开发
构建高效自动化运维系统的策略与实践构建高效Android应用:Kotlin协程的实践指南
【5月更文挑战第29天】随着信息技术的迅猛发展,企业IT基础设施变得日益复杂,传统的手动运维模式已难以满足高效率、高稳定性的要求。本文将深入探讨如何通过自动化工具和策略来构建一个高效的自动化运维系统。文中不仅分析了自动化运维的必要性,还详细介绍了实现过程中的关键步骤,包括监控、配置管理、故障响应等,并结合实际案例分析其效果,以期为读者提供一套行之有效的自动化运维解决方案。
|
15天前
|
人工智能 vr&ar Android开发
安卓与iOS系统的发展趋势及影响分析
在移动互联网时代,安卓和iOS作为两大主流移动操作系统,在不断发展变化中展现出不同的特点和发展趋势。本文从技术性角度出发,分析了安卓和iOS系统的发展趋势,并探讨了它们对移动设备市场和用户体验的影响,帮助读者更好地理解当前移动操作系统的发展方向和未来可能的变化。
17 0
|
17天前
|
存储 人工智能 安全
移动应用与系统:探索开发与操作系统的融合安卓应用开发:打造高效用户界面的关键技术
【5月更文挑战第27天】 随着移动互联网的飞速发展,移动应用和操作系统已经成为了我们日常生活中不可或缺的一部分。本文将深入探讨移动应用开发的关键要素,以及移动操作系统的核心功能。我们将分析移动应用开发的挑战和机遇,并讨论移动操作系统如何适应不断变化的技术环境。通过深入研究这些主题,我们希望为读者提供对移动应用和系统领域的全面理解。
|
17天前
|
存储 缓存 算法
深入理解操作系统内存管理:分页系统的优势与挑战构建高效Android应用:探究Kotlin协程的优势与实践
【5月更文挑战第27天】 在现代计算机系统中,内存管理是操作系统的核心功能之一。分页系统作为一种内存管理技术,通过将物理内存划分为固定大小的单元——页面,为每个运行的程序提供独立的虚拟地址空间。这种机制不仅提高了内存的使用效率,还为多任务环境提供了必要的隔离性。然而,分页系统的实现也带来了一系列的挑战,包括页面置换算法的选择、内存抖动问题以及TLB(Translation Lookaside Buffer)的管理等。本文旨在探讨分页系统的原理、优势及其面临的挑战,并通过分析现有解决方案,提出可能的改进措施。