uses-permission 解析
本文面向已经读过 Manifest 解析入口、四大组件解析 和 PackageParser2 架构 的读者。前文解释了 Manifest 标签路由和 ParsingPackage 的创建;本文聚焦 uses-permission、uses-permission-sdk-m、uses-permission-sdk-23 三个标签,追踪权限声明如何经过属性读取、子标签、SDK/feature 条件和重复检查,最终写入 PackageImpl。
本文讨论的是“Manifest 权限请求数据如何被解析和保存”,不是运行时权限授予策略的完整教程。解析阶段产生 requested permission mapping 和 implicit permission 集合;PermissionManager 后续才根据保护级别、用户状态、安装来源和策略决定是否授予。Android 17 的 ParsedUsesPermission 已包含 purpose、general-purpose 和 purposeStringResource 字段,不能只按旧版 name/flags 模型理解。
读完后,读者应能从 ParsingPackageUtils.parseBaseApkTag() 进入 parseUsesPermission();解释 minSdk/maxSdk 和 required-feature/required-not-feature 如何让一个声明“成功解析但不加入包”;判断重复权限何时静默忽略、何时因 flags/purpose 冲突失败;还能沿 convertCompatPermissions() 和 convertSplitPermissions() 看懂隐式权限从何而来。
1. 权限数据流
1.1 三个阶段
源码文件:
frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.javaframeworks/base/core/java/com/android/internal/pm/pkg/component/ParsedUsesPermission.javaframeworks/base/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
| 阶段 | owner | 数据 | 消费者 |
|---|---|---|---|
| Manifest parse | ParsingPackageUtils | name、flags、purpose、feature 条件 | PackageImpl.addUsesPermission |
| 兼容转换 | ParsingPackageUtils | implicitPermissions | 权限查询/安装策略 |
| 安装后权限 | PermissionManager | requested permissions、用户 grant | ApplicationInfo/权限检查 |
解析成功只表示 parser 没有错误。SDK/feature 不满足时,方法也返回 success,但不调用 addUsesPermission();重复声明且字段一致时同样返回 success。只有结构错误或同名声明字段冲突才返回 ParseResult error。
1.2 标签变体
parseBaseApkTag() 把三个标签变体路由到同一个方法;后缀不再单独决定逻辑,当前资源 SDK、feature callback、flags 和子标签才决定最终结果。
2. 数据模型
2.1 权限接口
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/component/ParsedUsesPermission.java
public interface ParsedUsesPermission {
int FLAG_NEVER_FOR_LOCATION = PackageInfo.REQUESTED_PERMISSION_NEVER_FOR_LOCATION;
int FLAG_ONLY_FOR_LOCATION_BUTTON = PackageInfo.REQUESTED_PERMISSION_ONLY_FOR_LOCATION_BUTTON;
@UsesPermissionFlags
int getUsesPermissionFlags();
@NonNull String getName();
@NonNull Set<String> getPurposes();
@NonNull Set<String> getGeneralPurposes();
int getPurposeStringResource();
}Android 17 有 NEVER_FOR_LOCATION 和 ONLY_FOR_LOCATION_BUTTON 两个 permission flags,并保存 purpose、general-purpose 和 purpose string resource。feature flag 关闭时,相关字段通常为空或 0,但接口仍保留完整模型。
2.2 实现类
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/component/ParsedUsesPermissionImpl.java
@DataClass(genGetters = true, genSetters = true, genBuilder = false,
genParcelable = true, genAidl = false)
public class ParsedUsesPermissionImpl implements ParsedUsesPermission, Parcelable {
@DataClass.ParcelWith(Parcelling.BuiltIn.ForInternedString.class)
@NonNull private String name;
private int usesPermissionFlags;
private int purposeStringResource;
@DataClass.ParcelWith(Parcelling.BuiltIn.ForStringSet.class)
@NonNull private Set<String> purposes;
@DataClass.ParcelWith(Parcelling.BuiltIn.ForStringSet.class)
@NonNull private Set<String> generalPurposes;
public ParsedUsesPermissionImpl(@NonNull String name, int usesPermissionFlags,
int purposeStringResource, @NonNull Set<String> purposes,
@NonNull Set<String> generalPurposes) {
this.name = name;
this.usesPermissionFlags = usesPermissionFlags;
this.purposeStringResource = purposeStringResource;
this.purposes = purposes;
this.generalPurposes = generalPurposes;
}
}实现类可 Parcelable 化,权限名使用 interned string,purpose 集合使用 StringSet parcelling。Manifest 语义校验不在构造函数中完成,而由 parseUsesPermission 负责。
2.3 PackageImpl 存储
源码文件:frameworks/base/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
@NonNull
private Map<String, ParsedUsesPermission> usesPermissionMapping = emptyMap();
@NonNull
private Set<String> implicitPermissions = emptySet();
@Override
public PackageImpl addUsesPermission(ParsedUsesPermission permission) {
// Duplicate requests were resolved during parsing.
usesPermissionMapping = CollectionUtils.add(usesPermissionMapping,
permission.getName(), permission);
return this;
}
@Override
public PackageImpl addImplicitPermission(String permission) {
addUsesPermission(new ParsedUsesPermissionImpl(permission, 0, 0,
emptySet(), emptySet()));
implicitPermissions = CollectionUtils.add(implicitPermissions,
TextUtils.safeIntern(permission));
return this;
}
@Override
public Set<String> getRequestedPermissions() {
return usesPermissionMapping.keySet();
}权限名 map 让 effective requested permissions 天然去重;implicit permission 同时进入 mapping 和独立集合,消费者可以区分 Manifest 显式请求与兼容性转换结果。
3. 标签解析
3.1 属性读取
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
private ParseResult<ParsingPackage> parseUsesPermission(ParseInput input,
ParsingPackage pkg, Resources res, XmlResourceParser parser)
throws IOException, XmlPullParserException {
TypedArray sa = res.obtainAttributes(parser,
R.styleable.AndroidManifestUsesPermission);
try {
String name = sa.getNonResourceString(
R.styleable.AndroidManifestUsesPermission_name);
int minSdkVersion = parseMinOrMaxSdkVersion(sa,
R.styleable.AndroidManifestUsesPermission_minSdkVersion,
Integer.MIN_VALUE);
int maxSdkVersion = parseMinOrMaxSdkVersion(sa,
R.styleable.AndroidManifestUsesPermission_maxSdkVersion,
Integer.MAX_VALUE);
int purposeStringResource = 0;
final boolean isAllPurposeEnabled =
android.permission.flags.Flags.ppdManifestEnabled();
if (isAllPurposeEnabled) {
purposeStringResource = sa.getResourceId(
R.styleable.AndroidManifestUsesPermission_purposeString, 0);
}
final ArraySet<String> requiredFeatures = new ArraySet<>();
String feature = sa.getNonConfigurationString(
R.styleable.AndroidManifestUsesPermission_requiredFeature, 0);
if (feature != null) {
requiredFeatures.add(feature);
}
final ArraySet<String> requiredNotFeatures = new ArraySet<>();
feature = sa.getNonConfigurationString(
R.styleable.AndroidManifestUsesPermission_requiredNotFeature, 0);
if (feature != null) {
requiredNotFeatures.add(feature);
}
final int usesPermissionFlags = sa.getInt(
R.styleable.AndroidManifestUsesPermission_usesPermissionFlags, 0);权限名禁止资源引用;min/max 默认不限制;purpose string 只有 ppdManifestEnabled 打开时读取。required feature 先收集,等待子标签解析和 callback 判断。
3.2 子标签
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
final Set<String> purposes = new ArraySet<>();
final Set<String> generalPurposes = new ArraySet<>();
final boolean isPurposesEnabled =
android.permission.flags.Flags.ppdPurposeEnabled()
|| android.permission.flags.Flags.ppdInstallTimeEnabled();
final int outerDepth = parser.getDepth();
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
final ParseResult<?> result;
switch (parser.getName()) {
case "required-feature":
result = parseRequiredFeature(input, res, parser);
if (result.isSuccess()) requiredFeatures.add((String) result.getResult());
break;
case "required-not-feature":
result = parseRequiredNotFeature(input, res, parser);
if (result.isSuccess()) requiredNotFeatures.add((String) result.getResult());
break;
case "purpose":
result = isPurposesEnabled ? parsePurpose(input, res, parser)
: input.success(null);
if (result.isSuccess() && result.getResult() != null) {
purposes.add((String) result.getResult());
}
break;
case "general-purpose":
result = isAllPurposeEnabled ? parseGeneralPurpose(input, res, parser)
: input.success(null);
if (result.isSuccess() && result.getResult() != null) {
generalPurposes.add((String) result.getResult());
}
break;
default:
result = ParsingUtils.unknownTag("<uses-permission>", pkg, parser, input);
break;
}
if (result.isError()) return input.error(result);
}purpose/general-purpose 是条件功能:关闭时标签会被静默跳过;required-feature 名称缺失则返回解析错误;未知子标签由 unknownTag 决定 warning、skip 或 error。
3.3 SDK 与 feature
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
ParseResult<ParsingPackage> success = input.success(pkg);
if (name == null) return success;
if (Build.VERSION.RESOURCES_SDK_INT < minSdkVersion
|| Build.VERSION.RESOURCES_SDK_INT > maxSdkVersion) {
return success;
}
if (mCallback != null) {
for (int i = requiredFeatures.size() - 1; i >= 0; i--) {
if (!mCallback.hasFeature(requiredFeatures.valueAt(i))) return success;
}
for (int i = requiredNotFeatures.size() - 1; i >= 0; i--) {
if (mCallback.hasFeature(requiredNotFeatures.valueAt(i))) return success;
}
}name 缺失、SDK 不在窗口、required feature 缺失或 required-not-feature 存在,都会返回 success 但不加入 mapping。这是“声明对当前设备不适用”,不是 Manifest malformed。
3.4 重复检查
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
boolean found = false;
for (ParsedUsesPermission usesPermission :
pkg.getUsesPermissionMapping().values()) {
if (Objects.equals(usesPermission.getName(), name)) {
if (usesPermission.getUsesPermissionFlags() != usesPermissionFlags) {
return input.error("Conflicting uses-permissions flags: " + name);
} else if (isPurposesEnabled
&& !Objects.equals(usesPermission.getPurposes(), purposes)) {
return input.error("Conflicting uses-permissions purposes: " + name);
} else if (isAllPurposeEnabled
&& !Objects.equals(usesPermission.getGeneralPurposes(), generalPurposes)) {
return input.error("Conflicting uses-permissions general purposes: " + name);
} else if (isAllPurposeEnabled
&& usesPermission.getPurposeStringResource() != purposeStringResource) {
return input.error("Conflicting uses-permissions purpose string: " + name);
} else {
Slog.w(TAG, "Ignoring duplicate uses-permissions: " + name);
}
found = true;
break;
}
}
if (!found) {
pkg.addUsesPermission(new ParsedUsesPermissionImpl(name, usesPermissionFlags,
purposeStringResource, purposes, generalPurposes));
}
return success;
} finally {
sa.recycle();
}
}同名声明 flags 不同总是失败;purpose、general-purpose 和 purposeStringResource 只在对应 feature flag 打开时参与冲突比较;字段一致则 warning 后忽略。
4. 隐式权限
4.1 compat 转换
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
private static void convertCompatPermissions(ParsingPackage pkg) {
for (int i = 0, size = CompatibilityPermissionInfo.COMPAT_PERMS.length; i < size; i++) {
final CompatibilityPermissionInfo info = CompatibilityPermissionInfo.COMPAT_PERMS[i];
if (pkg.getTargetSdkVersion() >= info.getSdkVersion()) {
break;
}
if (!pkg.getRequestedPermissions().contains(info.getName())) {
pkg.addImplicitPermission(info.getName());
}
}
}兼容性转换按 target SDK 与 CompatibilityPermissionInfo 表比较;低于门槛且 Manifest 未请求的权限加入 implicitPermissions。它发生在 parseUsesPermission 之后,不是标签解析的副作用。
4.2 split 转换
源码文件:frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
private void convertSplitPermissions(ParsingPackage pkg) {
for (int is = 0; is < mSplitPermissionInfos.size(); is++) {
final PermissionManager.SplitPermissionInfo spi = mSplitPermissionInfos.get(is);
String featureFlag = spi.getFeatureFlag();
if (featureFlag != null && AconfigFlags.getInstance().skip(pkg, featureFlag,
spi.isFeatureFlagNegated())) {
continue;
}
Set<String> requestedPermissions = pkg.getRequestedPermissions();
if (pkg.getTargetSdkVersion() >= spi.getTargetSdk()
|| !requestedPermissions.contains(spi.getSplitPermission())) {
continue;
}
for (String perm : spi.getNewPermissions()) {
if (!requestedPermissions.contains(perm)) {
pkg.addImplicitPermission(perm);
}
}
}
}split permission 转换还受 AConfig flag、target SDK 和旧 split permission 是否已请求控制。每个 new permission 只有在 mapping 中不存在时才加入。
4.3 effective set
PackageImpl.getRequestedPermissions() 返回 usesPermissionMapping.keySet(),因此显式和隐式权限在查询时合并;getImplicitPermissions() 才能区分转换来源。PermissionManager 消费的是 effective set,而不是 Manifest 文本。
5. 消费路径
5.1 PackageInfo
源码文件:frameworks/base/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
PackageInfoUtils 依据 AndroidPackage.getRequestedPermissions() 生成 PackageInfo.requestedPermissions,并结合 ParsedUsesPermission 映射生成 requested permission flags。requestedPermissions 是 effective set,不能用来判断某权限是否直接出现在 Manifest。
5.2 权限消费
源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
if (pkgSetting.getPkg() != null) {
mPm.mPermissionManager.onPackageInstalled(pkgSetting.getPkg(),
Process.INVALID_UID /* previousAppId */,
permissionParamsBuilder.build(), userId);
}commit 后 PermissionManager 才取得已注册 package,依据 requested permissions、签名、用户和安装参数处理权限状态。parseUsesPermission 不直接授予 runtime permission。
5.3 cache 与 parcel
PackageImpl 会将 usesPermissionMapping 和 implicitPermissions 写入 Parcel;PackageCacher 反序列化时恢复它们。cache 命中后 parser 不重跑标签,但 PMS 仍可根据当前 feature、签名和权限策略继续处理。
6. 失败与验证
6.1 失败表
| 条件 | 阶段 | 结果 |
|---|---|---|
| name 缺失 | parseUsesPermission | success,不加入 |
| SDK 不在 min/max | parseUsesPermission | success,不加入 |
| required feature 不满足 | parseUsesPermission | success,不加入 |
| required-not-feature 存在 | parseUsesPermission | success,不加入 |
| child feature name 缺失 | child parser | ParseResult error |
| flags/purpose 冲突 | duplicate check | ParseResult error |
| unknown child | unknownTag | 按 parser 模式处理 |
| AConfig skip | child loop | 跳过标签 |
“成功但不加入”是设计行为,不是异常吞掉。权限缺失时先检查 SDK/feature/flag 条件,再检查 PermissionManager。
6.2 资源清理
parseUsesPermission、purpose、general-purpose、required-feature 子解析分别在 finally 中 recycle TypedArray。权限解析失败不会把半成品对象加入 PackageImpl,外层负责关闭 XML/AssetLoader。
6.3 测试范围
源码测试应覆盖三种标签路由、name 缺失、min/max SDK、required feature、purpose/general-purpose flag、flags/purpose 冲突、重复忽略、compat/split implicit 转换和 PackageImpl Parcelable/cache round-trip。每个测试应说明 Manifest 输入、关键断言和未覆盖的 runtime grant。
6.4 诊断命令
adb shell dumpsys package <package-name>
adb shell dumpsys package permissions
adb shell logcat -s PackageParser PackageManagerService PermissionManager对照 requestedPermissions、requested permission flags、target SDK、features、implicit permissions 和用户 grant。dumpsys 只能看到提交后的 effective 状态;声明为何缺失仍需回到 parseUsesPermission。
7. 源码路线
定位 uses-permission 问题时,按以下顺序阅读:
- ParsingPackageUtils.parseBaseApkTag:确认三个标签是否共用入口。
- parseUsesPermission:确认属性、子标签、SDK/feature、重复和对象创建。
- ParsedUsesPermission/ParsedUsesPermissionImpl:确认 flags、purpose 字段和 Parcelable。
- PackageImpl.addUsesPermission/addImplicitPermission:确认 mapping 与 implicit 集合。
- convertCompatPermissions/convertSplitPermissions:确认 effective set 扩展。
- PackageInfoUtils:确认 requestedPermissions 和 flags 输出。
- PermissionManager.onPackageInstalled:确认解析结果何时进入权限状态。
- PackageCacher/PackageImpl Parcel:确认 cache 命中后的权限数据来源。
一个实用练习是:Manifest 两次声明 CAMERA,第一次设置 NEVER_FOR_LOCATION,第二次设置不同 flags;设备缺少 required-feature,target SDK 又低于 minSdk。请判断 parser 会不会报 flags conflict、mapping 是否加入 CAMERA、purpose 子标签是否参与比较,以及 PermissionManager 是否会收到该权限请求。
8. 设计收束
Android 17 的 uses-permission 解析是一条“标签路由 → 条件过滤 → 映射去重 → 隐式转换 → 权限消费”链路:
- 三种 uses-permission 标签共用 parseUsesPermission;name、SDK 窗口、设备 feature 和 AConfig flags 决定声明是否真正进入 mapping。
- name 缺失、SDK/feature 不适用会返回 success 但不加入;结构错误和同名字段冲突才返回 ParseResult error。
- ParsedUsesPermissionImpl 保存 flags、purpose/general-purpose 集合和 purpose string resource;PackageImpl 用权限名 map 去重。
- compat/split permission 转换在 parseUsesPermission 之后发生,隐式权限同时进入 requested mapping 和 implicitPermissions 集合。
- PackageInfoUtils 和 PermissionManager 消费 effective requested permissions;解析阶段不直接授予 runtime permission。
- cache/Parcelable、TypedArray recycle、unknownTag、AConfig skip 和用户权限状态各有独立边界,诊断必须沿 owner 分层取证。
后续专题会继续解析 Manifest metadata,说明 Bundle/Property 数据如何从标签进入 PackageImpl,并与权限、组件和 cache 的生命周期衔接。
