Skip to content

uses-permission 解析

追踪 Android 17 uses-permission 标签的版本过滤、设备特性、用途声明、去重和隐式权限转换。

基于android-17.0.0_r1
AndroidPackageManagerServiceAndroidManifestuses-permissionPermissionManager源码阅读

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.java
  • frameworks/base/core/java/com/android/internal/pm/pkg/component/ParsedUsesPermission.java
  • frameworks/base/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
阶段owner数据消费者
Manifest parseParsingPackageUtilsname、flags、purpose、feature 条件PackageImpl.addUsesPermission
兼容转换ParsingPackageUtilsimplicitPermissions权限查询/安装策略
安装后权限PermissionManagerrequested permissions、用户 grantApplicationInfo/权限检查

解析成功只表示 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

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

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

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

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

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

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

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

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

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

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 缺失parseUsesPermissionsuccess,不加入
SDK 不在 min/maxparseUsesPermissionsuccess,不加入
required feature 不满足parseUsesPermissionsuccess,不加入
required-not-feature 存在parseUsesPermissionsuccess,不加入
child feature name 缺失child parserParseResult error
flags/purpose 冲突duplicate checkParseResult error
unknown childunknownTag按 parser 模式处理
AConfig skipchild 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 诊断命令 ​

text
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 问题时,按以下顺序阅读:

  1. ParsingPackageUtils.parseBaseApkTag:确认三个标签是否共用入口。
  2. parseUsesPermission:确认属性、子标签、SDK/feature、重复和对象创建。
  3. ParsedUsesPermission/ParsedUsesPermissionImpl:确认 flags、purpose 字段和 Parcelable。
  4. PackageImpl.addUsesPermission/addImplicitPermission:确认 mapping 与 implicit 集合。
  5. convertCompatPermissions/convertSplitPermissions:确认 effective set 扩展。
  6. PackageInfoUtils:确认 requestedPermissions 和 flags 输出。
  7. PermissionManager.onPackageInstalled:确认解析结果何时进入权限状态。
  8. 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 的生命周期衔接。