| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.content.pm.parsing; |
| |
| import static android.content.pm.ActivityInfo.FLAG_SUPPORTS_PICTURE_IN_PICTURE; |
| import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; |
| import static android.content.pm.PackageManager.FEATURE_WATCH; |
| import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST; |
| import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES; |
| import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NOT_APK; |
| import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION; |
| import static android.os.Build.VERSION_CODES.DONUT; |
| import static android.os.Build.VERSION_CODES.O; |
| import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; |
| |
| import android.annotation.AnyRes; |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.StyleableRes; |
| import android.app.ActivityThread; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.ConfigurationInfo; |
| import android.content.pm.FeatureGroupInfo; |
| import android.content.pm.FeatureInfo; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageParser; |
| import android.content.pm.PackageParser.PackageParserException; |
| import android.content.pm.PackageParser.SigningDetails; |
| import android.content.pm.Signature; |
| import android.content.pm.parsing.component.ComponentParseUtils; |
| import android.content.pm.parsing.component.ParsedActivity; |
| import android.content.pm.parsing.component.ParsedActivityUtils; |
| import android.content.pm.parsing.component.ParsedFeature; |
| import android.content.pm.parsing.component.ParsedFeatureUtils; |
| import android.content.pm.parsing.component.ParsedInstrumentation; |
| import android.content.pm.parsing.component.ParsedInstrumentationUtils; |
| import android.content.pm.parsing.component.ParsedIntentInfo; |
| import android.content.pm.parsing.component.ParsedIntentInfoUtils; |
| import android.content.pm.parsing.component.ParsedMainComponent; |
| import android.content.pm.parsing.component.ParsedPermission; |
| import android.content.pm.parsing.component.ParsedPermissionGroup; |
| import android.content.pm.parsing.component.ParsedPermissionUtils; |
| import android.content.pm.parsing.component.ParsedProcess; |
| import android.content.pm.parsing.component.ParsedProcessUtils; |
| import android.content.pm.parsing.component.ParsedProvider; |
| import android.content.pm.parsing.component.ParsedProviderUtils; |
| import android.content.pm.parsing.component.ParsedService; |
| import android.content.pm.parsing.component.ParsedServiceUtils; |
| import android.content.pm.parsing.result.ParseInput; |
| import android.content.pm.parsing.result.ParseResult; |
| import android.content.pm.permission.SplitPermissionInfoParcelable; |
| import android.content.pm.split.DefaultSplitAssetLoader; |
| import android.content.pm.split.SplitAssetDependencyLoader; |
| import android.content.pm.split.SplitAssetLoader; |
| import android.content.res.ApkAssets; |
| import android.content.res.AssetManager; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.content.res.TypedArray; |
| import android.content.res.XmlResourceParser; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.FileUtils; |
| import android.os.RemoteException; |
| import android.os.SystemProperties; |
| import android.os.Trace; |
| import android.os.ext.SdkExtensions; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.AttributeSet; |
| import android.util.DisplayMetrics; |
| import android.util.Pair; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| import android.util.TypedValue; |
| import android.util.apk.ApkSignatureVerifier; |
| |
| import com.android.internal.R; |
| import com.android.internal.os.ClassLoaderFactory; |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.util.XmlUtils; |
| |
| import libcore.io.IoUtils; |
| import libcore.util.EmptyArray; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.security.PublicKey; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.StringTokenizer; |
| |
| /** |
| * TODO(b/135203078): Differentiate between parse_ methods and some add_ method for whether it |
| * mutates the passed-in component or not. Or consolidate so all parse_ methods mutate. |
| * |
| * @hide |
| */ |
| public class ParsingPackageUtils { |
| |
| public static final String TAG = ParsingUtils.TAG; |
| |
| private boolean mOnlyCoreApps; |
| private String[] mSeparateProcesses; |
| private DisplayMetrics mDisplayMetrics; |
| private Callback mCallback; |
| |
| public ParsingPackageUtils(boolean onlyCoreApps, String[] separateProcesses, |
| DisplayMetrics displayMetrics, @NonNull Callback callback) { |
| mOnlyCoreApps = onlyCoreApps; |
| mSeparateProcesses = separateProcesses; |
| mDisplayMetrics = displayMetrics; |
| mCallback = callback; |
| } |
| |
| /** |
| * Parse the package at the given location. Automatically detects if the |
| * package is a monolithic style (single APK file) or cluster style |
| * (directory of APKs). |
| * <p> |
| * This performs sanity checking on cluster style packages, such as |
| * requiring identical package name and version codes, a single base APK, |
| * and unique split names. |
| * <p> |
| * Note that this <em>does not</em> perform signature verification; that |
| * must be done separately in {@link #collectCertificates(ParsingPackageRead, boolean)}. |
| * |
| * If {@code useCaches} is true, the package parser might return a cached |
| * result from a previous parse of the same {@code packageFile} with the same |
| * {@code flags}. Note that this method does not check whether {@code packageFile} |
| * has changed since the last parse, it's up to callers to do so. |
| * |
| * @see PackageParser#parsePackageLite(File, int) |
| */ |
| public ParseResult<ParsingPackage> parsePackage(ParseInput input, File packageFile, |
| int flags) |
| throws PackageParserException { |
| if (packageFile.isDirectory()) { |
| return parseClusterPackage(input, packageFile, flags); |
| } else { |
| return parseMonolithicPackage(input, packageFile, flags); |
| } |
| } |
| |
| /** |
| * Parse all APKs contained in the given directory, treating them as a |
| * single package. This also performs sanity checking, such as requiring |
| * identical package name and version codes, a single base APK, and unique |
| * split names. |
| * <p> |
| * Note that this <em>does not</em> perform signature verification; that |
| * must be done separately in {@link #collectCertificates(ParsingPackageRead, boolean)}. |
| */ |
| private ParseResult<ParsingPackage> parseClusterPackage(ParseInput input, File packageDir, |
| int flags) throws PackageParserException { |
| final PackageParser.PackageLite lite = ApkLiteParseUtils.parseClusterPackageLite(packageDir, |
| 0); |
| if (mOnlyCoreApps && !lite.coreApp) { |
| return input.error("Not a coreApp: " + packageDir); |
| } |
| |
| // Build the split dependency tree. |
| SparseArray<int[]> splitDependencies = null; |
| final SplitAssetLoader assetLoader; |
| if (lite.isolatedSplits && !ArrayUtils.isEmpty(lite.splitNames)) { |
| try { |
| splitDependencies = SplitAssetDependencyLoader.createDependenciesFromPackage(lite); |
| assetLoader = new SplitAssetDependencyLoader(lite, splitDependencies, flags); |
| } catch (SplitAssetDependencyLoader.IllegalDependencyException e) { |
| return input.error(INSTALL_PARSE_FAILED_BAD_MANIFEST, e.getMessage()); |
| } |
| } else { |
| assetLoader = new DefaultSplitAssetLoader(lite, flags); |
| } |
| |
| try { |
| final AssetManager assets = assetLoader.getBaseAssetManager(); |
| final File baseApk = new File(lite.baseCodePath); |
| ParseResult<ParsingPackage> result = parseBaseApk(input, baseApk, |
| lite.codePath, assets, flags); |
| // TODO(b/135203078): Pass original error up? |
| if (result.isError()) { |
| return input.error(INSTALL_PARSE_FAILED_NOT_APK, |
| "Failed to parse base APK: " + baseApk); |
| } |
| |
| ParsingPackage pkg = result.getResult(); |
| if (!ArrayUtils.isEmpty(lite.splitNames)) { |
| pkg.asSplit( |
| lite.splitNames, |
| lite.splitCodePaths, |
| lite.splitRevisionCodes, |
| splitDependencies |
| ); |
| final int num = lite.splitNames.length; |
| |
| for (int i = 0; i < num; i++) { |
| final AssetManager splitAssets = assetLoader.getSplitAssetManager(i); |
| parseSplitApk(input, pkg, i, splitAssets, flags); |
| } |
| } |
| |
| pkg.setUse32BitAbi(lite.use32bitAbi); |
| return input.success(pkg); |
| } finally { |
| IoUtils.closeQuietly(assetLoader); |
| } |
| } |
| |
| /** |
| * Parse the given APK file, treating it as as a single monolithic package. |
| * <p> |
| * Note that this <em>does not</em> perform signature verification; that |
| * must be done separately in {@link #collectCertificates(ParsingPackageRead, boolean)}. |
| */ |
| private ParseResult<ParsingPackage> parseMonolithicPackage(ParseInput input, File apkFile, |
| int flags) throws PackageParserException { |
| final PackageParser.PackageLite lite = ApkLiteParseUtils.parseMonolithicPackageLite(apkFile, |
| flags); |
| if (mOnlyCoreApps && !lite.coreApp) { |
| return input.error("Not a coreApp: " + apkFile); |
| } |
| |
| final SplitAssetLoader assetLoader = new DefaultSplitAssetLoader(lite, flags); |
| try { |
| ParseResult<ParsingPackage> result = parseBaseApk(input, |
| apkFile, |
| apkFile.getCanonicalPath(), |
| assetLoader.getBaseAssetManager(), flags); |
| if (result.isError()) { |
| return input.error(result); |
| } |
| |
| return input.success(result.getResult() |
| .setUse32BitAbi(lite.use32bitAbi)); |
| } catch (IOException e) { |
| return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, |
| "Failed to get path: " + apkFile, e); |
| } finally { |
| IoUtils.closeQuietly(assetLoader); |
| } |
| } |
| |
| private ParseResult<ParsingPackage> parseBaseApk(ParseInput input, File apkFile, |
| String codePath, AssetManager assets, int flags) { |
| final String apkPath = apkFile.getAbsolutePath(); |
| |
| String volumeUuid = null; |
| if (apkPath.startsWith(PackageParser.MNT_EXPAND)) { |
| final int end = apkPath.indexOf('/', PackageParser.MNT_EXPAND.length()); |
| volumeUuid = apkPath.substring(PackageParser.MNT_EXPAND.length(), end); |
| } |
| |
| if (PackageParser.DEBUG_JAR) Slog.d(TAG, "Scanning base APK: " + apkPath); |
| |
| final int cookie = assets.findCookieForPath(apkPath); |
| if (cookie == 0) { |
| return input.error(INSTALL_PARSE_FAILED_BAD_MANIFEST, |
| "Failed adding asset path: " + apkPath); |
| } |
| |
| try (XmlResourceParser parser = assets.openXmlResourceParser(cookie, |
| PackageParser.ANDROID_MANIFEST_FILENAME)) { |
| final Resources res = new Resources(assets, mDisplayMetrics, null); |
| |
| ParseResult<ParsingPackage> result = parseBaseApk(input, apkPath, codePath, res, |
| parser, flags); |
| if (result.isError()) { |
| return input.error(result.getErrorCode(), |
| apkPath + " (at " + parser.getPositionDescription() + "): " |
| + result.getErrorMessage()); |
| } |
| |
| ParsingPackage pkg = result.getResult(); |
| ApkAssets apkAssets = assets.getApkAssets()[0]; |
| if (apkAssets.definesOverlayable()) { |
| SparseArray<String> packageNames = assets.getAssignedPackageIdentifiers(); |
| int size = packageNames.size(); |
| for (int index = 0; index < size; index++) { |
| String packageName = packageNames.get(index); |
| Map<String, String> overlayableToActor = assets.getOverlayableMap(packageName); |
| if (overlayableToActor != null && !overlayableToActor.isEmpty()) { |
| for (String overlayable : overlayableToActor.keySet()) { |
| pkg.addOverlayable(overlayable, overlayableToActor.get(overlayable)); |
| } |
| } |
| } |
| } |
| |
| pkg.setVolumeUuid(volumeUuid) |
| .setSigningDetails(SigningDetails.UNKNOWN); |
| |
| return input.success(pkg); |
| } catch (Exception e) { |
| return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, |
| "Failed to read manifest from " + apkPath, e); |
| } |
| } |
| |
| private ParseResult<ParsingPackage> parseSplitApk(ParseInput input, |
| ParsingPackage pkg, int splitIndex, AssetManager assets, int flags) { |
| final String apkPath = pkg.getSplitCodePaths()[splitIndex]; |
| |
| if (PackageParser.DEBUG_JAR) Slog.d(TAG, "Scanning split APK: " + apkPath); |
| |
| // This must always succeed, as the path has been added to the AssetManager before. |
| final int cookie = assets.findCookieForPath(apkPath); |
| if (cookie == 0) { |
| return input.error(INSTALL_PARSE_FAILED_BAD_MANIFEST, |
| "Failed adding asset path: " + apkPath); |
| } |
| try (XmlResourceParser parser = assets.openXmlResourceParser(cookie, |
| PackageParser.ANDROID_MANIFEST_FILENAME)) { |
| Resources res = new Resources(assets, mDisplayMetrics, null); |
| ParseResult<ParsingPackage> parseResult = parseSplitApk(input, pkg, res, |
| parser, flags, splitIndex); |
| if (parseResult.isError()) { |
| return input.error(parseResult.getErrorCode(), |
| apkPath + " (at " + parser.getPositionDescription() + "): " |
| + parseResult.getErrorMessage()); |
| } |
| |
| return parseResult; |
| } catch (Exception e) { |
| return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, |
| "Failed to read manifest from " + apkPath, e); |
| } |
| } |
| |
| /** |
| * Parse the manifest of a <em>base APK</em>. When adding new features you |
| * need to consider whether they should be supported by split APKs and child |
| * packages. |
| * |
| * @param apkPath The package apk file path |
| * @param res The resources from which to resolve values |
| * @param parser The manifest parser |
| * @param flags Flags how to parse |
| * @return Parsed package or null on error. |
| */ |
| private ParseResult<ParsingPackage> parseBaseApk(ParseInput input, String apkPath, |
| String codePath, Resources res, XmlResourceParser parser, int flags) |
| throws XmlPullParserException, IOException { |
| final String splitName; |
| final String pkgName; |
| |
| try { |
| Pair<String, String> packageSplit = PackageParser.parsePackageSplitNames(parser, |
| parser); |
| pkgName = packageSplit.first; |
| splitName = packageSplit.second; |
| |
| if (!TextUtils.isEmpty(splitName)) { |
| return input.error( |
| PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME, |
| "Expected base APK, but found split " + splitName |
| ); |
| } |
| } catch (PackageParserException e) { |
| return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME); |
| } |
| |
| TypedArray manifestArray = res.obtainAttributes(parser, R.styleable.AndroidManifest); |
| try { |
| boolean isCoreApp = parser.getAttributeBooleanValue(null, "coreApp", false); |
| |
| ParsingPackage pkg = mCallback.startParsingPackage(pkgName, apkPath, codePath, |
| manifestArray, isCoreApp); |
| |
| ParseResult<ParsingPackage> result = parseBaseApkTags(input, pkg, manifestArray, |
| res, parser, flags); |
| if (result.isError()) { |
| return result; |
| } |
| |
| return input.success(pkg); |
| } finally { |
| manifestArray.recycle(); |
| } |
| } |
| |
| /** |
| * Parse the manifest of a <em>split APK</em>. |
| * <p> |
| * Note that split APKs have many more restrictions on what they're capable |
| * of doing, so many valid features of a base APK have been carefully |
| * omitted here. |
| * |
| * @param pkg builder to fill |
| * @return false on failure |
| */ |
| private ParseResult<ParsingPackage> parseSplitApk(ParseInput input, ParsingPackage pkg, |
| Resources res, XmlResourceParser parser, int flags, int splitIndex) |
| throws XmlPullParserException, IOException, PackageParserException { |
| AttributeSet attrs = parser; |
| |
| // We parsed manifest tag earlier; just skip past it |
| PackageParser.parsePackageSplitNames(parser, attrs); |
| |
| int type; |
| |
| boolean foundApp = false; |
| |
| int outerDepth = parser.getDepth(); |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { |
| if (outerDepth + 1 < parser.getDepth() || type != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| final ParseResult result; |
| String tagName = parser.getName(); |
| if (PackageParser.TAG_APPLICATION.equals(tagName)) { |
| if (foundApp) { |
| if (PackageParser.RIGID_PARSER) { |
| result = input.error("<manifest> has more than one <application>"); |
| } else { |
| Slog.w(TAG, "<manifest> has more than one <application>"); |
| result = input.success(null); |
| } |
| } else { |
| foundApp = true; |
| result = parseSplitApplication(input, pkg, res, parser, flags, splitIndex); |
| } |
| } else { |
| result = ParsingUtils.unknownTag("<manifest>", pkg, parser, input); |
| } |
| |
| if (result.isError()) { |
| return input.error(result); |
| } |
| } |
| |
| if (!foundApp) { |
| return input.error( |
| PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY, |
| "<manifest> does not contain an <application>" |
| ); |
| } |
| |
| return input.success(pkg); |
| } |
| |
| /** |
| * Parse the {@code application} XML tree at the current parse location in a |
| * <em>split APK</em> manifest. |
| * <p> |
| * Note that split APKs have many more restrictions on what they're capable |
| * of doing, so many valid features of a base APK have been carefully |
| * omitted here. |
| */ |
| private ParseResult<ParsingPackage> parseSplitApplication(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser, int flags, int splitIndex) |
| throws XmlPullParserException, IOException { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestApplication); |
| try { |
| pkg.setSplitHasCode(splitIndex, sa.getBoolean( |
| R.styleable.AndroidManifestApplication_hasCode, true)); |
| |
| final String classLoaderName = sa.getString( |
| R.styleable.AndroidManifestApplication_classLoader); |
| if (classLoaderName == null || ClassLoaderFactory.isValidClassLoaderName( |
| classLoaderName)) { |
| pkg.setSplitClassLoaderName(splitIndex, classLoaderName); |
| } else { |
| return input.error("Invalid class loader name: " + classLoaderName); |
| } |
| } finally { |
| sa.recycle(); |
| } |
| final int depth = parser.getDepth(); |
| int type; |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG |
| || parser.getDepth() > depth)) { |
| if (type != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| ParsedMainComponent mainComponent = null; |
| |
| final ParseResult result; |
| String tagName = parser.getName(); |
| boolean isActivity = false; |
| switch (tagName) { |
| case "activity": |
| isActivity = true; |
| // fall-through |
| case "receiver": |
| ParseResult<ParsedActivity> activityResult = |
| ParsedActivityUtils.parseActivityOrReceiver(mSeparateProcesses, pkg, |
| res, |
| parser, flags, PackageParser.sUseRoundIcon, input); |
| if (activityResult.isSuccess()) { |
| ParsedActivity activity = activityResult.getResult(); |
| if (isActivity) { |
| pkg.addActivity(activity); |
| } else { |
| pkg.addReceiver(activity); |
| } |
| mainComponent = activity; |
| } |
| result = activityResult; |
| break; |
| case "service": |
| ParseResult<ParsedService> serviceResult = ParsedServiceUtils.parseService( |
| mSeparateProcesses, pkg, res, parser, flags, |
| PackageParser.sUseRoundIcon, input); |
| if (serviceResult.isSuccess()) { |
| ParsedService service = serviceResult.getResult(); |
| pkg.addService(service); |
| mainComponent = service; |
| } |
| result = serviceResult; |
| break; |
| case "provider": |
| ParseResult<ParsedProvider> providerResult = |
| ParsedProviderUtils.parseProvider(mSeparateProcesses, pkg, res, parser, |
| flags, PackageParser.sUseRoundIcon, input); |
| if (providerResult.isSuccess()) { |
| ParsedProvider provider = providerResult.getResult(); |
| pkg.addProvider(provider); |
| mainComponent = provider; |
| } |
| result = providerResult; |
| break; |
| case "activity-alias": |
| activityResult = ParsedActivityUtils.parseActivityAlias(pkg, res, parser, |
| PackageParser.sUseRoundIcon, input); |
| if (activityResult.isSuccess()) { |
| ParsedActivity activity = activityResult.getResult(); |
| pkg.addActivity(activity); |
| mainComponent = activity; |
| } |
| |
| result = activityResult; |
| break; |
| default: |
| result = parseSplitBaseAppChildTags(input, tagName, pkg, res, parser); |
| break; |
| } |
| |
| if (result.isError()) { |
| return input.error(result); |
| } |
| |
| if (mainComponent != null && mainComponent.getSplitName() == null) { |
| // If the loaded component did not specify a split, inherit the split name |
| // based on the split it is defined in. |
| // This is used to later load the correct split when starting this |
| // component. |
| mainComponent.setSplitName(pkg.getSplitNames()[splitIndex]); |
| } |
| } |
| |
| return input.success(pkg); |
| } |
| |
| /** |
| * For parsing non-MainComponents. Main ones have an order and some special handling which is |
| * done directly in {@link #parseSplitApplication(ParseInput, ParsingPackage, Resources, |
| * XmlResourceParser, int, int)}. |
| */ |
| private ParseResult parseSplitBaseAppChildTags(ParseInput input, String tag, ParsingPackage pkg, |
| Resources res, XmlResourceParser parser) throws IOException, XmlPullParserException { |
| switch (tag) { |
| case "meta-data": |
| // note: application meta-data is stored off to the side, so it can |
| // remain null in the primary copy (we like to avoid extra copies because |
| // it can be large) |
| ParseResult<Bundle> metaDataResult = parseMetaData(pkg, res, parser, |
| pkg.getMetaData(), input); |
| if (metaDataResult.isSuccess()) { |
| pkg.setMetaData(metaDataResult.getResult()); |
| } |
| return metaDataResult; |
| case "uses-static-library": |
| return parseUsesStaticLibrary(input, pkg, res, parser); |
| case "uses-library": |
| return parseUsesLibrary(input, pkg, res, parser); |
| case "uses-package": |
| // Dependencies for app installers; we don't currently try to |
| // enforce this. |
| return input.success(null); |
| default: |
| return ParsingUtils.unknownTag("<application>", pkg, parser, input); |
| } |
| } |
| |
| private ParseResult<ParsingPackage> parseBaseApkTags(ParseInput input, ParsingPackage pkg, |
| TypedArray sa, Resources res, XmlResourceParser parser, int flags) |
| throws XmlPullParserException, IOException { |
| ParseResult<ParsingPackage> sharedUserResult = parseSharedUser(input, pkg, sa); |
| if (sharedUserResult.isError()) { |
| return sharedUserResult; |
| } |
| |
| pkg.setInstallLocation(anInt(PackageParser.PARSE_DEFAULT_INSTALL_LOCATION, |
| R.styleable.AndroidManifest_installLocation, sa)) |
| .setTargetSandboxVersion(anInt(PackageParser.PARSE_DEFAULT_TARGET_SANDBOX, |
| R.styleable.AndroidManifest_targetSandboxVersion, sa)) |
| /* Set the global "on SD card" flag */ |
| .setExternalStorage((flags & PackageParser.PARSE_EXTERNAL_STORAGE) != 0) |
| .setIsolatedSplitLoading( |
| bool(false, R.styleable.AndroidManifest_isolatedSplits, sa)); |
| |
| boolean foundApp = false; |
| final int depth = parser.getDepth(); |
| int type; |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG |
| || parser.getDepth() > depth)) { |
| if (type != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| String tagName = parser.getName(); |
| final ParseResult result; |
| |
| // TODO(b/135203078): Convert to instance methods to share variables |
| // <application> has special logic, so it's handled outside the general method |
| if (PackageParser.TAG_APPLICATION.equals(tagName)) { |
| if (foundApp) { |
| if (PackageParser.RIGID_PARSER) { |
| result = input.error("<manifest> has more than one <application>"); |
| } else { |
| Slog.w(TAG, "<manifest> has more than one <application>"); |
| result = input.success(null); |
| } |
| } else { |
| foundApp = true; |
| result = parseBaseApplication(input, pkg, res, parser, flags); |
| } |
| } else { |
| result = parseBaseApkTag(tagName, input, pkg, res, parser, flags); |
| } |
| |
| if (result.isError()) { |
| return input.error(result); |
| } |
| } |
| |
| if (!foundApp && ArrayUtils.size(pkg.getInstrumentations()) == 0) { |
| return input.error( |
| PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY, |
| "<manifest> does not contain an <application> or <instrumentation>" |
| ); |
| } |
| |
| if (!ParsedFeature.isCombinationValid(pkg.getFeatures())) { |
| return input.error( |
| INSTALL_PARSE_FAILED_BAD_MANIFEST, |
| "Combination <feature> tags are not valid" |
| ); |
| } |
| |
| convertNewPermissions(pkg); |
| |
| convertSplitPermissions(pkg); |
| |
| // At this point we can check if an application is not supporting densities and hence |
| // cannot be windowed / resized. Note that an SDK version of 0 is common for |
| // pre-Doughnut applications. |
| if (pkg.getTargetSdkVersion() < DONUT |
| || (!pkg.isSupportsSmallScreens() |
| && !pkg.isSupportsNormalScreens() |
| && !pkg.isSupportsLargeScreens() |
| && !pkg.isSupportsExtraLargeScreens() |
| && !pkg.isResizeable() |
| && !pkg.isAnyDensity())) { |
| adjustPackageToBeUnresizeableAndUnpipable(pkg); |
| } |
| |
| return input.success(pkg); |
| } |
| |
| private ParseResult parseBaseApkTag(String tag, ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser, int flags) |
| throws IOException, XmlPullParserException { |
| switch (tag) { |
| case PackageParser.TAG_OVERLAY: |
| return parseOverlay(input, pkg, res, parser); |
| case PackageParser.TAG_KEY_SETS: |
| return parseKeySets(input, pkg, res, parser); |
| case PackageParser.TAG_FEATURE: |
| return parseFeature(input, pkg, res, parser); |
| case PackageParser.TAG_PERMISSION_GROUP: |
| return parsePermissionGroup(input, pkg, res, parser); |
| case PackageParser.TAG_PERMISSION: |
| return parsePermission(input, pkg, res, parser); |
| case PackageParser.TAG_PERMISSION_TREE: |
| return parsePermissionTree(input, pkg, res, parser); |
| case PackageParser.TAG_USES_PERMISSION: |
| case PackageParser.TAG_USES_PERMISSION_SDK_M: |
| case PackageParser.TAG_USES_PERMISSION_SDK_23: |
| return parseUsesPermission(input, pkg, res, parser); |
| case PackageParser.TAG_USES_CONFIGURATION: |
| return parseUsesConfiguration(input, pkg, res, parser); |
| case PackageParser.TAG_USES_FEATURE: |
| return parseUsesFeature(input, pkg, res, parser); |
| case PackageParser.TAG_FEATURE_GROUP: |
| return parseFeatureGroup(input, pkg, res, parser); |
| case PackageParser.TAG_USES_SDK: |
| return parseUsesSdk(input, pkg, res, parser); |
| case PackageParser.TAG_SUPPORT_SCREENS: |
| return parseSupportScreens(input, pkg, res, parser); |
| case PackageParser.TAG_PROTECTED_BROADCAST: |
| return parseProtectedBroadcast(input, pkg, res, parser); |
| case PackageParser.TAG_INSTRUMENTATION: |
| return parseInstrumentation(input, pkg, res, parser); |
| case PackageParser.TAG_ORIGINAL_PACKAGE: |
| return parseOriginalPackage(input, pkg, res, parser); |
| case PackageParser.TAG_ADOPT_PERMISSIONS: |
| return parseAdoptPermissions(input, pkg, res, parser); |
| case PackageParser.TAG_USES_GL_TEXTURE: |
| case PackageParser.TAG_COMPATIBLE_SCREENS: |
| case PackageParser.TAG_SUPPORTS_INPUT: |
| case PackageParser.TAG_EAT_COMMENT: |
| // Just skip this tag |
| XmlUtils.skipCurrentTag(parser); |
| return input.success(pkg); |
| case PackageParser.TAG_RESTRICT_UPDATE: |
| return parseRestrictUpdateHash(flags, input, pkg, res, parser); |
| case PackageParser.TAG_QUERIES: |
| return parseQueries(input, pkg, res, parser); |
| default: |
| return ParsingUtils.unknownTag("<manifest>", pkg, parser, input); |
| } |
| } |
| |
| private static ParseResult<ParsingPackage> parseSharedUser(ParseInput input, |
| ParsingPackage pkg, TypedArray sa) { |
| String str = nonConfigString(0, R.styleable.AndroidManifest_sharedUserId, sa); |
| if (TextUtils.isEmpty(str)) { |
| return input.success(pkg); |
| } |
| |
| ParseResult nameResult = validateName(input, str, true, true); |
| if (nameResult.isError()) { |
| if ("android".equals(pkg.getPackageName())) { |
| nameResult.ignoreError(); |
| } else { |
| return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID, |
| "<manifest> specifies bad sharedUserId name \"" + str + "\": " |
| + nameResult.getErrorMessage()); |
| } |
| } |
| |
| return input.success(pkg |
| .setSharedUserId(str.intern()) |
| .setSharedUserLabel(resId(R.styleable.AndroidManifest_sharedUserLabel, sa))); |
| } |
| |
| private static ParseResult<ParsingPackage> parseKeySets(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) |
| throws XmlPullParserException, IOException { |
| // we've encountered the 'key-sets' tag |
| // all the keys and keysets that we want must be defined here |
| // so we're going to iterate over the parser and pull out the things we want |
| int outerDepth = parser.getDepth(); |
| int currentKeySetDepth = -1; |
| int type; |
| String currentKeySet = null; |
| ArrayMap<String, PublicKey> publicKeys = new ArrayMap<>(); |
| ArraySet<String> upgradeKeySets = new ArraySet<>(); |
| ArrayMap<String, ArraySet<String>> definedKeySets = new ArrayMap<>(); |
| ArraySet<String> improperKeySets = new ArraySet<>(); |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { |
| if (type == XmlPullParser.END_TAG) { |
| if (parser.getDepth() == currentKeySetDepth) { |
| currentKeySet = null; |
| currentKeySetDepth = -1; |
| } |
| continue; |
| } |
| String tagName = parser.getName(); |
| switch (tagName) { |
| case "key-set": { |
| if (currentKeySet != null) { |
| return input.error("Improperly nested 'key-set' tag at " |
| + parser.getPositionDescription()); |
| } |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestKeySet); |
| try { |
| final String keysetName = sa.getNonResourceString( |
| R.styleable.AndroidManifestKeySet_name); |
| definedKeySets.put(keysetName, new ArraySet<>()); |
| currentKeySet = keysetName; |
| currentKeySetDepth = parser.getDepth(); |
| } finally { |
| sa.recycle(); |
| } |
| } break; |
| case "public-key": { |
| if (currentKeySet == null) { |
| return input.error("Improperly nested 'key-set' tag at " |
| + parser.getPositionDescription()); |
| } |
| TypedArray sa = res.obtainAttributes(parser, |
| R.styleable.AndroidManifestPublicKey); |
| try { |
| final String publicKeyName = nonResString( |
| R.styleable.AndroidManifestPublicKey_name, sa); |
| final String encodedKey = nonResString( |
| R.styleable.AndroidManifestPublicKey_value, sa); |
| if (encodedKey == null && publicKeys.get(publicKeyName) == null) { |
| return input.error("'public-key' " + publicKeyName |
| + " must define a public-key value on first use at " |
| + parser.getPositionDescription()); |
| } else if (encodedKey != null) { |
| PublicKey currentKey = PackageParser.parsePublicKey(encodedKey); |
| if (currentKey == null) { |
| Slog.w(TAG, "No recognized valid key in 'public-key' tag at " |
| + parser.getPositionDescription() + " key-set " |
| + currentKeySet |
| + " will not be added to the package's defined key-sets."); |
| improperKeySets.add(currentKeySet); |
| XmlUtils.skipCurrentTag(parser); |
| continue; |
| } |
| if (publicKeys.get(publicKeyName) == null |
| || publicKeys.get(publicKeyName).equals(currentKey)) { |
| |
| /* public-key first definition, or matches old definition */ |
| publicKeys.put(publicKeyName, currentKey); |
| } else { |
| return input.error("Value of 'public-key' " + publicKeyName |
| + " conflicts with previously defined value at " |
| + parser.getPositionDescription()); |
| } |
| } |
| definedKeySets.get(currentKeySet).add(publicKeyName); |
| XmlUtils.skipCurrentTag(parser); |
| } finally { |
| sa.recycle(); |
| } |
| } break; |
| case "upgrade-key-set": { |
| TypedArray sa = res.obtainAttributes(parser, |
| R.styleable.AndroidManifestUpgradeKeySet); |
| try { |
| String name = sa.getNonResourceString( |
| R.styleable.AndroidManifestUpgradeKeySet_name); |
| upgradeKeySets.add(name); |
| XmlUtils.skipCurrentTag(parser); |
| } finally { |
| sa.recycle(); |
| } |
| } break; |
| default: |
| ParseResult result = ParsingUtils.unknownTag("<key-sets>", pkg, parser, |
| input); |
| if (result.isError()) { |
| return input.error(result); |
| } |
| break; |
| } |
| } |
| String packageName = pkg.getPackageName(); |
| Set<String> publicKeyNames = publicKeys.keySet(); |
| if (publicKeyNames.removeAll(definedKeySets.keySet())) { |
| return input.error("Package" + packageName |
| + " AndroidManifest.xml 'key-set' and 'public-key' names must be distinct."); |
| } |
| |
| for (ArrayMap.Entry<String, ArraySet<String>> e : definedKeySets.entrySet()) { |
| final String keySetName = e.getKey(); |
| if (e.getValue().size() == 0) { |
| Slog.w(TAG, "Package" + packageName + " AndroidManifest.xml " |
| + "'key-set' " + keySetName + " has no valid associated 'public-key'." |
| + " Not including in package's defined key-sets."); |
| continue; |
| } else if (improperKeySets.contains(keySetName)) { |
| Slog.w(TAG, "Package" + packageName + " AndroidManifest.xml " |
| + "'key-set' " + keySetName + " contained improper 'public-key'" |
| + " tags. Not including in package's defined key-sets."); |
| continue; |
| } |
| |
| for (String s : e.getValue()) { |
| pkg.addKeySet(keySetName, publicKeys.get(s)); |
| } |
| } |
| if (pkg.getKeySetMapping().keySet().containsAll(upgradeKeySets)) { |
| pkg.setUpgradeKeySets(upgradeKeySets); |
| } else { |
| return input.error("Package" + packageName |
| + " AndroidManifest.xml does not define all 'upgrade-key-set's ."); |
| } |
| |
| return input.success(pkg); |
| } |
| |
| private static ParseResult<ParsingPackage> parseFeature(ParseInput input, ParsingPackage pkg, |
| Resources res, XmlResourceParser parser) throws IOException, XmlPullParserException { |
| ParseResult<ParsedFeature> result = ParsedFeatureUtils.parseFeature(res, parser, input); |
| if (result.isError()) { |
| return input.error(result); |
| } |
| return input.success(pkg.addFeature(result.getResult())); |
| } |
| |
| private static ParseResult<ParsingPackage> parsePermissionGroup(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) |
| throws XmlPullParserException, IOException { |
| ParseResult<ParsedPermissionGroup> result = ParsedPermissionUtils.parsePermissionGroup( |
| pkg, res, parser, PackageParser.sUseRoundIcon, input); |
| if (result.isError()) { |
| return input.error(result); |
| } |
| return input.success(pkg.addPermissionGroup(result.getResult())); |
| } |
| |
| private static ParseResult<ParsingPackage> parsePermission(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) |
| throws XmlPullParserException, IOException { |
| ParseResult<ParsedPermission> result = ParsedPermissionUtils.parsePermission( |
| pkg, res, parser, PackageParser.sUseRoundIcon, input); |
| if (result.isError()) { |
| return input.error(result); |
| } |
| return input.success(pkg.addPermission(result.getResult())); |
| } |
| |
| private static ParseResult<ParsingPackage> parsePermissionTree(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) |
| throws XmlPullParserException, IOException { |
| ParseResult<ParsedPermission> result = ParsedPermissionUtils.parsePermissionTree( |
| pkg, res, parser, PackageParser.sUseRoundIcon, input); |
| if (result.isError()) { |
| return input.error(result); |
| } |
| return input.success(pkg.addPermission(result.getResult())); |
| } |
| |
| private ParseResult<ParsingPackage> parseUsesPermission(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) |
| throws IOException, XmlPullParserException { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestUsesPermission); |
| try { |
| // Note: don't allow this value to be a reference to a resource |
| // that may change. |
| String name = sa.getNonResourceString( |
| R.styleable.AndroidManifestUsesPermission_name); |
| |
| int maxSdkVersion = 0; |
| TypedValue val = sa.peekValue( |
| R.styleable.AndroidManifestUsesPermission_maxSdkVersion); |
| if (val != null) { |
| if (val.type >= TypedValue.TYPE_FIRST_INT && val.type <= TypedValue.TYPE_LAST_INT) { |
| maxSdkVersion = val.data; |
| } |
| } |
| |
| final String requiredFeature = sa.getNonConfigurationString( |
| R.styleable.AndroidManifestUsesPermission_requiredFeature, 0); |
| |
| final String requiredNotfeature = sa.getNonConfigurationString( |
| R.styleable.AndroidManifestUsesPermission_requiredNotFeature, |
| 0); |
| |
| XmlUtils.skipCurrentTag(parser); |
| |
| // Can only succeed from here on out |
| ParseResult<ParsingPackage> success = input.success(pkg); |
| |
| if (name == null) { |
| return success; |
| } |
| |
| if ((maxSdkVersion != 0) && (maxSdkVersion < Build.VERSION.RESOURCES_SDK_INT)) { |
| return success; |
| } |
| |
| // Only allow requesting this permission if the platform supports the given feature. |
| if (requiredFeature != null && mCallback != null && !mCallback.hasFeature( |
| requiredFeature)) { |
| return success; |
| } |
| |
| // Only allow requesting this permission if the platform doesn't support the given |
| // feature. |
| if (requiredNotfeature != null && mCallback != null |
| && mCallback.hasFeature(requiredNotfeature)) { |
| return success; |
| } |
| |
| if (!pkg.getRequestedPermissions().contains(name)) { |
| pkg.addRequestedPermission(name.intern()); |
| } else { |
| Slog.w(TAG, "Ignoring duplicate uses-permissions/uses-permissions-sdk-m: " |
| + name + " in package: " + pkg.getPackageName() + " at: " |
| + parser.getPositionDescription()); |
| } |
| |
| return success; |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| private static ParseResult<ParsingPackage> parseUsesConfiguration(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) { |
| ConfigurationInfo cPref = new ConfigurationInfo(); |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestUsesConfiguration); |
| try { |
| cPref.reqTouchScreen = sa.getInt( |
| R.styleable.AndroidManifestUsesConfiguration_reqTouchScreen, |
| Configuration.TOUCHSCREEN_UNDEFINED); |
| cPref.reqKeyboardType = sa.getInt( |
| R.styleable.AndroidManifestUsesConfiguration_reqKeyboardType, |
| Configuration.KEYBOARD_UNDEFINED); |
| if (sa.getBoolean( |
| R.styleable.AndroidManifestUsesConfiguration_reqHardKeyboard, |
| false)) { |
| cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD; |
| } |
| cPref.reqNavigation = sa.getInt( |
| R.styleable.AndroidManifestUsesConfiguration_reqNavigation, |
| Configuration.NAVIGATION_UNDEFINED); |
| if (sa.getBoolean( |
| R.styleable.AndroidManifestUsesConfiguration_reqFiveWayNav, |
| false)) { |
| cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV; |
| } |
| pkg.addConfigPreference(cPref); |
| return input.success(pkg); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| private static ParseResult<ParsingPackage> parseUsesFeature(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) { |
| FeatureInfo fi = parseFeatureInfo(res, parser); |
| pkg.addReqFeature(fi); |
| |
| if (fi.name == null) { |
| ConfigurationInfo cPref = new ConfigurationInfo(); |
| cPref.reqGlEsVersion = fi.reqGlEsVersion; |
| pkg.addConfigPreference(cPref); |
| } |
| |
| return input.success(pkg); |
| } |
| |
| private static FeatureInfo parseFeatureInfo(Resources res, AttributeSet attrs) { |
| FeatureInfo fi = new FeatureInfo(); |
| TypedArray sa = res.obtainAttributes(attrs, R.styleable.AndroidManifestUsesFeature); |
| try { |
| // Note: don't allow this value to be a reference to a resource |
| // that may change. |
| fi.name = sa.getNonResourceString(R.styleable.AndroidManifestUsesFeature_name); |
| fi.version = sa.getInt(R.styleable.AndroidManifestUsesFeature_version, 0); |
| if (fi.name == null) { |
| fi.reqGlEsVersion = sa.getInt(R.styleable.AndroidManifestUsesFeature_glEsVersion, |
| FeatureInfo.GL_ES_VERSION_UNDEFINED); |
| } |
| if (sa.getBoolean(R.styleable.AndroidManifestUsesFeature_required, true)) { |
| fi.flags |= FeatureInfo.FLAG_REQUIRED; |
| } |
| return fi; |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| private static ParseResult<ParsingPackage> parseFeatureGroup(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) |
| throws IOException, XmlPullParserException { |
| FeatureGroupInfo group = new FeatureGroupInfo(); |
| ArrayList<FeatureInfo> features = null; |
| final int depth = parser.getDepth(); |
| int type; |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG |
| || parser.getDepth() > depth)) { |
| if (type != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| final String innerTagName = parser.getName(); |
| if (innerTagName.equals("uses-feature")) { |
| FeatureInfo featureInfo = parseFeatureInfo(res, parser); |
| // FeatureGroups are stricter and mandate that |
| // any <uses-feature> declared are mandatory. |
| featureInfo.flags |= FeatureInfo.FLAG_REQUIRED; |
| features = ArrayUtils.add(features, featureInfo); |
| } else { |
| Slog.w(TAG, |
| "Unknown element under <feature-group>: " + innerTagName + |
| " at " + pkg.getBaseCodePath() + " " + |
| parser.getPositionDescription()); |
| } |
| } |
| |
| if (features != null) { |
| group.features = new FeatureInfo[features.size()]; |
| group.features = features.toArray(group.features); |
| } |
| |
| pkg.addFeatureGroup(group); |
| return input.success(pkg); |
| } |
| |
| private static ParseResult<ParsingPackage> parseUsesSdk(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) |
| throws IOException, XmlPullParserException { |
| if (PackageParser.SDK_VERSION > 0) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestUsesSdk); |
| try { |
| int minVers = 1; |
| String minCode = null; |
| int targetVers = 0; |
| String targetCode = null; |
| |
| TypedValue val = sa.peekValue(R.styleable.AndroidManifestUsesSdk_minSdkVersion); |
| if (val != null) { |
| if (val.type == TypedValue.TYPE_STRING && val.string != null) { |
| minCode = val.string.toString(); |
| } else { |
| // If it's not a string, it's an integer. |
| minVers = val.data; |
| } |
| } |
| |
| val = sa.peekValue(R.styleable.AndroidManifestUsesSdk_targetSdkVersion); |
| if (val != null) { |
| if (val.type == TypedValue.TYPE_STRING && val.string != null) { |
| targetCode = val.string.toString(); |
| if (minCode == null) { |
| minCode = targetCode; |
| } |
| } else { |
| // If it's not a string, it's an integer. |
| targetVers = val.data; |
| } |
| } else { |
| targetVers = minVers; |
| targetCode = minCode; |
| } |
| |
| ParseResult<Integer> minSdkVersionResult = computeMinSdkVersion(minVers, minCode, |
| PackageParser.SDK_VERSION, PackageParser.SDK_CODENAMES, input); |
| if (minSdkVersionResult.isError()) { |
| return input.error(minSdkVersionResult); |
| } |
| |
| int minSdkVersion = minSdkVersionResult.getResult(); |
| |
| ParseResult<Integer> targetSdkVersionResult = computeTargetSdkVersion( |
| targetVers, targetCode, PackageParser.SDK_CODENAMES, input); |
| if (targetSdkVersionResult.isError()) { |
| return input.error(targetSdkVersionResult); |
| } |
| |
| int targetSdkVersion = minSdkVersionResult.getResult(); |
| |
| pkg.setMinSdkVersion(minSdkVersion) |
| .setTargetSdkVersion(targetSdkVersion); |
| |
| int type; |
| final int innerDepth = parser.getDepth(); |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { |
| if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { |
| continue; |
| } |
| |
| final ParseResult result; |
| if (parser.getName().equals("extension-sdk")) { |
| result = parseExtensionSdk(input, pkg, res, parser); |
| XmlUtils.skipCurrentTag(parser); |
| } else { |
| result = ParsingUtils.unknownTag("<uses-sdk>", pkg, parser, input); |
| } |
| |
| if (result.isError()) { |
| return input.error(result); |
| } |
| } |
| } finally { |
| sa.recycle(); |
| } |
| } |
| return input.success(pkg); |
| } |
| |
| private static ParseResult parseExtensionSdk(ParseInput input, ParsingPackage pkg, |
| Resources res, XmlResourceParser parser) { |
| int sdkVersion; |
| int minVersion; |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestExtensionSdk); |
| try { |
| sdkVersion = sa.getInt(R.styleable.AndroidManifestExtensionSdk_sdkVersion, -1); |
| minVersion = sa.getInt(R.styleable.AndroidManifestExtensionSdk_minExtensionVersion, -1); |
| } finally { |
| sa.recycle(); |
| } |
| |
| if (sdkVersion < 0) { |
| return input.error( |
| PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, |
| "<extension-sdk> must specify an sdkVersion >= 0"); |
| } |
| if (minVersion < 0) { |
| return input.error( |
| PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, |
| "<extension-sdk> must specify minExtensionVersion >= 0"); |
| } |
| |
| try { |
| int version = SdkExtensions.getExtensionVersion(sdkVersion); |
| if (version < minVersion) { |
| return input.error( |
| PackageManager.INSTALL_FAILED_OLDER_SDK, |
| "Package requires " + sdkVersion + " extension version " + minVersion |
| + " which exceeds device version " + version); |
| } |
| } catch (RuntimeException e) { |
| return input.error( |
| PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, |
| "Specified sdkVersion " + sdkVersion + " is not valid"); |
| } |
| return input.success(pkg); |
| } |
| |
| /** |
| * {@link ParseResult} version of |
| * {@link PackageParser#computeMinSdkVersion(int, String, int, String[], String[])} |
| */ |
| public static ParseResult<Integer> computeMinSdkVersion(@IntRange(from = 1) int minVers, |
| @Nullable String minCode, @IntRange(from = 1) int platformSdkVersion, |
| @NonNull String[] platformSdkCodenames, @NonNull ParseInput input) { |
| // If it's a release SDK, make sure we meet the minimum SDK requirement. |
| if (minCode == null) { |
| if (minVers <= platformSdkVersion) { |
| return input.success(minVers); |
| } |
| |
| // We don't meet the minimum SDK requirement. |
| return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, |
| "Requires newer sdk version #" + minVers |
| + " (current version is #" + platformSdkVersion + ")"); |
| } |
| |
| // If it's a pre-release SDK and the codename matches this platform, we |
| // definitely meet the minimum SDK requirement. |
| if (matchTargetCode(platformSdkCodenames, minCode)) { |
| return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); |
| } |
| |
| // Otherwise, we're looking at an incompatible pre-release SDK. |
| if (platformSdkCodenames.length > 0) { |
| return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, |
| "Requires development platform " + minCode |
| + " (current platform is any of " |
| + Arrays.toString(platformSdkCodenames) + ")"); |
| } else { |
| return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, |
| "Requires development platform " + minCode |
| + " but this is a release platform."); |
| } |
| } |
| |
| /** |
| * {@link ParseResult} version of |
| * {@link PackageParser#computeTargetSdkVersion(int, String, String[], String[])} |
| */ |
| public static ParseResult<Integer> computeTargetSdkVersion(@IntRange(from = 0) int targetVers, |
| @Nullable String targetCode, @NonNull String[] platformSdkCodenames, |
| @NonNull ParseInput input) { |
| // If it's a release SDK, return the version number unmodified. |
| if (targetCode == null) { |
| return input.success(targetVers); |
| } |
| |
| // If it's a pre-release SDK and the codename matches this platform, it |
| // definitely targets this SDK. |
| if (matchTargetCode(platformSdkCodenames, targetCode)) { |
| return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); |
| } |
| |
| // Otherwise, we're looking at an incompatible pre-release SDK. |
| if (platformSdkCodenames.length > 0) { |
| return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, |
| "Requires development platform " + targetCode |
| + " (current platform is any of " |
| + Arrays.toString(platformSdkCodenames) + ")"); |
| } else { |
| return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, |
| "Requires development platform " + targetCode |
| + " but this is a release platform."); |
| } |
| } |
| |
| /** |
| * Matches a given {@code targetCode} against a set of release codeNames. Target codes can |
| * either be of the form {@code [codename]}" (e.g {@code "Q"}) or of the form |
| * {@code [codename].[fingerprint]} (e.g {@code "Q.cafebc561"}). |
| */ |
| private static boolean matchTargetCode(@NonNull String[] codeNames, |
| @NonNull String targetCode) { |
| final String targetCodeName; |
| final int targetCodeIdx = targetCode.indexOf('.'); |
| if (targetCodeIdx == -1) { |
| targetCodeName = targetCode; |
| } else { |
| targetCodeName = targetCode.substring(0, targetCodeIdx); |
| } |
| return ArrayUtils.contains(codeNames, targetCodeName); |
| } |
| |
| private static ParseResult<ParsingPackage> parseRestrictUpdateHash(int flags, ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) { |
| if ((flags & PackageParser.PARSE_IS_SYSTEM_DIR) != 0) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestRestrictUpdate); |
| try { |
| final String hash = sa.getNonConfigurationString( |
| R.styleable.AndroidManifestRestrictUpdate_hash, |
| 0); |
| |
| if (hash != null) { |
| final int hashLength = hash.length(); |
| final byte[] hashBytes = new byte[hashLength / 2]; |
| for (int i = 0; i < hashLength; i += 2) { |
| hashBytes[i / 2] = (byte) ((Character.digit(hash.charAt(i), 16) |
| << 4) |
| + Character.digit(hash.charAt(i + 1), 16)); |
| } |
| pkg.setRestrictUpdateHash(hashBytes); |
| } else { |
| pkg.setRestrictUpdateHash(null); |
| } |
| } finally { |
| sa.recycle(); |
| } |
| } |
| return input.success(pkg); |
| } |
| |
| private static ParseResult<ParsingPackage> parseQueries(ParseInput input, ParsingPackage pkg, |
| Resources res, XmlResourceParser parser) throws IOException, XmlPullParserException { |
| final int depth = parser.getDepth(); |
| int type; |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG |
| || parser.getDepth() > depth)) { |
| if (type != XmlPullParser.START_TAG) { |
| continue; |
| } |
| if (parser.getName().equals("intent")) { |
| ParseResult<ParsedIntentInfo> result = ParsedIntentInfoUtils.parseIntentInfo(null, |
| pkg, res, parser, true /*allowGlobs*/, true /*allowAutoVerify*/, input); |
| if (result.isError()) { |
| return input.error(result); |
| } |
| |
| ParsedIntentInfo intentInfo = result.getResult(); |
| |
| Uri data = null; |
| String dataType = null; |
| String host = ""; |
| final int numActions = intentInfo.countActions(); |
| final int numSchemes = intentInfo.countDataSchemes(); |
| final int numTypes = intentInfo.countDataTypes(); |
| final int numHosts = intentInfo.getHosts().length; |
| if ((numSchemes == 0 && numTypes == 0 && numActions == 0)) { |
| return input.error("intent tags must contain either an action or data."); |
| } |
| if (numActions > 1) { |
| return input.error("intent tag may have at most one action."); |
| } |
| if (numTypes > 1) { |
| return input.error("intent tag may have at most one data type."); |
| } |
| if (numSchemes > 1) { |
| return input.error("intent tag may have at most one data scheme."); |
| } |
| if (numHosts > 1) { |
| return input.error("intent tag may have at most one data host."); |
| } |
| Intent intent = new Intent(); |
| for (int i = 0, max = intentInfo.countCategories(); i < max; i++) { |
| intent.addCategory(intentInfo.getCategory(i)); |
| } |
| if (numHosts == 1) { |
| host = intentInfo.getHosts()[0]; |
| } |
| if (numSchemes == 1) { |
| data = new Uri.Builder() |
| .scheme(intentInfo.getDataScheme(0)) |
| .authority(host) |
| .build(); |
| } |
| if (numTypes == 1) { |
| dataType = intentInfo.getDataType(0); |
| } |
| intent.setDataAndType(data, dataType); |
| if (numActions == 1) { |
| intent.setAction(intentInfo.getAction(0)); |
| } |
| pkg.addQueriesIntent(intent); |
| } else if (parser.getName().equals("package")) { |
| final TypedArray sa = res.obtainAttributes(parser, |
| R.styleable.AndroidManifestQueriesPackage); |
| final String packageName = sa.getString( |
| R.styleable.AndroidManifestQueriesPackage_name); |
| if (TextUtils.isEmpty(packageName)) { |
| return input.error("Package name is missing from package tag."); |
| } |
| pkg.addQueriesPackage(packageName.intern()); |
| } else if (parser.getName().equals("provider")) { |
| final TypedArray sa = res.obtainAttributes(parser, |
| R.styleable.AndroidManifestQueriesProvider); |
| try { |
| final String authorities = |
| sa.getString(R.styleable.AndroidManifestQueriesProvider_authorities); |
| if (TextUtils.isEmpty(authorities)) { |
| return input.error( |
| PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, |
| "Authority missing from provider tag." |
| ); |
| } |
| StringTokenizer authoritiesTokenizer = new StringTokenizer(authorities, ";"); |
| while (authoritiesTokenizer.hasMoreElements()) { |
| pkg.addQueriesProvider(authoritiesTokenizer.nextToken()); |
| } |
| } finally { |
| sa.recycle(); |
| } |
| } |
| } |
| return input.success(pkg); |
| } |
| |
| /** |
| * 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. |
| * |
| * This method should avoid using a getter for fields set by this method. Prefer assigning |
| * a local variable and using it. Otherwise there's an ordering problem which can be broken |
| * if any code moves around. |
| */ |
| private ParseResult<ParsingPackage> parseBaseApplication(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser, int flags) |
| throws XmlPullParserException, IOException { |
| final String pkgName = pkg.getPackageName(); |
| int targetSdk = pkg.getTargetSdkVersion(); |
| |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestApplication); |
| try { |
| // TODO(b/135203078): Remove this and force unit tests to mock an empty manifest |
| // This case can only happen in unit tests where we sometimes need to create fakes |
| // of various package parser data structures. |
| if (sa == null) { |
| return input.error("<application> does not contain any attributes"); |
| } |
| |
| String name = sa.getNonConfigurationString(R.styleable.AndroidManifestApplication_name, |
| 0); |
| if (name != null) { |
| String packageName = pkg.getPackageName(); |
| String outInfoName = ParsingUtils.buildClassName(packageName, name); |
| if (PackageManager.APP_DETAILS_ACTIVITY_CLASS_NAME.equals(outInfoName)) { |
| return input.error("<application> invalid android:name"); |
| } else if (outInfoName == null) { |
| return input.error("Empty class name in package " + packageName); |
| } |
| |
| pkg.setClassName(outInfoName); |
| } |
| |
| TypedValue labelValue = sa.peekValue(R.styleable.AndroidManifestApplication_label); |
| if (labelValue != null) { |
| pkg.setLabelRes(labelValue.resourceId); |
| if (labelValue.resourceId == 0) { |
| pkg.setNonLocalizedLabel(labelValue.coerceToString()); |
| } |
| } |
| |
| parseBaseAppBasicFlags(pkg, sa); |
| |
| String manageSpaceActivity = nonConfigString(Configuration.NATIVE_CONFIG_VERSION, |
| R.styleable.AndroidManifestApplication_manageSpaceActivity, sa); |
| if (manageSpaceActivity != null) { |
| String manageSpaceActivityName = ParsingUtils.buildClassName(pkgName, |
| manageSpaceActivity); |
| |
| if (manageSpaceActivityName == null) { |
| return input.error("Empty class name in package " + pkgName); |
| } |
| |
| pkg.setManageSpaceActivityName(manageSpaceActivityName); |
| } |
| |
| if (pkg.isAllowBackup()) { |
| // backupAgent, killAfterRestore, fullBackupContent, backupInForeground, |
| // and restoreAnyVersion are only relevant if backup is possible for the |
| // given application. |
| String backupAgent = nonConfigString(Configuration.NATIVE_CONFIG_VERSION, |
| R.styleable.AndroidManifestApplication_backupAgent, sa); |
| if (backupAgent != null) { |
| String backupAgentName = ParsingUtils.buildClassName(pkgName, backupAgent); |
| if (backupAgentName == null) { |
| return input.error("Empty class name in package " + pkgName); |
| } |
| |
| if (PackageParser.DEBUG_BACKUP) { |
| Slog.v(TAG, "android:backupAgent = " + backupAgentName |
| + " from " + pkgName + "+" + backupAgent); |
| } |
| |
| pkg.setBackupAgentName(backupAgentName) |
| .setKillAfterRestore(bool(true, |
| R.styleable.AndroidManifestApplication_killAfterRestore, sa)) |
| .setRestoreAnyVersion(bool(false, |
| R.styleable.AndroidManifestApplication_restoreAnyVersion, sa)) |
| .setFullBackupOnly(bool(false, |
| R.styleable.AndroidManifestApplication_fullBackupOnly, sa)) |
| .setBackupInForeground(bool(false, |
| R.styleable.AndroidManifestApplication_backupInForeground, sa)); |
| } |
| |
| TypedValue v = sa.peekValue( |
| R.styleable.AndroidManifestApplication_fullBackupContent); |
| int fullBackupContent = 0; |
| |
| if (v != null) { |
| fullBackupContent = v.resourceId; |
| |
| if (v.resourceId == 0) { |
| if (PackageParser.DEBUG_BACKUP) { |
| Slog.v(TAG, "fullBackupContent specified as boolean=" + |
| (v.data == 0 ? "false" : "true")); |
| } |
| // "false" => -1, "true" => 0 |
| fullBackupContent = v.data == 0 ? -1 : 0; |
| } |
| |
| pkg.setFullBackupContent(fullBackupContent); |
| } |
| if (PackageParser.DEBUG_BACKUP) { |
| Slog.v(TAG, "fullBackupContent=" + fullBackupContent + " for " + pkgName); |
| } |
| } |
| |
| if (sa.getBoolean(R.styleable.AndroidManifestApplication_persistent, false)) { |
| // Check if persistence is based on a feature being present |
| final String requiredFeature = sa.getNonResourceString(R.styleable |
| .AndroidManifestApplication_persistentWhenFeatureAvailable); |
| pkg.setPersistent(requiredFeature == null || mCallback.hasFeature(requiredFeature)); |
| } |
| |
| // TODO(b/135203078): Should parsing code be responsible for this? Maybe move to a |
| // util or just have PackageImpl return true if either flag is set |
| // Debuggable implies profileable |
| pkg.setProfileableByShell(pkg.isProfileableByShell() || pkg.isDebuggable()); |
| |
| if (sa.hasValueOrEmpty(R.styleable.AndroidManifestApplication_resizeableActivity)) { |
| pkg.setResizeableActivity(sa.getBoolean( |
| R.styleable.AndroidManifestApplication_resizeableActivity, true)); |
| } else { |
| pkg.setResizeableActivityViaSdkVersion( |
| targetSdk >= Build.VERSION_CODES.N); |
| } |
| |
| String taskAffinity; |
| if (targetSdk >= Build.VERSION_CODES.FROYO) { |
| taskAffinity = sa.getNonConfigurationString( |
| R.styleable.AndroidManifestApplication_taskAffinity, |
| Configuration.NATIVE_CONFIG_VERSION); |
| } else { |
| // Some older apps have been seen to use a resource reference |
| // here that on older builds was ignored (with a warning). We |
| // need to continue to do this for them so they don't break. |
| taskAffinity = sa.getNonResourceString( |
| R.styleable.AndroidManifestApplication_taskAffinity); |
| } |
| |
| ParseResult<String> taskAffinityResult = ComponentParseUtils.buildTaskAffinityName( |
| pkgName, pkgName, taskAffinity, input); |
| if (taskAffinityResult.isError()) { |
| return input.error(taskAffinityResult); |
| } |
| |
| pkg.setTaskAffinity(taskAffinityResult.getResult()); |
| String factory = sa.getNonResourceString( |
| R.styleable.AndroidManifestApplication_appComponentFactory); |
| if (factory != null) { |
| String appComponentFactory = ParsingUtils.buildClassName(pkgName, factory); |
| if (appComponentFactory == null) { |
| return input.error("Empty class name in package " + pkgName); |
| } |
| |
| pkg.setAppComponentFactory(appComponentFactory); |
| } |
| |
| CharSequence pname; |
| if (targetSdk >= Build.VERSION_CODES.FROYO) { |
| pname = sa.getNonConfigurationString( |
| R.styleable.AndroidManifestApplication_process, |
| Configuration.NATIVE_CONFIG_VERSION); |
| } else { |
| // Some older apps have been seen to use a resource reference |
| // here that on older builds was ignored (with a warning). We |
| // need to continue to do this for them so they don't break. |
| pname = sa.getNonResourceString( |
| R.styleable.AndroidManifestApplication_process); |
| } |
| ParseResult<String> processNameResult = ComponentParseUtils.buildProcessName( |
| pkgName, null, pname, flags, mSeparateProcesses, input); |
| if (processNameResult.isError()) { |
| return input.error(processNameResult); |
| } |
| |
| String processName = processNameResult.getResult(); |
| pkg.setProcessName(processName); |
| |
| if (pkg.isCantSaveState()) { |
| // A heavy-weight application can not be in a custom process. |
| // We can do direct compare because we intern all strings. |
| if (processName != null && !processName.equals(pkgName)) { |
| return input.error( |
| "cantSaveState applications can not use custom processes"); |
| } |
| } |
| |
| String classLoaderName = pkg.getClassLoaderName(); |
| if (classLoaderName != null |
| && !ClassLoaderFactory.isValidClassLoaderName(classLoaderName)) { |
| return input.error("Invalid class loader name: " + classLoaderName); |
| } |
| } finally { |
| sa.recycle(); |
| } |
| |
| boolean hasActivityOrder = false; |
| boolean hasReceiverOrder = false; |
| boolean hasServiceOrder = false; |
| final int depth = parser.getDepth(); |
| int type; |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG |
| || parser.getDepth() > depth)) { |
| if (type != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| final ParseResult result; |
| String tagName = parser.getName(); |
| boolean isActivity = false; |
| switch (tagName) { |
| case "activity": |
| isActivity = true; |
| // fall-through |
| case "receiver": |
| ParseResult<ParsedActivity> activityResult = |
| ParsedActivityUtils.parseActivityOrReceiver(mSeparateProcesses, pkg, |
| res, parser, flags, PackageParser.sUseRoundIcon, input); |
| |
| if (activityResult.isSuccess()) { |
| ParsedActivity activity = activityResult.getResult(); |
| if (isActivity) { |
| hasActivityOrder |= (activity.getOrder() != 0); |
| pkg.addActivity(activity); |
| } else { |
| hasReceiverOrder |= (activity.getOrder() != 0); |
| pkg.addReceiver(activity); |
| } |
| } |
| |
| result = activityResult; |
| break; |
| case "service": |
| ParseResult<ParsedService> serviceResult = |
| ParsedServiceUtils.parseService(mSeparateProcesses, pkg, res, parser, |
| flags, PackageParser.sUseRoundIcon, input); |
| if (serviceResult.isSuccess()) { |
| ParsedService service = serviceResult.getResult(); |
| hasServiceOrder |= (service.getOrder() != 0); |
| pkg.addService(service); |
| } |
| |
| result = serviceResult; |
| break; |
| case "provider": |
| ParseResult<ParsedProvider> providerResult = |
| ParsedProviderUtils.parseProvider(mSeparateProcesses, pkg, res, parser, |
| flags, PackageParser.sUseRoundIcon, input); |
| if (providerResult.isSuccess()) { |
| pkg.addProvider(providerResult.getResult()); |
| } |
| |
| result = providerResult; |
| break; |
| case "activity-alias": |
| activityResult = ParsedActivityUtils.parseActivityAlias(pkg, res, |
| parser, PackageParser.sUseRoundIcon, input); |
| if (activityResult.isSuccess()) { |
| ParsedActivity activity = activityResult.getResult(); |
| hasActivityOrder |= (activity.getOrder() != 0); |
| pkg.addActivity(activity); |
| } |
| |
| result = activityResult; |
| break; |
| default: |
| result = parseBaseAppChildTag(input, tagName, pkg, res, parser, flags); |
| break; |
| } |
| |
| if (result.isError()) { |
| return input.error(result); |
| } |
| } |
| |
| if (TextUtils.isEmpty(pkg.getStaticSharedLibName())) { |
| // Add a hidden app detail activity to normal apps which forwards user to App Details |
| // page. |
| ParseResult<ParsedActivity> a = generateAppDetailsHiddenActivity(input, pkg); |
| // Backwards-compat, assume success |
| pkg.addActivity(a.getResult()); |
| a.ignoreError(); |
| } |
| |
| if (hasActivityOrder) { |
| pkg.sortActivities(); |
| } |
| if (hasReceiverOrder) { |
| pkg.sortReceivers(); |
| } |
| if (hasServiceOrder) { |
| pkg.sortServices(); |
| } |
| |
| // Must be run after the entire {@link ApplicationInfo} has been fully processed and after |
| // every activity info has had a chance to set it from its attributes. |
| setMaxAspectRatio(pkg); |
| setMinAspectRatio(pkg); |
| |
| pkg.setHasDomainUrls(hasDomainURLs(pkg)); |
| |
| return input.success(pkg); |
| } |
| |
| /** |
| * Collection of single-line, no (or little) logic assignments. Separated for readability. |
| * |
| * Flags are separated by type and by default value. They are sorted alphabetically within each |
| * section. |
| */ |
| private void parseBaseAppBasicFlags(ParsingPackage pkg, TypedArray sa) { |
| int targetSdk = pkg.getTargetSdkVersion(); |
| //@formatter:off |
| // CHECKSTYLE:off |
| pkg |
| // Default true |
| .setAllowBackup(bool(true, R.styleable.AndroidManifestApplication_allowBackup, sa)) |
| .setAllowClearUserData(bool(true, R.styleable.AndroidManifestApplication_allowClearUserData, sa)) |
| .setAllowClearUserDataOnFailedRestore(bool(true, R.styleable.AndroidManifestApplication_allowClearUserDataOnFailedRestore, sa)) |
| .setAllowNativeHeapPointerTagging(bool(true, R.styleable.AndroidManifestApplication_allowNativeHeapPointerTagging, sa)) |
| .setEnabled(bool(true, R.styleable.AndroidManifestApplication_enabled, sa)) |
| .setExtractNativeLibs(bool(true, R.styleable.AndroidManifestApplication_extractNativeLibs, sa)) |
| .setHasCode(bool(true, R.styleable.AndroidManifestApplication_hasCode, sa)) |
| // Default false |
| .setAllowTaskReparenting(bool(false, R.styleable.AndroidManifestApplication_allowTaskReparenting, sa)) |
| .setCantSaveState(bool(false, R.styleable.AndroidManifestApplication_cantSaveState, sa)) |
| .setCrossProfile(bool(false, R.styleable.AndroidManifestApplication_crossProfile, sa)) |
| .setDebuggable(bool(false, R.styleable.AndroidManifestApplication_debuggable, sa)) |
| .setDefaultToDeviceProtectedStorage(bool(false, R.styleable.AndroidManifestApplication_defaultToDeviceProtectedStorage, sa)) |
| .setDirectBootAware(bool(false, R.styleable.AndroidManifestApplication_directBootAware, sa)) |
| .setForceQueryable(bool(false, R.styleable.AndroidManifestApplication_forceQueryable, sa)) |
| .setGame(bool(false, R.styleable.AndroidManifestApplication_isGame, sa)) |
| .setHasFragileUserData(bool(false, R.styleable.AndroidManifestApplication_hasFragileUserData, sa)) |
| .setLargeHeap(bool(false, R.styleable.AndroidManifestApplication_largeHeap, sa)) |
| .setMultiArch(bool(false, R.styleable.AndroidManifestApplication_multiArch, sa)) |
| .setPreserveLegacyExternalStorage(bool(false, R.styleable.AndroidManifestApplication_preserveLegacyExternalStorage, sa)) |
| .setRequiredForAllUsers(bool(false, R.styleable.AndroidManifestApplication_requiredForAllUsers, sa)) |
| .setSupportsRtl(bool(false, R.styleable.AndroidManifestApplication_supportsRtl, sa)) |
| .setTestOnly(bool(false, R.styleable.AndroidManifestApplication_testOnly, sa)) |
| .setUseEmbeddedDex(bool(false, R.styleable.AndroidManifestApplication_useEmbeddedDex, sa)) |
| .setUsesNonSdkApi(bool(false, R.styleable.AndroidManifestApplication_usesNonSdkApi, sa)) |
| .setVmSafeMode(bool(false, R.styleable.AndroidManifestApplication_vmSafeMode, sa)) |
| // targetSdkVersion gated |
| .setAllowAudioPlaybackCapture(bool(targetSdk >= Build.VERSION_CODES.Q, R.styleable.AndroidManifestApplication_allowAudioPlaybackCapture, sa)) |
| .setBaseHardwareAccelerated(bool(targetSdk >= Build.VERSION_CODES.ICE_CREAM_SANDWICH, R.styleable.AndroidManifestApplication_hardwareAccelerated, sa)) |
| .setRequestLegacyExternalStorage(bool(targetSdk < Build.VERSION_CODES.Q, R.styleable.AndroidManifestApplication_requestLegacyExternalStorage, sa)) |
| .setUsesCleartextTraffic(bool(targetSdk < Build.VERSION_CODES.P, R.styleable.AndroidManifestApplication_usesCleartextTraffic, sa)) |
| // Ints Default 0 |
| .setUiOptions(anInt(R.styleable.AndroidManifestApplication_uiOptions, sa)) |
| // Ints |
| .setCategory(anInt(ApplicationInfo.CATEGORY_UNDEFINED, R.styleable.AndroidManifestApplication_appCategory, sa)) |
| // Floats Default 0f |
| .setMaxAspectRatio(aFloat(R.styleable.AndroidManifestApplication_maxAspectRatio, sa)) |
| .setMinAspectRatio(aFloat(R.styleable.AndroidManifestApplication_minAspectRatio, sa)) |
| // Resource ID |
| .setBanner(resId(R.styleable.AndroidManifestApplication_banner, sa)) |
| .setDescriptionRes(resId(R.styleable.AndroidManifestApplication_description, sa)) |
| .setIconRes(resId(R.styleable.AndroidManifestApplication_icon, sa)) |
| .setLogo(resId(R.styleable.AndroidManifestApplication_logo, sa)) |
| .setNetworkSecurityConfigRes(resId(R.styleable.AndroidManifestApplication_networkSecurityConfig, sa)) |
| .setRoundIconRes(resId(R.styleable.AndroidManifestApplication_roundIcon, sa)) |
| .setTheme(resId(R.styleable.AndroidManifestApplication_theme, sa)) |
| // Strings |
| .setClassLoaderName(string(R.styleable.AndroidManifestApplication_classLoader, sa)) |
| .setRequiredAccountType(string(R.styleable.AndroidManifestApplication_requiredAccountType, sa)) |
| .setRestrictedAccountType(string(R.styleable.AndroidManifestApplication_restrictedAccountType, sa)) |
| .setZygotePreloadName(string(R.styleable.AndroidManifestApplication_zygotePreloadName, sa)) |
| // Non-Config String |
| .setPermission(nonConfigString(0, R.styleable.AndroidManifestApplication_permission, sa)); |
| // CHECKSTYLE:on |
| //@formatter:on |
| } |
| |
| /** |
| * For parsing non-MainComponents. Main ones have an order and some special handling which is |
| * done directly in {@link #parseBaseApplication(ParseInput, ParsingPackage, Resources, |
| * XmlResourceParser, int)}. |
| */ |
| private ParseResult parseBaseAppChildTag(ParseInput input, String tag, ParsingPackage pkg, |
| Resources res, XmlResourceParser parser, int flags) |
| throws IOException, XmlPullParserException { |
| switch (tag) { |
| case "meta-data": |
| // TODO(b/135203078): I have no idea what this comment means |
| // note: application meta-data is stored off to the side, so it can |
| // remain null in the primary copy (we like to avoid extra copies because |
| // it can be large) |
| ParseResult<Bundle> metaDataResult = parseMetaData(pkg, res, parser, |
| pkg.getMetaData(), input); |
| if (metaDataResult.isSuccess()) { |
| pkg.setMetaData(metaDataResult.getResult()); |
| } |
| |
| return metaDataResult; |
| case "static-library": |
| return parseStaticLibrary(pkg, res, parser, input); |
| case "library": |
| return parseLibrary(pkg, res, parser, input); |
| case "uses-static-library": |
| return parseUsesStaticLibrary(input, pkg, res, parser); |
| case "uses-library": |
| return parseUsesLibrary(input, pkg, res, parser); |
| case "processes": |
| return parseProcesses(input, pkg, res, parser, mSeparateProcesses, flags); |
| case "uses-package": |
| // Dependencies for app installers; we don't currently try to |
| // enforce this. |
| return input.success(null); |
| case "profileable": |
| return parseProfileable(input, pkg, res, parser); |
| default: |
| return ParsingUtils.unknownTag("<application>", pkg, parser, input); |
| } |
| } |
| |
| @NonNull |
| private static ParseResult<ParsingPackage> parseStaticLibrary( |
| ParsingPackage pkg, Resources res, |
| XmlResourceParser parser, ParseInput input) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestStaticLibrary); |
| try { |
| // Note: don't allow this value to be a reference to a resource |
| // that may change. |
| String lname = sa.getNonResourceString( |
| R.styleable.AndroidManifestStaticLibrary_name); |
| final int version = sa.getInt( |
| R.styleable.AndroidManifestStaticLibrary_version, -1); |
| final int versionMajor = sa.getInt( |
| R.styleable.AndroidManifestStaticLibrary_versionMajor, |
| 0); |
| |
| // Since the app canot run without a static lib - fail if malformed |
| if (lname == null || version < 0) { |
| return input.error("Bad static-library declaration name: " + lname |
| + " version: " + version); |
| } else if (pkg.getSharedUserId() != null) { |
| return input.error( |
| PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID, |
| "sharedUserId not allowed in static shared library" |
| ); |
| } else if (pkg.getStaticSharedLibName() != null) { |
| return input.error("Multiple static-shared libs for package " |
| + pkg.getPackageName()); |
| } |
| |
| return input.success(pkg.setStaticSharedLibName(lname.intern()) |
| .setStaticSharedLibVersion( |
| PackageInfo.composeLongVersionCode(versionMajor, version)) |
| .setStaticSharedLibrary(true)); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| @NonNull |
| private static ParseResult<ParsingPackage> parseLibrary( |
| ParsingPackage pkg, Resources res, |
| XmlResourceParser parser, ParseInput input) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestLibrary); |
| try { |
| // Note: don't allow this value to be a reference to a resource |
| // that may change. |
| String lname = sa.getNonResourceString(R.styleable.AndroidManifestLibrary_name); |
| |
| if (lname != null) { |
| lname = lname.intern(); |
| if (!ArrayUtils.contains(pkg.getLibraryNames(), lname)) { |
| pkg.addLibraryName(lname); |
| } |
| } |
| return input.success(pkg); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| @NonNull |
| private static ParseResult<ParsingPackage> parseUsesStaticLibrary(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) |
| throws XmlPullParserException, IOException { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestUsesStaticLibrary); |
| try { |
| // Note: don't allow this value to be a reference to a resource that may change. |
| String lname = sa.getNonResourceString( |
| R.styleable.AndroidManifestUsesLibrary_name); |
| final int version = sa.getInt( |
| R.styleable.AndroidManifestUsesStaticLibrary_version, -1); |
| String certSha256Digest = sa.getNonResourceString(R.styleable |
| .AndroidManifestUsesStaticLibrary_certDigest); |
| |
| // Since an APK providing a static shared lib can only provide the lib - fail if |
| // malformed |
| if (lname == null || version < 0 || certSha256Digest == null) { |
| return input.error("Bad uses-static-library declaration name: " + lname |
| + " version: " + version + " certDigest" + certSha256Digest); |
| } |
| |
| // Can depend only on one version of the same library |
| List<String> usesStaticLibraries = pkg.getUsesStaticLibraries(); |
| if (usesStaticLibraries.contains(lname)) { |
| return input.error( |
| "Depending on multiple versions of static library " + lname); |
| } |
| |
| lname = lname.intern(); |
| // We allow ":" delimiters in the SHA declaration as this is the format |
| // emitted by the certtool making it easy for developers to copy/paste. |
| certSha256Digest = certSha256Digest.replace(":", "").toLowerCase(); |
| |
| // Fot apps targeting O-MR1 we require explicit enumeration of all certs. |
| String[] additionalCertSha256Digests = EmptyArray.STRING; |
| if (pkg.getTargetSdkVersion() >= Build.VERSION_CODES.O_MR1) { |
| ParseResult<String[]> certResult = parseAdditionalCertificates(input, res, parser); |
| if (certResult.isError()) { |
| return input.error(certResult); |
| } |
| additionalCertSha256Digests = certResult.getResult(); |
| } |
| |
| final String[] certSha256Digests = new String[additionalCertSha256Digests.length + 1]; |
| certSha256Digests[0] = certSha256Digest; |
| System.arraycopy(additionalCertSha256Digests, 0, certSha256Digests, |
| 1, additionalCertSha256Digests.length); |
| |
| return input.success(pkg.addUsesStaticLibrary(lname) |
| .addUsesStaticLibraryVersion(version) |
| .addUsesStaticLibraryCertDigests(certSha256Digests)); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| @NonNull |
| private static ParseResult<ParsingPackage> parseUsesLibrary(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestUsesLibrary); |
| try { |
| // Note: don't allow this value to be a reference to a resource |
| // that may change. |
| String lname = sa.getNonResourceString(R.styleable.AndroidManifestUsesLibrary_name); |
| boolean req = sa.getBoolean(R.styleable.AndroidManifestUsesLibrary_required, true); |
| |
| if (lname != null) { |
| lname = lname.intern(); |
| if (req) { |
| // Upgrade to treat as stronger constraint |
| pkg.addUsesLibrary(lname) |
| .removeUsesOptionalLibrary(lname); |
| } else { |
| // Ignore if someone already defined as required |
| if (!ArrayUtils.contains(pkg.getUsesLibraries(), lname)) { |
| pkg.addUsesOptionalLibrary(lname); |
| } |
| } |
| } |
| |
| return input.success(pkg); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| @NonNull |
| private static ParseResult<ParsingPackage> parseProcesses(ParseInput input, ParsingPackage pkg, |
| Resources res, XmlResourceParser parser, String[] separateProcesses, int flags) |
| throws IOException, XmlPullParserException { |
| ParseResult<ArrayMap<String, ParsedProcess>> result = |
| ParsedProcessUtils.parseProcesses(separateProcesses, pkg, res, parser, flags, |
| input); |
| if (result.isError()) { |
| return input.error(result); |
| } |
| |
| return input.success(pkg.setProcesses(result.getResult())); |
| } |
| |
| @NonNull |
| private static ParseResult<ParsingPackage> parseProfileable(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestProfileable); |
| try { |
| return input.success(pkg.setProfileableByShell(pkg.isProfileableByShell() |
| || bool(false, R.styleable.AndroidManifestProfileable_shell, sa))); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| private static ParseResult<String[]> parseAdditionalCertificates(ParseInput input, |
| Resources resources, XmlResourceParser parser) |
| throws XmlPullParserException, IOException { |
| String[] certSha256Digests = EmptyArray.STRING; |
| final int depth = parser.getDepth(); |
| int type; |
| while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG |
| || parser.getDepth() > depth)) { |
| if (type != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| final String nodeName = parser.getName(); |
| if (nodeName.equals("additional-certificate")) { |
| TypedArray sa = resources.obtainAttributes(parser, |
| R.styleable.AndroidManifestAdditionalCertificate); |
| try { |
| String certSha256Digest = sa.getNonResourceString( |
| R.styleable.AndroidManifestAdditionalCertificate_certDigest); |
| |
| if (TextUtils.isEmpty(certSha256Digest)) { |
| return input.error("Bad additional-certificate declaration with empty" |
| + " certDigest:" + certSha256Digest); |
| } |
| |
| |
| // We allow ":" delimiters in the SHA declaration as this is the format |
| // emitted by the certtool making it easy for developers to copy/paste. |
| certSha256Digest = certSha256Digest.replace(":", "").toLowerCase(); |
| certSha256Digests = ArrayUtils.appendElement(String.class, |
| certSha256Digests, certSha256Digest); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| } |
| |
| return input.success(certSha256Digests); |
| } |
| |
| /** |
| * Generate activity object that forwards user to App Details page automatically. |
| * This activity should be invisible to user and user should not know or see it. |
| */ |
| @NonNull |
| private static ParseResult<ParsedActivity> generateAppDetailsHiddenActivity(ParseInput input, |
| ParsingPackage pkg) { |
| String packageName = pkg.getPackageName(); |
| ParseResult<String> taskAffinityResult = ComponentParseUtils.buildTaskAffinityName( |
| packageName, packageName, ":app_details", input); |
| |
| String taskAffinity; |
| if (taskAffinityResult.isSuccess()) { |
| taskAffinity = taskAffinityResult.getResult(); |
| } else { |
| // Backwards-compat, do not fail |
| taskAffinity = null; |
| taskAffinityResult.ignoreError(); |
| } |
| |
| // Build custom App Details activity info instead of parsing it from xml |
| return input.success(ParsedActivity.makeAppDetailsActivity(packageName, |
| pkg.getProcessName(), pkg.getUiOptions(), taskAffinity, |
| pkg.isBaseHardwareAccelerated())); |
| } |
| |
| /** |
| * Check if one of the IntentFilter as both actions DEFAULT / VIEW and a HTTP/HTTPS data URI |
| */ |
| private static boolean hasDomainURLs(ParsingPackage pkg) { |
| final List<ParsedActivity> activities = pkg.getActivities(); |
| final int activitiesSize = activities.size(); |
| for (int index = 0; index < activitiesSize; index++) { |
| ParsedActivity activity = activities.get(index); |
| List<ParsedIntentInfo> filters = activity.getIntents(); |
| final int filtersSize = filters.size(); |
| for (int filtersIndex = 0; filtersIndex < filtersSize; filtersIndex++) { |
| ParsedIntentInfo aii = filters.get(filtersIndex); |
| if (!aii.hasAction(Intent.ACTION_VIEW)) continue; |
| if (!aii.hasAction(Intent.ACTION_DEFAULT)) continue; |
| if (aii.hasDataScheme(IntentFilter.SCHEME_HTTP) || |
| aii.hasDataScheme(IntentFilter.SCHEME_HTTPS)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Sets the max aspect ratio of every child activity that doesn't already have an aspect |
| * ratio set. |
| */ |
| private static void setMaxAspectRatio(ParsingPackage pkg) { |
| // Default to (1.86) 16.7:9 aspect ratio for pre-O apps and unset for O and greater. |
| // NOTE: 16.7:9 was the max aspect ratio Android devices can support pre-O per the CDD. |
| float maxAspectRatio = pkg.getTargetSdkVersion() < O |
| ? PackageParser.DEFAULT_PRE_O_MAX_ASPECT_RATIO : 0; |
| |
| float packageMaxAspectRatio = pkg.getMaxAspectRatio(); |
| if (packageMaxAspectRatio != 0) { |
| // Use the application max aspect ration as default if set. |
| maxAspectRatio = packageMaxAspectRatio; |
| } else { |
| Bundle appMetaData = pkg.getMetaData(); |
| if (appMetaData != null && appMetaData.containsKey( |
| PackageParser.METADATA_MAX_ASPECT_RATIO)) { |
| maxAspectRatio = appMetaData.getFloat(PackageParser.METADATA_MAX_ASPECT_RATIO, |
| maxAspectRatio); |
| } |
| } |
| |
| List<ParsedActivity> activities = pkg.getActivities(); |
| int activitiesSize = activities.size(); |
| for (int index = 0; index < activitiesSize; index++) { |
| ParsedActivity activity = activities.get(index); |
| // If the max aspect ratio for the activity has already been set, skip. |
| if (activity.getMaxAspectRatio() != null) { |
| continue; |
| } |
| |
| // By default we prefer to use a values defined on the activity directly than values |
| // defined on the application. We do not check the styled attributes on the activity |
| // as it would have already been set when we processed the activity. We wait to |
| // process the meta data here since this method is called at the end of processing |
| // the application and all meta data is guaranteed. |
| final float activityAspectRatio = activity.getMetaData() != null |
| ? activity.getMetaData().getFloat(PackageParser.METADATA_MAX_ASPECT_RATIO, |
| maxAspectRatio) |
| : maxAspectRatio; |
| |
| activity.setMaxAspectRatio(activity.getResizeMode(), activityAspectRatio); |
| } |
| } |
| |
| /** |
| * Sets the min aspect ratio of every child activity that doesn't already have an aspect |
| * ratio set. |
| */ |
| private void setMinAspectRatio(ParsingPackage pkg) { |
| final float minAspectRatio; |
| float packageMinAspectRatio = pkg.getMinAspectRatio(); |
| if (packageMinAspectRatio != 0) { |
| // Use the application max aspect ration as default if set. |
| minAspectRatio = packageMinAspectRatio; |
| } else { |
| // Default to (1.33) 4:3 aspect ratio for pre-Q apps and unset for Q and greater. |
| // NOTE: 4:3 was the min aspect ratio Android devices can support pre-Q per the CDD, |
| // except for watches which always supported 1:1. |
| minAspectRatio = pkg.getTargetSdkVersion() >= Build.VERSION_CODES.Q |
| ? 0 |
| : (mCallback != null && mCallback.hasFeature(FEATURE_WATCH)) |
| ? PackageParser.DEFAULT_PRE_Q_MIN_ASPECT_RATIO_WATCH |
| : PackageParser.DEFAULT_PRE_Q_MIN_ASPECT_RATIO; |
| } |
| |
| List<ParsedActivity> activities = pkg.getActivities(); |
| int activitiesSize = activities.size(); |
| for (int index = 0; index < activitiesSize; index++) { |
| ParsedActivity activity = activities.get(index); |
| if (activity.getMinAspectRatio() == null) { |
| activity.setMinAspectRatio(activity.getResizeMode(), minAspectRatio); |
| } |
| } |
| } |
| |
| private static ParseResult<ParsingPackage> parseOverlay(ParseInput input, ParsingPackage pkg, |
| Resources res, XmlResourceParser parser) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestResourceOverlay); |
| try { |
| String target = sa.getString(R.styleable.AndroidManifestResourceOverlay_targetPackage); |
| int priority = anInt(0, R.styleable.AndroidManifestResourceOverlay_priority, sa); |
| |
| if (target == null) { |
| return input.error("<overlay> does not specify a target package"); |
| } else if (priority < 0 || priority > 9999) { |
| return input.error("<overlay> priority must be between 0 and 9999"); |
| } |
| |
| // check to see if overlay should be excluded based on system property condition |
| String propName = sa.getString( |
| R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyName); |
| String propValue = sa.getString( |
| R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyValue); |
| if (!checkOverlayRequiredSystemProperty(propName, propValue)) { |
| Slog.i(TAG, "Skipping target and overlay pair " + target + " and " |
| + pkg.getBaseCodePath() |
| + ": overlay ignored due to required system property: " |
| + propName + " with value: " + propValue); |
| return input.error("Skipping target and overlay pair " + target + " and " |
| + pkg.getBaseCodePath() |
| + ": overlay ignored due to required system property: " |
| + propName + " with value: " + propValue); |
| } |
| |
| return input.success(pkg.setOverlay(true) |
| .setOverlayTarget(target) |
| .setOverlayPriority(priority) |
| .setOverlayTargetName( |
| sa.getString(R.styleable.AndroidManifestResourceOverlay_targetName)) |
| .setOverlayCategory( |
| sa.getString(R.styleable.AndroidManifestResourceOverlay_category)) |
| .setOverlayIsStatic( |
| bool(false, R.styleable.AndroidManifestResourceOverlay_isStatic, sa))); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| private static ParseResult<ParsingPackage> parseProtectedBroadcast(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestProtectedBroadcast); |
| try { |
| // Note: don't allow this value to be a reference to a resource |
| // that may change. |
| String name = nonResString(R.styleable.AndroidManifestProtectedBroadcast_name, sa); |
| if (name != null) { |
| pkg.addProtectedBroadcast(name); |
| } |
| return input.success(pkg); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| private static ParseResult<ParsingPackage> parseSupportScreens(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestSupportsScreens); |
| try { |
| int requiresSmallestWidthDp = anInt(0, |
| R.styleable.AndroidManifestSupportsScreens_requiresSmallestWidthDp, sa); |
| int compatibleWidthLimitDp = anInt(0, |
| R.styleable.AndroidManifestSupportsScreens_compatibleWidthLimitDp, sa); |
| int largestWidthLimitDp = anInt(0, |
| R.styleable.AndroidManifestSupportsScreens_largestWidthLimitDp, sa); |
| |
| // This is a trick to get a boolean and still able to detect |
| // if a value was actually set. |
| return input.success(pkg |
| .setSupportsSmallScreens( |
| anInt(1, R.styleable.AndroidManifestSupportsScreens_smallScreens, sa)) |
| .setSupportsNormalScreens( |
| anInt(1, R.styleable.AndroidManifestSupportsScreens_normalScreens, sa)) |
| .setSupportsLargeScreens( |
| anInt(1, R.styleable.AndroidManifestSupportsScreens_largeScreens, sa)) |
| .setSupportsExtraLargeScreens( |
| anInt(1, R.styleable.AndroidManifestSupportsScreens_xlargeScreens, sa)) |
| .setResizeable( |
| anInt(1, R.styleable.AndroidManifestSupportsScreens_resizeable, sa)) |
| .setAnyDensity( |
| anInt(1, R.styleable.AndroidManifestSupportsScreens_anyDensity, sa)) |
| .setRequiresSmallestWidthDp(requiresSmallestWidthDp) |
| .setCompatibleWidthLimitDp(compatibleWidthLimitDp) |
| .setLargestWidthLimitDp(largestWidthLimitDp)); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| private static ParseResult<ParsingPackage> parseInstrumentation(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) |
| throws XmlPullParserException, IOException { |
| ParseResult<ParsedInstrumentation> result = ParsedInstrumentationUtils.parseInstrumentation( |
| pkg, res, parser, PackageParser.sUseRoundIcon, input); |
| if (result.isError()) { |
| return input.error(result); |
| } |
| return input.success(pkg.addInstrumentation(result.getResult())); |
| } |
| |
| private static ParseResult<ParsingPackage> parseOriginalPackage(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestOriginalPackage); |
| try { |
| String orig = sa.getNonConfigurationString( |
| R.styleable.AndroidManifestOriginalPackage_name, |
| 0); |
| if (!pkg.getPackageName().equals(orig)) { |
| if (pkg.getOriginalPackages().isEmpty()) { |
| pkg.setRealPackage(pkg.getPackageName()); |
| } |
| pkg.addOriginalPackage(orig); |
| } |
| return input.success(pkg); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| private static ParseResult<ParsingPackage> parseAdoptPermissions(ParseInput input, |
| ParsingPackage pkg, Resources res, XmlResourceParser parser) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestOriginalPackage); |
| try { |
| String name = nonConfigString(0, R.styleable.AndroidManifestOriginalPackage_name, sa); |
| if (name != null) { |
| pkg.addAdoptPermission(name); |
| } |
| return input.success(pkg); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| private static void convertNewPermissions(ParsingPackage pkg) { |
| final int NP = PackageParser.NEW_PERMISSIONS.length; |
| StringBuilder newPermsMsg = null; |
| for (int ip = 0; ip < NP; ip++) { |
| final PackageParser.NewPermissionInfo npi |
| = PackageParser.NEW_PERMISSIONS[ip]; |
| if (pkg.getTargetSdkVersion() >= npi.sdkVersion) { |
| break; |
| } |
| if (!pkg.getRequestedPermissions().contains(npi.name)) { |
| if (newPermsMsg == null) { |
| newPermsMsg = new StringBuilder(128); |
| newPermsMsg.append(pkg.getPackageName()); |
| newPermsMsg.append(": compat added "); |
| } else { |
| newPermsMsg.append(' '); |
| } |
| newPermsMsg.append(npi.name); |
| pkg.addRequestedPermission(npi.name) |
| .addImplicitPermission(npi.name); |
| } |
| } |
| if (newPermsMsg != null) { |
| Slog.i(TAG, newPermsMsg.toString()); |
| } |
| } |
| |
| private static void convertSplitPermissions(ParsingPackage pkg) { |
| List<SplitPermissionInfoParcelable> splitPermissions; |
| |
| try { |
| splitPermissions = ActivityThread.getPermissionManager().getSplitPermissions(); |
| } catch (RemoteException e) { |
| throw e.rethrowFromSystemServer(); |
| } |
| |
| final int listSize = splitPermissions.size(); |
| for (int is = 0; is < listSize; is++) { |
| final SplitPermissionInfoParcelable spi = splitPermissions.get(is); |
| List<String> requestedPermissions = pkg.getRequestedPermissions(); |
| if (pkg.getTargetSdkVersion() >= spi.getTargetSdk() |
| || !requestedPermissions.contains(spi.getSplitPermission())) { |
| continue; |
| } |
| final List<String> newPerms = spi.getNewPermissions(); |
| for (int in = 0; in < newPerms.size(); in++) { |
| final String perm = newPerms.get(in); |
| if (!requestedPermissions.contains(perm)) { |
| pkg.addRequestedPermission(perm) |
| .addImplicitPermission(perm); |
| } |
| } |
| } |
| } |
| |
| private static boolean checkOverlayRequiredSystemProperty(String propName, String propValue) { |
| if (TextUtils.isEmpty(propName) || TextUtils.isEmpty(propValue)) { |
| if (!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue)) { |
| // malformed condition - incomplete |
| Slog.w(TAG, "Disabling overlay - incomplete property :'" + propName |
| + "=" + propValue + "' - require both requiredSystemPropertyName" |
| + " AND requiredSystemPropertyValue to be specified."); |
| return false; |
| } |
| // no valid condition set - so no exclusion criteria, overlay will be included. |
| return true; |
| } |
| |
| // check property value - make sure it is both set and equal to expected value |
| final String currValue = SystemProperties.get(propName); |
| return (currValue != null && currValue.equals(propValue)); |
| } |
| |
| /** |
| * This is a pre-density application which will get scaled - instead of being pixel perfect. |
| * This type of application is not resizable. |
| * |
| * @param pkg The package which needs to be marked as unresizable. |
| */ |
| private static void adjustPackageToBeUnresizeableAndUnpipable(ParsingPackage pkg) { |
| List<ParsedActivity> activities = pkg.getActivities(); |
| int activitiesSize = activities.size(); |
| for (int index = 0; index < activitiesSize; index++) { |
| ParsedActivity activity = activities.get(index); |
| activity.setResizeMode(RESIZE_MODE_UNRESIZEABLE) |
| .setFlags(activity.getFlags() & ~FLAG_SUPPORTS_PICTURE_IN_PICTURE); |
| } |
| } |
| |
| private static ParseResult validateName(ParseInput input, String name, boolean requireSeparator, |
| boolean requireFilename) { |
| final int N = name.length(); |
| boolean hasSep = false; |
| boolean front = true; |
| for (int i = 0; i < N; i++) { |
| final char c = name.charAt(i); |
| if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { |
| front = false; |
| continue; |
| } |
| if (!front) { |
| if ((c >= '0' && c <= '9') || c == '_') { |
| continue; |
| } |
| } |
| if (c == '.') { |
| hasSep = true; |
| front = true; |
| continue; |
| } |
| return input.error("bad character '" + c + "'"); |
| } |
| if (requireFilename && !FileUtils.isValidExtFilename(name)) { |
| return input.error("Invalid filename"); |
| } |
| return hasSep || !requireSeparator |
| ? input.success(null) |
| : input.error("must have at least one '.' separator"); |
| } |
| |
| public static ParseResult<Bundle> parseMetaData(ParsingPackage pkg, Resources res, |
| XmlResourceParser parser, Bundle data, ParseInput input) { |
| TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestMetaData); |
| try { |
| if (data == null) { |
| data = new Bundle(); |
| } |
| |
| String name = TextUtils.safeIntern( |
| nonConfigString(0, R.styleable.AndroidManifestMetaData_name, sa)); |
| if (name == null) { |
| return input.error("<meta-data> requires an android:name attribute"); |
| } |
| |
| TypedValue v = sa.peekValue(R.styleable.AndroidManifestMetaData_resource); |
| if (v != null && v.resourceId != 0) { |
| //Slog.i(TAG, "Meta data ref " + name + ": " + v); |
| data.putInt(name, v.resourceId); |
| } else { |
| v = sa.peekValue(R.styleable.AndroidManifestMetaData_value); |
| //Slog.i(TAG, "Meta data " + name + ": " + v); |
| if (v != null) { |
| if (v.type == TypedValue.TYPE_STRING) { |
| CharSequence cs = v.coerceToString(); |
| data.putString(name, cs != null ? cs.toString() : null); |
| } else if (v.type == TypedValue.TYPE_INT_BOOLEAN) { |
| data.putBoolean(name, v.data != 0); |
| } else if (v.type >= TypedValue.TYPE_FIRST_INT |
| && v.type <= TypedValue.TYPE_LAST_INT) { |
| data.putInt(name, v.data); |
| } else if (v.type == TypedValue.TYPE_FLOAT) { |
| data.putFloat(name, v.getFloat()); |
| } else { |
| if (!PackageParser.RIGID_PARSER) { |
| Slog.w(TAG, |
| "<meta-data> only supports string, integer, float, color, " |
| + "boolean, and resource reference types: " |
| + parser.getName() + " at " |
| + pkg.getBaseCodePath() + " " |
| + parser.getPositionDescription()); |
| } else { |
| return input.error("<meta-data> only supports string, integer, float, " |
| + "color, boolean, and resource reference types"); |
| } |
| } |
| } else { |
| return input.error("<meta-data> requires an android:value " |
| + "or android:resource attribute"); |
| } |
| } |
| return input.success(data); |
| } finally { |
| sa.recycle(); |
| } |
| } |
| |
| /** |
| * Collect certificates from all the APKs described in the given package. Also asserts that |
| * all APK contents are signed correctly and consistently. |
| */ |
| public static SigningDetails collectCertificates(ParsingPackageRead pkg, boolean skipVerify) |
| throws PackageParserException { |
| SigningDetails signingDetails = SigningDetails.UNKNOWN; |
| |
| Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "collectCertificates"); |
| try { |
| signingDetails = collectCertificates( |
| pkg.getBaseCodePath(), |
| skipVerify, |
| pkg.isStaticSharedLibrary(), |
| signingDetails, |
| pkg.getTargetSdkVersion() |
| ); |
| |
| String[] splitCodePaths = pkg.getSplitCodePaths(); |
| if (!ArrayUtils.isEmpty(splitCodePaths)) { |
| for (int i = 0; i < splitCodePaths.length; i++) { |
| signingDetails = collectCertificates( |
| splitCodePaths[i], |
| skipVerify, |
| pkg.isStaticSharedLibrary(), |
| signingDetails, |
| pkg.getTargetSdkVersion() |
| ); |
| } |
| } |
| return signingDetails; |
| } finally { |
| Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); |
| } |
| } |
| |
| public static SigningDetails collectCertificates(String baseCodePath, boolean skipVerify, |
| boolean isStaticSharedLibrary, @NonNull SigningDetails existingSigningDetails, |
| int targetSdk) throws PackageParserException { |
| int minSignatureScheme = ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk( |
| targetSdk); |
| if (isStaticSharedLibrary) { |
| // must use v2 signing scheme |
| minSignatureScheme = SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2; |
| } |
| SigningDetails verified; |
| if (skipVerify) { |
| // systemDir APKs are already trusted, save time by not verifying |
| verified = ApkSignatureVerifier.unsafeGetCertsWithoutVerification( |
| baseCodePath, minSignatureScheme); |
| } else { |
| verified = ApkSignatureVerifier.verify(baseCodePath, minSignatureScheme); |
| } |
| |
| // Verify that entries are signed consistently with the first pkg |
| // we encountered. Note that for splits, certificates may have |
| // already been populated during an earlier parse of a base APK. |
| if (existingSigningDetails == SigningDetails.UNKNOWN) { |
| return verified; |
| } else { |
| if (!Signature.areExactMatch(existingSigningDetails.signatures, verified.signatures)) { |
| throw new PackageParserException( |
| INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, |
| baseCodePath + " has mismatched certificates"); |
| } |
| |
| return existingSigningDetails; |
| } |
| } |
| |
| /* |
| The following set of methods makes code easier to read by re-ordering the TypedArray methods. |
| |
| The first parameter is the default, which is the most important to understand for someone |
| reading through the parsing code. |
| |
| That's followed by the attribute name, which is usually irrelevant during reading because |
| it'll look like setSomeValue(true, R.styleable.ReallyLongParentName_SomeValueAttr... and |
| the "setSomeValue" part is enough to communicate what the line does. |
| |
| Last comes the TypedArray, which is by far the least important since each try-with-resources |
| should only have 1. |
| */ |
| |
| // Note there is no variant of bool without a defaultValue parameter, since explicit true/false |
| // is important to specify when adding an attribute. |
| private static boolean bool(boolean defaultValue, @StyleableRes int attribute, TypedArray sa) { |
| return sa.getBoolean(attribute, defaultValue); |
| } |
| |
| private static float aFloat(float defaultValue, @StyleableRes int attribute, TypedArray sa) { |
| return sa.getFloat(attribute, defaultValue); |
| } |
| |
| private static float aFloat(@StyleableRes int attribute, TypedArray sa) { |
| return sa.getFloat(attribute, 0f); |
| } |
| |
| private static int anInt(int defaultValue, @StyleableRes int attribute, TypedArray sa) { |
| return sa.getInt(attribute, defaultValue); |
| } |
| |
| private static int anInt(@StyleableRes int attribute, TypedArray sa) { |
| return sa.getInt(attribute, 0); |
| } |
| |
| @AnyRes |
| private static int resId(@StyleableRes int attribute, TypedArray sa) { |
| return sa.getResourceId(attribute, 0); |
| } |
| |
| private static String string(@StyleableRes int attribute, TypedArray sa) { |
| return sa.getString(attribute); |
| } |
| |
| private static String nonConfigString(int allowedChangingConfigs, @StyleableRes int attribute, |
| TypedArray sa) { |
| return sa.getNonConfigurationString(attribute, allowedChangingConfigs); |
| } |
| |
| private static String nonResString(@StyleableRes int index, TypedArray sa) { |
| return sa.getNonResourceString(index); |
| } |
| |
| /** |
| * Callback interface for retrieving information that may be needed while parsing |
| * a package. |
| */ |
| public interface Callback { |
| boolean hasFeature(String feature); |
| |
| ParsingPackage startParsingPackage(String packageName, String baseCodePath, String codePath, |
| TypedArray manifestArray, boolean isCoreApp); |
| } |
| } |