在Android使用fuse文件系统开始,Android针对外置存储支持了独立的沙箱存储空间, 一般通过Context.getExternalFilesDir() Api获取,该空间内的数据为应用独有,并且不需要申请任何权限即可使用。但是当时并没有限制应用读写非沙箱内的数据。但是从Android Q开始,出于数据隐私问题,Android 希望禁止应用程序操作非沙箱内的数据。 但是为了过度,Android提供了requestLegacyExternalStorage机制,来帮助应用使用原来的机制继续读写存储卡。 关于Android 存储机制的说明请参考Android官方文档 存储。 新版本的适配可以参考Android Q分区存储权限变更及适配这篇文章。本文意在说明Android 系统如何实现requestLegacyExternalStorage机制,所以这里简单说明下,在Android Q上,是不希望应用继续访问非沙盒路径的,沙盒路径一般为外置存储下的/storage/emulated/${userid}/Android/${dir}/${package}/, 这里通过包名来划分路径,应用可以不需要权限直接访问沙盒路径下的数据。非沙盒路径为外置存储下的其他路径,不允许访问。 为了过度,Android提供了以下策略:
Android应用程序即使获取了读写存储卡权限也不能读写非沙盒路径下的数据。除非
Android应用程序获得读写存储卡权限的情况下,必须在AndroidManifest.xml的application标签下声明requestLegacyExternalStorage=true,才可以访问沙盒路径下的数据。
targetSdkVersion<29 的应用程序默认带有requestLegacyExternalStorage=true属性。
读过Android存储系统-外置存储权权限管理实现 这篇文章的应该知道,Android在启动的时候根据应用有没有获取读写外置存储权限,来给应用绑定到不同的fuse目录,应用获取对应外置存储读写权限。 在应用启动之初会调用MountService的getExternalStorageMountMode方法来设置对应权限。 在Android 10中MountService已经不负存在,对应逻辑被移动到了StorageManagerService中。
private int getMountModeInternal(int uid, String packageName) {
try {
// Get some easy cases out of the way first
if (Process.isIsolated(uid)) { // 隔离进程不赋予读写存储卡权限
return Zygote.MOUNT_EXTERNAL_NONE;
}
final String[] packagesForUid = mIPackageManager.getPackagesForUid(uid);
if (ArrayUtils.isEmpty(packagesForUid)) {
// It's possible the package got uninstalled already, so just ignore.
return Zygote.MOUNT_EXTERNAL_NONE;
}
if (packageName == null) {
packageName = packagesForUid[0];
}
if (mPmInternal.isInstantApp(packageName, UserHandle.getUserId(uid))) { // instant app不授予读写权限
return Zygote.MOUNT_EXTERNAL_NONE;
}
// Determine if caller is holding runtime permission
final boolean hasRead = StorageManager.checkPermissionAndCheckOp(mContext, false, 0,
uid, packageName, READ_EXTERNAL_STORAGE, OP_READ_EXTERNAL_STORAGE); // 获取了读权限和appops的OP_READ_EXTERNAL_STORAGE操作权限
final boolean hasWrite = StorageManager.checkPermissionAndCheckOp(mContext, false, 0,
uid, packageName, WRITE_EXTERNAL_STORAGE, OP_WRITE_EXTERNAL_STORAGE); // 获取了写权限和appops 的OP_WRITE_EXTERNAL_STORAGE权限
// We're only willing to give out broad access if they also hold
// runtime permission; this is a firm CDD requirement
// 获取了WRITE_MEDIA_STORAGE 权限, 该权限为系统签名权限,一般应用无法获取
final boolean hasFull = mIPackageManager.checkUidPermission(WRITE_MEDIA_STORAGE,
uid) == PERMISSION_GRANTED;
if (hasFull && hasWrite) { // 1 有WRITE_MEDIA_STORAGE 和写存储卡权限在授予所有读写存储卡能力。
return Zygote.MOUNT_EXTERNAL_FULL;
}
// We're only willing to give out installer access if they also hold
// runtime permission; this is a firm CDD requirement
final boolean hasInstall = mIPackageManager.checkUidPermission(INSTALL_PACKAGES,
uid) == PERMISSION_GRANTED;
boolean hasInstallOp = false;
// OP_REQUEST_INSTALL_PACKAGES is granted/denied per package but vold can't
// update mountpoints of a specific package. So, check the appop for all packages
// sharing the uid and allow same level of storage access for all packages even if
// one of the packages has the appop granted.
for (String uidPackageName : packagesForUid) {
if (mIAppOpsService.checkOperation(
OP_REQUEST_INSTALL_PACKAGES, uid, uidPackageName) == MODE_ALLOWED) {
hasInstallOp = true;
break;
}
}
if ((hasInstall || hasInstallOp) && hasWrite) { // 2 允许安装应用并且包含写权限则允许读写存储卡(installer是非常特殊的)
return Zygote.MOUNT_EXTERNAL_WRITE;
}
// Otherwise we're willing to give out sandboxed or non-sandboxed if
// they hold the runtime permission
// Appops允许OP_LEGACY_STORAGE操作(应用设置了requestLegacyExternalStorage)
final boolean hasLegacy = mIAppOpsService.checkOperation(OP_LEGACY_STORAGE,
uid, packageName) == MODE_ALLOWED;
if (hasLegacy && hasWrite) { // 3 必须 获取写权限和 requestLegacyExternalStorage标签才允许写存储卡
return Zygote.MOUNT_EXTERNAL_WRITE;
} else if (hasLegacy && hasRead) {// 4 必须 获取读权限和 requestLegacyExternalStorage标签才允许读存储卡
return Zygote.MOUNT_EXTERNAL_READ;
} else {
return Zygote.MOUNT_EXTERNAL_DEFAULT;
}
} catch (RemoteException e) {
// Should not happen
}
return Zygote.MOUNT_EXTERNAL_NONE;
}
上面列举了四种情况获取读写权限,分别检查了 Appos 的操作 OP_READ_EXTERNAL_STORAGE, OP_WRITE_EXTERNAL_STORAGE , OP_REQUEST_INSTALL_PACKAGES 和OP_LEGACY_STORAGE, 以及 权限READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE 和INSTALL_PACKAGES。
在AppOps中 OP_READ_EXTERNAL_STORAGE 和 OP_WRITE_EXTERNAL_STORAGE 默认都是允许的,OP_LEGACY_STORAGE 在应用声明了 requestLegacyExternalStorage 标签的时候是允许的。 OP_REQUEST_INSTALL_PACKAGES 需要单独授予。 WRITE_MEDIA_STORAGE权限是系统签名级别的保护权限,获取该权限的程序是非常可信的。
所以当获取WRITE_MEDIA_STORAGE权限的应用程序又获取了读写存储卡权限会直接授予。 installer程序是另外一个特例,因为在Android 10 中加入了受限制的权限。关于受限制的权限请参考android官方文档 运行时权限 。 受限权限要在安装之初添加到白名单,否则应用可能无法获取, 读写存储卡权限正是受限权限。
除了上述特例外应用要想获取读写外置存储的能力,必须通过AppOps的检查, OP_LEGACY_STORAGE操作必须被允许, 而允许OP_LEGACY_STORAGE的操作代表应用程序声明了 requestLegacyExternalStorage标签(或者targetSdk < 29 默认获得requestLegacyExternalStorage属性)。
我们先验证targetSdk < 29会获得 requestLegacyExternalStorage属性。
frameworks/base/core/java/android/content/pm/PackageParser.java
/**
* Parse the {@code application} XML tree at the current parse location in a
* <em>base APK</em> manifest.
* <p>
* When adding new features, carefully consider if they should also be
* supported by split APKs.
*/
@UnsupportedAppUsage
private boolean parseBaseApplication(Package owner, Resources res,
XmlResourceParser parser, int flags, String[] outError)
......
if (sa.getBoolean(
R.styleable.AndroidManifestApplication_requestLegacyExternalStorage,
owner.applicationInfo.targetSdkVersion < Build.VERSION_CODES.Q)) {
ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE;
}
......
}
注意这里默认值的设置,owner.applicationInfo.targetSdkVersion < Build.VERSION_CODES.Q的时候 sa.getBoolean(
R.styleable.AndroidManifestApplication_requestLegacyExternalStorage) 默认为true。
再来看下Android如何根据ApplicationInfo.PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE标志设置AppOps
frameworks/base/services/core/java/com/android/server/policy/SoftRestrictedPermissionPolicy.java
/**
* Get the policy for a soft restricted permission.
*
* @param context A context to use
* @param appInfo The application the permission belongs to. Can be {@code null}, but then
* only {@link #resolveAppOp} will work.
* @param user The user the app belongs to. Can be {@code null}, but then only
* {@link #resolveAppOp} will work.
* @param permission The name of the permission
*
* @return The policy for this permission
*/
public static @NonNull SoftRestrictedPermissionPolicy forPermission(@NonNull Context context,
@Nullable ApplicationInfo appInfo, @Nullable UserHandle user,
@NonNull String permission) {
switch (permission) {
// Storage uses a special app op to decide the mount state and supports soft restriction
// where the restricted state allows the permission but only for accessing the medial
// collections.
case READ_EXTERNAL_STORAGE: {
final int flags;
final boolean applyRestriction;
final boolean isWhiteListed;
final boolean hasRequestedLegacyExternalStorage;
final int targetSDK;
if (appInfo != null) {
PackageManager pm = context.getPackageManager();
flags = pm.getPermissionFlags(permission, appInfo.packageName, user);
applyRestriction = (flags & FLAG_PERMISSION_APPLY_RESTRICTION) != 0;
isWhiteListed = (flags & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0;
targetSDK = getMinimumTargetSDK(context, appInfo, user);
boolean hasAnyRequestedLegacyExternalStorage =
appInfo.hasRequestedLegacyExternalStorage();
// hasRequestedLegacyExternalStorage is per package. To make sure two apps in
// the same shared UID do not fight over what to set, always compute the
// combined hasRequestedLegacyExternalStorage
String[] uidPkgs = pm.getPackagesForUid(appInfo.uid);
if (uidPkgs != null) {
for (String uidPkg : uidPkgs) {
if (!uidPkg.equals(appInfo.packageName)) {
ApplicationInfo uidPkgInfo;
try {
uidPkgInfo = pm.getApplicationInfoAsUser(uidPkg, 0, user);
} catch (PackageManager.NameNotFoundException e) {
continue;
}
hasAnyRequestedLegacyExternalStorage |=
uidPkgInfo.hasRequestedLegacyExternalStorage();
}
}
}
hasRequestedLegacyExternalStorage = hasAnyRequestedLegacyExternalStorage;
} else {
flags = 0;
applyRestriction = false;
isWhiteListed = false;
hasRequestedLegacyExternalStorage = false;
targetSDK = 0;
}
return new SoftRestrictedPermissionPolicy() {
@Override
public int resolveAppOp() {
return OP_LEGACY_STORAGE;
}
@Override
public int getDesiredOpMode() {
if (applyRestriction) {
return MODE_DEFAULT;
} else if (hasRequestedLegacyExternalStorage) {
return MODE_ALLOWED;
} else {
return MODE_IGNORED;
}
}
@Override
public boolean shouldSetAppOpIfNotDefault() {
// Do not switch from allowed -> ignored as this would mean to retroactively
// turn on isolated storage. This will make the app loose all its files.
return getDesiredOpMode() != MODE_IGNORED;
}
@Override
public boolean canBeGranted() {
if (isWhiteListed || targetSDK >= Build.VERSION_CODES.Q) {
return true;
} else {
return false;
}
}
};
}
case WRITE_EXTERNAL_STORAGE: {
final boolean isWhiteListed;
final int targetSDK;
if (appInfo != null) {
final int flags = context.getPackageManager().getPermissionFlags(permission,
appInfo.packageName, user);
isWhiteListed = (flags & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0;
targetSDK = getMinimumTargetSDK(context, appInfo, user);
} else {
isWhiteListed = false;
targetSDK = 0;
}
return new SoftRestrictedPermissionPolicy() {
@Override
public int resolveAppOp() {
return OP_NONE;
}
@Override
public int getDesiredOpMode() {
return MODE_DEFAULT;
}
@Override
public boolean shouldSetAppOpIfNotDefault() {
return false;
}
@Override
public boolean canBeGranted() {
return isWhiteListed || targetSDK >= Build.VERSION_CODES.Q;
}
};
}
default:
return DUMMY_POLICY;
}
}
SoftRestrictedPermissionPolicy对于读存储卡权限,如果hasRequestedLegacyExternalStorage条件成立,则返回AppOpsManager.MODE_ALLOWED 标签,也就是该op被允许。 SoftRestrictedPermissionPolicy.getDesiredOpMode()函数的返回值最终会被frameworks/base/services/core/java/com/android/server/policy/PermissionPolicyService.java 的addOpIfRestricted() 函数同步给AppOpsService。