在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在启动的时候根据应用有没有获取读写外置存储权限,来给应用绑定到不同的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
* base APK manifest.
*
* 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。