diff --git a/aosp/frameworks/av/media/codec2/components/raw/C2SoftRawDec.cpp b/aosp/frameworks/av/media/codec2/components/raw/C2SoftRawDec.cpp new file mode 100644 index 0000000000000000000000000000000000000000..521001ee55580f8deef1cd0a8a6c47c49bd0a6d7 --- /dev/null +++ b/aosp/frameworks/av/media/codec2/components/raw/C2SoftRawDec.cpp @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2018 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. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "C2SoftRawDec" +#include + +#include + +#include +#include + +#include "C2SoftRawDec.h" + +namespace android { + +namespace { + +constexpr char COMPONENT_NAME[] = "c2.android.raw.decoder"; + +} // namespace + +class C2SoftRawDec::IntfImpl : public SimpleInterface::BaseParams { +public: + explicit IntfImpl(const std::shared_ptr &helper) + : SimpleInterface::BaseParams( + helper, + COMPONENT_NAME, + C2Component::KIND_DECODER, + C2Component::DOMAIN_AUDIO, + MEDIA_MIMETYPE_AUDIO_RAW) { + noPrivateBuffers(); + noInputReferences(); + noOutputReferences(); + noInputLatency(); + noTimeStretch(); + setDerivedInstance(this); + + addParameter( + DefineParam(mAttrib, C2_PARAMKEY_COMPONENT_ATTRIBUTES) + .withConstValue(new C2ComponentAttributesSetting( + C2Component::ATTRIB_IS_TEMPORAL)) + .build()); + + addParameter( + DefineParam(mSampleRate, C2_PARAMKEY_SAMPLE_RATE) + .withDefault(new C2StreamSampleRateInfo::output(0u, 44100)) + .withFields({C2F(mSampleRate, value).greaterThan(0)}) + .withSetter((Setter::StrictValueWithNoDeps)) + .build()); + + addParameter( + DefineParam(mChannelCount, C2_PARAMKEY_CHANNEL_COUNT) + .withDefault(new C2StreamChannelCountInfo::output(0u, 2)) + .withFields({C2F(mChannelCount, value).inRange(1, 12)}) + .withSetter(Setter::StrictValueWithNoDeps) + .build()); + + addParameter( + DefineParam(mBitrate, C2_PARAMKEY_BITRATE) + .withDefault(new C2StreamBitrateInfo::input(0u, 64000)) + .withFields({C2F(mBitrate, value).inRange(1, 98304000)}) + .withSetter(Setter::NonStrictValueWithNoDeps) + .build()); + + addParameter( + DefineParam(mInputMaxBufSize, C2_PARAMKEY_INPUT_MAX_BUFFER_SIZE) + .withConstValue(new C2StreamMaxBufferSizeInfo::input(0u, 64 * 1024)) + .build()); + + addParameter( + DefineParam(mPcmEncodingInfo, C2_PARAMKEY_PCM_ENCODING) + .withDefault(new C2StreamPcmEncodingInfo::output(0u, C2Config::PCM_16)) + .withFields({C2F(mPcmEncodingInfo, value).oneOf({ + C2Config::PCM_16, + C2Config::PCM_8, + C2Config::PCM_FLOAT, + C2Config::PCM_24, + C2Config::PCM_32}) + }) + .withSetter((Setter::StrictValueWithNoDeps)) + .build()); + + } + +private: + std::shared_ptr mSampleRate; + std::shared_ptr mChannelCount; + std::shared_ptr mBitrate; + std::shared_ptr mInputMaxBufSize; + std::shared_ptr mPcmEncodingInfo; +}; + +C2SoftRawDec::C2SoftRawDec( + const char *name, + c2_node_id_t id, + const std::shared_ptr &intfImpl) + : SimpleC2Component(std::make_shared>(name, id, intfImpl)), + mIntf(intfImpl) { +} + +C2SoftRawDec::~C2SoftRawDec() { + onRelease(); +} + +c2_status_t C2SoftRawDec::onInit() { + mSignalledEos = false; + return C2_OK; +} + +c2_status_t C2SoftRawDec::onStop() { + mSignalledEos = false; + return C2_OK; +} + +void C2SoftRawDec::onReset() { + (void)onStop(); +} + +void C2SoftRawDec::onRelease() { +} + +c2_status_t C2SoftRawDec::onFlush_sm() { + return onStop(); +} + +void C2SoftRawDec::process( + const std::unique_ptr &work, + const std::shared_ptr &pool) { + work->result = C2_OK; + work->workletsProcessed = 1u; + + if (mSignalledEos) { + work->result = C2_BAD_VALUE; + return; + } + + ALOGV("in buffer attr. timestamp %d frameindex %d", + (int)work->input.ordinal.timestamp.peeku(), (int)work->input.ordinal.frameIndex.peeku()); + + work->worklets.front()->output.flags = work->input.flags; + work->worklets.front()->output.buffers.clear(); + work->worklets.front()->output.ordinal = work->input.ordinal; + if (!work->input.buffers.empty()) { + C2ReadView rView = work->input.buffers[0]->data().linearBlocks().front().map().get(); + size_t inSize = rView.capacity(); + if (inSize && rView.error()) { + ALOGE("read view map failed %d", rView.error()); + work->result = C2_CORRUPTED; + return; + } + + if (inSize == 0) { + ALOGI("inSize is 0"); + work->result = C2_BAD_VALUE; + return; + } + + std::shared_ptr block; + C2MemoryUsage usage = { C2MemoryUsage::CPU_READ, C2MemoryUsage::CPU_WRITE }; + c2_status_t err = pool->fetchLinearBlock( + inSize, + usage, &block); + if (err != C2_OK) { + ALOGE("fetchLinearBlock for Output failed with status %d", err); + work->result = C2_NO_MEMORY; + return; + } + C2WriteView wView = block->map().get(); + if (wView.error()) { + ALOGE("write view map failed %d", wView.error()); + work->result = C2_CORRUPTED; + return; + } + + memcpy(wView.data(), rView.data(), inSize); + work->worklets.front()->output.buffers.push_back(createLinearBuffer(block, 0, inSize)); + } + if (work->input.flags & C2FrameData::FLAG_END_OF_STREAM) { + mSignalledEos = true; + ALOGV("signalled EOS"); + } +} + +c2_status_t C2SoftRawDec::drain( + uint32_t drainMode, + const std::shared_ptr &pool) { + (void) pool; + if (drainMode == NO_DRAIN) { + ALOGW("drain with NO_DRAIN: no-op"); + return C2_OK; + } + if (drainMode == DRAIN_CHAIN) { + ALOGW("DRAIN_CHAIN not supported"); + return C2_OMITTED; + } + + return C2_OK; +} + +class C2SoftRawDecFactory : public C2ComponentFactory { +public: + C2SoftRawDecFactory() : mHelper(std::static_pointer_cast( + GetCodec2PlatformComponentStore()->getParamReflector())) { + } + + virtual c2_status_t createComponent( + c2_node_id_t id, + std::shared_ptr* const component, + std::function deleter) override { + *component = std::shared_ptr( + new C2SoftRawDec(COMPONENT_NAME, + id, + std::make_shared(mHelper)), + deleter); + return C2_OK; + } + + virtual c2_status_t createInterface( + c2_node_id_t id, + std::shared_ptr* const interface, + std::function deleter) override { + *interface = std::shared_ptr( + new SimpleInterface( + COMPONENT_NAME, id, std::make_shared(mHelper)), + deleter); + return C2_OK; + } + + virtual ~C2SoftRawDecFactory() override = default; + +private: + std::shared_ptr mHelper; +}; + +} // namespace android + +__attribute__((cfi_canonical_jump_table)) +extern "C" ::C2ComponentFactory* CreateCodec2Factory() { + ALOGV("in %s", __func__); + return new ::android::C2SoftRawDecFactory(); +} + +__attribute__((cfi_canonical_jump_table)) +extern "C" void DestroyCodec2Factory(::C2ComponentFactory* factory) { + ALOGV("in %s", __func__); + delete factory; +} diff --git a/aosp/frameworks/base/core/java/android/content/pm/PackageParser.java b/aosp/frameworks/base/core/java/android/content/pm/PackageParser.java new file mode 100644 index 0000000000000000000000000000000000000000..d706bf0f74cb57d1cb4ec0d3ad4b77f0dc62a6d9 --- /dev/null +++ b/aosp/frameworks/base/core/java/android/content/pm/PackageParser.java @@ -0,0 +1,9336 @@ +/* + * Copyright (C) 2022 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; + +import static android.content.pm.ActivityInfo.FLAG_ALWAYS_FOCUSABLE; +import static android.content.pm.ActivityInfo.FLAG_SUPPORTS_PICTURE_IN_PICTURE; +import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY; +import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY; +import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION; +import static android.content.pm.ActivityInfo.RESIZE_MODE_FORCE_RESIZEABLE; +import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; +import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION; +import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.content.pm.ApplicationInfo.FLAG_SUSPENDED; +import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE; +import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION; +import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE; +import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; +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.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; +import static android.content.pm.PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; +import static android.os.Build.VERSION_CODES.O; +import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; +import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; + +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StringRes; +import android.apex.ApexInfo; +import android.app.ActivityTaskManager; +import android.app.ActivityThread; +import android.app.ResourcesManager; +import android.compat.annotation.UnsupportedAppUsage; +import android.content.ComponentName; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.overlay.OverlayPaths; +import android.content.pm.parsing.result.ParseResult; +import android.content.pm.parsing.result.ParseTypeImpl; +import android.content.pm.permission.SplitPermissionInfoParcelable; +import android.content.pm.pkg.FrameworkPackageUserState; +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.os.Build; +import android.os.Bundle; +import android.os.Debug; +import android.os.FileUtils; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PatternMatcher; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.os.Trace; +import android.os.UserHandle; +import android.os.storage.StorageManager; +import android.permission.PermissionManager; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.AttributeSet; +import android.util.Base64; +import android.util.DebugUtils; +import android.util.DisplayMetrics; +import android.util.IntArray; +import android.util.Log; +import android.util.PackageUtils; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArray; +import android.util.TypedValue; +import android.util.apk.ApkSignatureVerifier; +import android.view.Gravity; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +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 libcore.util.HexEncoding; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Parser for package files (APKs) on disk. This supports apps packaged either + * as a single "monolithic" APK, or apps packaged as a "cluster" of multiple + * APKs in a single directory. + *

+ * Apps packaged as multiple APKs always consist of a single "base" APK (with a + * {@code null} split name) and zero or more "split" APKs (with unique split + * names). Any subset of those split APKs are a valid install, as long as the + * following constraints are met: + *

    + *
  • All APKs must have the exact same package name, version code, and signing + * certificates. + *
  • All APKs must have unique split names. + *
  • All installations must contain a single base APK. + *
+ * + * @deprecated This class is mostly unused and no new changes should be added to it. Use + * ParsingPackageUtils and related parsing v2 infrastructure in + * the core/services parsing subpackages. Or for a quick parse of a provided APK, use + * {@link PackageManager#getPackageArchiveInfo(String, int)}. + * + * @hide + */ +@Deprecated +public class PackageParser { + + public static final boolean DEBUG_JAR = false; + public static final boolean DEBUG_PARSER = false; + public static final boolean DEBUG_BACKUP = false; + public static final boolean LOG_PARSE_TIMINGS = Build.IS_DEBUGGABLE; + public static final int LOG_PARSE_TIMINGS_THRESHOLD_MS = 100; + + private static final String PROPERTY_CHILD_PACKAGES_ENABLED = + "persist.sys.child_packages_enabled"; + + public static final boolean MULTI_PACKAGE_APK_ENABLED = Build.IS_DEBUGGABLE && + SystemProperties.getBoolean(PROPERTY_CHILD_PACKAGES_ENABLED, false); + + public static final float DEFAULT_PRE_O_MAX_ASPECT_RATIO = 1.86f; + + private static final int DEFAULT_MIN_SDK_VERSION = 1; + private static final int DEFAULT_TARGET_SDK_VERSION = 0; + + // TODO: switch outError users to PackageParserException + // TODO: refactor "codePath" to "apkPath" + + /** File name in an APK for the Android manifest. */ + public static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml"; + + /** Path prefix for apps on expanded storage */ + public static final String MNT_EXPAND = "/mnt/expand/"; + + public static final String TAG_ADOPT_PERMISSIONS = "adopt-permissions"; + public static final String TAG_APPLICATION = "application"; + public static final String TAG_COMPATIBLE_SCREENS = "compatible-screens"; + public static final String TAG_EAT_COMMENT = "eat-comment"; + public static final String TAG_FEATURE_GROUP = "feature-group"; + public static final String TAG_INSTRUMENTATION = "instrumentation"; + public static final String TAG_KEY_SETS = "key-sets"; + public static final String TAG_MANIFEST = "manifest"; + public static final String TAG_ORIGINAL_PACKAGE = "original-package"; + public static final String TAG_OVERLAY = "overlay"; + public static final String TAG_PACKAGE = "package"; + public static final String TAG_PACKAGE_VERIFIER = "package-verifier"; + public static final String TAG_ATTRIBUTION = "attribution"; + public static final String TAG_PERMISSION = "permission"; + public static final String TAG_PERMISSION_GROUP = "permission-group"; + public static final String TAG_PERMISSION_TREE = "permission-tree"; + public static final String TAG_PROTECTED_BROADCAST = "protected-broadcast"; + public static final String TAG_QUERIES = "queries"; + public static final String TAG_RESTRICT_UPDATE = "restrict-update"; + public static final String TAG_SUPPORT_SCREENS = "supports-screens"; + public static final String TAG_SUPPORTS_INPUT = "supports-input"; + public static final String TAG_USES_CONFIGURATION = "uses-configuration"; + public static final String TAG_USES_FEATURE = "uses-feature"; + public static final String TAG_USES_GL_TEXTURE = "uses-gl-texture"; + public static final String TAG_USES_PERMISSION = "uses-permission"; + public static final String TAG_USES_PERMISSION_SDK_23 = "uses-permission-sdk-23"; + public static final String TAG_USES_PERMISSION_SDK_M = "uses-permission-sdk-m"; + public static final String TAG_USES_SDK = "uses-sdk"; + public static final String TAG_USES_SPLIT = "uses-split"; + public static final String TAG_PROFILEABLE = "profileable"; + + public static final String METADATA_MAX_ASPECT_RATIO = "android.max_aspect"; + public static final String METADATA_SUPPORTS_SIZE_CHANGES = "android.supports_size_changes"; + public static final String METADATA_ACTIVITY_WINDOW_LAYOUT_AFFINITY = + "android.activity_window_layout_affinity"; + + /** + * Bit mask of all the valid bits that can be set in recreateOnConfigChanges. + * @hide + */ + private static final int RECREATE_ON_CONFIG_CHANGES_MASK = + ActivityInfo.CONFIG_MCC | ActivityInfo.CONFIG_MNC; + + // These are the tags supported by child packages + public static final Set CHILD_PACKAGE_TAGS = new ArraySet<>(); + static { + CHILD_PACKAGE_TAGS.add(TAG_APPLICATION); + CHILD_PACKAGE_TAGS.add(TAG_COMPATIBLE_SCREENS); + CHILD_PACKAGE_TAGS.add(TAG_EAT_COMMENT); + CHILD_PACKAGE_TAGS.add(TAG_FEATURE_GROUP); + CHILD_PACKAGE_TAGS.add(TAG_INSTRUMENTATION); + CHILD_PACKAGE_TAGS.add(TAG_SUPPORT_SCREENS); + CHILD_PACKAGE_TAGS.add(TAG_SUPPORTS_INPUT); + CHILD_PACKAGE_TAGS.add(TAG_USES_CONFIGURATION); + CHILD_PACKAGE_TAGS.add(TAG_USES_FEATURE); + CHILD_PACKAGE_TAGS.add(TAG_USES_GL_TEXTURE); + CHILD_PACKAGE_TAGS.add(TAG_USES_PERMISSION); + CHILD_PACKAGE_TAGS.add(TAG_USES_PERMISSION_SDK_23); + CHILD_PACKAGE_TAGS.add(TAG_USES_PERMISSION_SDK_M); + CHILD_PACKAGE_TAGS.add(TAG_USES_SDK); + } + + public static final boolean LOG_UNSAFE_BROADCASTS = false; + + // Set of broadcast actions that are safe for manifest receivers + public static final Set SAFE_BROADCASTS = new ArraySet<>(); + static { + SAFE_BROADCASTS.add(Intent.ACTION_BOOT_COMPLETED); + } + + /** @hide */ + public static final String APK_FILE_EXTENSION = ".apk"; + /** @hide */ + public static final String APEX_FILE_EXTENSION = ".apex"; + + /** @hide */ + public static class NewPermissionInfo { + @UnsupportedAppUsage + public final String name; + @UnsupportedAppUsage + public final int sdkVersion; + public final int fileVersion; + + public NewPermissionInfo(String name, int sdkVersion, int fileVersion) { + this.name = name; + this.sdkVersion = sdkVersion; + this.fileVersion = fileVersion; + } + } + + /** + * List of new permissions that have been added since 1.0. + * NOTE: These must be declared in SDK version order, with permissions + * added to older SDKs appearing before those added to newer SDKs. + * If sdkVersion is 0, then this is not a permission that we want to + * automatically add to older apps, but we do want to allow it to be + * granted during a platform update. + * @hide + */ + @UnsupportedAppUsage + public static final PackageParser.NewPermissionInfo NEW_PERMISSIONS[] = + new PackageParser.NewPermissionInfo[] { + new PackageParser.NewPermissionInfo(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + android.os.Build.VERSION_CODES.DONUT, 0), + new PackageParser.NewPermissionInfo(android.Manifest.permission.READ_PHONE_STATE, + android.os.Build.VERSION_CODES.DONUT, 0) + }; + + /** + * @deprecated callers should move to explicitly passing around source path. + */ + @Deprecated + public String mArchiveSourcePath; + + public String[] mSeparateProcesses; + private boolean mOnlyCoreApps; + private DisplayMetrics mMetrics; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public Callback mCallback; + private File mCacheDir; + + public static final int SDK_VERSION = Build.VERSION.SDK_INT; + public static final String[] SDK_CODENAMES = Build.VERSION.ACTIVE_CODENAMES; + + public int mParseError = PackageManager.INSTALL_SUCCEEDED; + + public static boolean sCompatibilityModeEnabled = true; + public static boolean sUseRoundIcon = false; + + public static final int PARSE_DEFAULT_INSTALL_LOCATION = + PackageInfo.INSTALL_LOCATION_UNSPECIFIED; + public static final int PARSE_DEFAULT_TARGET_SANDBOX = 1; + + static class ParsePackageItemArgs { + final Package owner; + final String[] outError; + final int nameRes; + final int labelRes; + final int iconRes; + final int roundIconRes; + final int logoRes; + final int bannerRes; + + String tag; + TypedArray sa; + + ParsePackageItemArgs(Package _owner, String[] _outError, + int _nameRes, int _labelRes, int _iconRes, int _roundIconRes, int _logoRes, + int _bannerRes) { + owner = _owner; + outError = _outError; + nameRes = _nameRes; + labelRes = _labelRes; + iconRes = _iconRes; + logoRes = _logoRes; + bannerRes = _bannerRes; + roundIconRes = _roundIconRes; + } + } + + /** @hide */ + @VisibleForTesting + public static class ParseComponentArgs extends ParsePackageItemArgs { + final String[] sepProcesses; + final int processRes; + final int descriptionRes; + final int enabledRes; + int flags; + + public ParseComponentArgs(Package _owner, String[] _outError, + int _nameRes, int _labelRes, int _iconRes, int _roundIconRes, int _logoRes, + int _bannerRes, + String[] _sepProcesses, int _processRes, + int _descriptionRes, int _enabledRes) { + super(_owner, _outError, _nameRes, _labelRes, _iconRes, _roundIconRes, _logoRes, + _bannerRes); + sepProcesses = _sepProcesses; + processRes = _processRes; + descriptionRes = _descriptionRes; + enabledRes = _enabledRes; + } + } + + /** + * Lightweight parsed details about a single package. + */ + public static class PackageLite { + @UnsupportedAppUsage + public final String packageName; + public final int versionCode; + public final int versionCodeMajor; + @UnsupportedAppUsage + public final int installLocation; + public final VerifierInfo[] verifiers; + + /** Names of any split APKs, ordered by parsed splitName */ + public final String[] splitNames; + + /** Names of any split APKs that are features. Ordered by splitName */ + public final boolean[] isFeatureSplits; + + /** Dependencies of any split APKs, ordered by parsed splitName */ + public final String[] usesSplitNames; + public final String[] configForSplit; + + /** + * Path where this package was found on disk. For monolithic packages + * this is path to single base APK file; for cluster packages this is + * path to the cluster directory. + */ + public final String codePath; + + /** Path of base APK */ + public final String baseCodePath; + /** Paths of any split APKs, ordered by parsed splitName */ + public final String[] splitCodePaths; + + /** Revision code of base APK */ + public final int baseRevisionCode; + /** Revision codes of any split APKs, ordered by parsed splitName */ + public final int[] splitRevisionCodes; + + public final boolean coreApp; + public final boolean debuggable; + public final boolean multiArch; + public final boolean use32bitAbi; + public final boolean extractNativeLibs; + public final boolean isolatedSplits; + + // This does not represent the actual manifest structure since the 'profilable' tag + // could be used with attributes other than 'shell'. Extend if necessary. + public final boolean profilableByShell; + public final boolean isSplitRequired; + public final boolean useEmbeddedDex; + + public PackageLite(String codePath, String baseCodePath, ApkLite baseApk, + String[] splitNames, boolean[] isFeatureSplits, String[] usesSplitNames, + String[] configForSplit, String[] splitCodePaths, int[] splitRevisionCodes) { + this.packageName = baseApk.packageName; + this.versionCode = baseApk.versionCode; + this.versionCodeMajor = baseApk.versionCodeMajor; + this.installLocation = baseApk.installLocation; + this.verifiers = baseApk.verifiers; + this.splitNames = splitNames; + this.isFeatureSplits = isFeatureSplits; + this.usesSplitNames = usesSplitNames; + this.configForSplit = configForSplit; + // The following paths may be different from the path in ApkLite because we + // move or rename the APK files. Use parameters to indicate the correct paths. + this.codePath = codePath; + this.baseCodePath = baseCodePath; + this.splitCodePaths = splitCodePaths; + this.baseRevisionCode = baseApk.revisionCode; + this.splitRevisionCodes = splitRevisionCodes; + this.coreApp = baseApk.coreApp; + this.debuggable = baseApk.debuggable; + this.multiArch = baseApk.multiArch; + this.use32bitAbi = baseApk.use32bitAbi; + this.extractNativeLibs = baseApk.extractNativeLibs; + this.isolatedSplits = baseApk.isolatedSplits; + this.useEmbeddedDex = baseApk.useEmbeddedDex; + this.isSplitRequired = baseApk.isSplitRequired; + this.profilableByShell = baseApk.profilableByShell; + } + + public List getAllCodePaths() { + ArrayList paths = new ArrayList<>(); + paths.add(baseCodePath); + if (!ArrayUtils.isEmpty(splitCodePaths)) { + Collections.addAll(paths, splitCodePaths); + } + return paths; + } + + public long getLongVersionCode() { + return PackageInfo.composeLongVersionCode(versionCodeMajor, versionCode); + } + } + + /** + * Lightweight parsed details about a single APK file. + */ + public static class ApkLite { + public final String codePath; + public final String packageName; + public final String splitName; + public boolean isFeatureSplit; + public final String configForSplit; + public final String usesSplitName; + public final int versionCode; + public final int versionCodeMajor; + public final int revisionCode; + public final int installLocation; + public final int minSdkVersion; + public final int targetSdkVersion; + public final VerifierInfo[] verifiers; + public final SigningDetails signingDetails; + public final boolean coreApp; + public final boolean debuggable; + // This does not represent the actual manifest structure since the 'profilable' tag + // could be used with attributes other than 'shell'. Extend if necessary. + public final boolean profilableByShell; + public final boolean multiArch; + public final boolean use32bitAbi; + public final boolean extractNativeLibs; + public final boolean isolatedSplits; + public final boolean isSplitRequired; + public final boolean useEmbeddedDex; + public final String targetPackageName; + public final boolean overlayIsStatic; + public final int overlayPriority; + public final int rollbackDataPolicy; + + public ApkLite(String codePath, String packageName, String splitName, + boolean isFeatureSplit, + String configForSplit, String usesSplitName, boolean isSplitRequired, + int versionCode, int versionCodeMajor, + int revisionCode, int installLocation, List verifiers, + SigningDetails signingDetails, boolean coreApp, + boolean debuggable, boolean profilableByShell, boolean multiArch, + boolean use32bitAbi, boolean useEmbeddedDex, boolean extractNativeLibs, + boolean isolatedSplits, String targetPackageName, boolean overlayIsStatic, + int overlayPriority, int minSdkVersion, int targetSdkVersion, + int rollbackDataPolicy) { + this.codePath = codePath; + this.packageName = packageName; + this.splitName = splitName; + this.isFeatureSplit = isFeatureSplit; + this.configForSplit = configForSplit; + this.usesSplitName = usesSplitName; + this.versionCode = versionCode; + this.versionCodeMajor = versionCodeMajor; + this.revisionCode = revisionCode; + this.installLocation = installLocation; + this.signingDetails = signingDetails; + this.verifiers = verifiers.toArray(new VerifierInfo[verifiers.size()]); + this.coreApp = coreApp; + this.debuggable = debuggable; + this.profilableByShell = profilableByShell; + this.multiArch = multiArch; + this.use32bitAbi = use32bitAbi; + this.useEmbeddedDex = useEmbeddedDex; + this.extractNativeLibs = extractNativeLibs; + this.isolatedSplits = isolatedSplits; + this.isSplitRequired = isSplitRequired; + this.targetPackageName = targetPackageName; + this.overlayIsStatic = overlayIsStatic; + this.overlayPriority = overlayPriority; + this.minSdkVersion = minSdkVersion; + this.targetSdkVersion = targetSdkVersion; + this.rollbackDataPolicy = rollbackDataPolicy; + } + + public long getLongVersionCode() { + return PackageInfo.composeLongVersionCode(versionCodeMajor, versionCode); + } + } + + /** + * Cached parse state for new components. + * + * Allows reuse of the same parse argument records to avoid GC pressure. Lifetime is carefully + * scoped to the parsing of a single application element. + */ + private static class CachedComponentArgs { + ParseComponentArgs mActivityArgs; + ParseComponentArgs mActivityAliasArgs; + ParseComponentArgs mServiceArgs; + ParseComponentArgs mProviderArgs; + } + + /** + * Cached state for parsing instrumentation to avoid GC pressure. + * + * Must be manually reset to null for each new manifest. + */ + private ParsePackageItemArgs mParseInstrumentationArgs; + + /** If set to true, we will only allow package files that exactly match + * the DTD. Otherwise, we try to get as much from the package as we + * can without failing. This should normally be set to false, to + * support extensions to the DTD in future versions. */ + public static final boolean RIGID_PARSER = false; + + private static final String TAG = "PackageParser"; + + @UnsupportedAppUsage + public PackageParser() { + mMetrics = new DisplayMetrics(); + mMetrics.setToDefaults(); + } + + @UnsupportedAppUsage + public void setSeparateProcesses(String[] procs) { + mSeparateProcesses = procs; + } + + /** + * Flag indicating this parser should only consider apps with + * {@code coreApp} manifest attribute to be valid apps. This is useful when + * creating a minimalist boot environment. + */ + public void setOnlyCoreApps(boolean onlyCoreApps) { + mOnlyCoreApps = onlyCoreApps; + } + + public void setDisplayMetrics(DisplayMetrics metrics) { + mMetrics = metrics; + } + + /** + * Sets the cache directory for this package parser. + */ + public void setCacheDir(File cacheDir) { + mCacheDir = cacheDir; + } + + /** + * Callback interface for retrieving information that may be needed while parsing + * a package. + */ + public interface Callback { + boolean hasFeature(String feature); + } + + /** + * Standard implementation of {@link Callback} on top of the public {@link PackageManager} + * class. + */ + public static final class CallbackImpl implements Callback { + private final PackageManager mPm; + + public CallbackImpl(PackageManager pm) { + mPm = pm; + } + + @Override public boolean hasFeature(String feature) { + return mPm.hasSystemFeature(feature); + } + } + + /** + * Set the {@link Callback} that can be used while parsing. + */ + public void setCallback(Callback cb) { + mCallback = cb; + } + + public static final boolean isApkFile(File file) { + return isApkPath(file.getName()); + } + + public static boolean isApkPath(String path) { + return path.endsWith(APK_FILE_EXTENSION); + } + + /** + * Returns true if the package is installed and not hidden, or if the caller + * explicitly wanted all uninstalled and hidden packages as well. + * @param appInfo The applicationInfo of the app being checked. + */ + private static boolean checkUseInstalledOrHidden(int flags, FrameworkPackageUserState state, + ApplicationInfo appInfo) { + // Returns false if the package is hidden system app until installed. + if ((flags & PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS) == 0 + && !state.isInstalled() + && appInfo != null && appInfo.hiddenUntilInstalled) { + return false; + } + + // If available for the target user, or trying to match uninstalled packages and it's + // a system app. + return isAvailable(state, flags) + || (appInfo != null && appInfo.isSystemApp() + && ((flags & PackageManager.MATCH_KNOWN_PACKAGES) != 0 + || (flags & PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS) != 0)); + } + + public static boolean isAvailable(FrameworkPackageUserState state) { + return checkUseInstalledOrHidden(0, state, null); + } + + /** + * Generate and return the {@link PackageInfo} for a parsed package. + * + * @param p the parsed package. + * @param flags indicating which optional information is included. + */ + @UnsupportedAppUsage + public static PackageInfo generatePackageInfo(PackageParser.Package p, + int[] gids, int flags, long firstInstallTime, long lastUpdateTime, + Set grantedPermissions, FrameworkPackageUserState state) { + + return generatePackageInfo(p, gids, flags, firstInstallTime, lastUpdateTime, + grantedPermissions, state, UserHandle.getCallingUserId()); + } + + @UnsupportedAppUsage + public static PackageInfo generatePackageInfo(PackageParser.Package p, + int[] gids, int flags, long firstInstallTime, long lastUpdateTime, + Set grantedPermissions, FrameworkPackageUserState state, int userId) { + + return generatePackageInfo(p, null, gids, flags, firstInstallTime, lastUpdateTime, + grantedPermissions, state, userId); + } + + /** + * PackageInfo generator specifically for apex files. + * + * @param pkg Package to generate info from. Should be derived from an apex. + * @param apexInfo Apex info relating to the package. + * @return PackageInfo + * @throws PackageParserException + */ + public static PackageInfo generatePackageInfo( + PackageParser.Package pkg, ApexInfo apexInfo, int flags) { + return generatePackageInfo(pkg, apexInfo, EmptyArray.INT, flags, 0, 0, + Collections.emptySet(), FrameworkPackageUserState.DEFAULT, UserHandle.getCallingUserId()); + } + + private static PackageInfo generatePackageInfo(PackageParser.Package p, ApexInfo apexInfo, + int gids[], int flags, long firstInstallTime, long lastUpdateTime, + Set grantedPermissions, FrameworkPackageUserState state, int userId) { + if (!checkUseInstalledOrHidden(flags, state, p.applicationInfo) || !p.isMatch(flags)) { + return null; + } + + final ApplicationInfo applicationInfo; + if ((flags & (PackageManager.GET_ACTIVITIES | PackageManager.GET_RECEIVERS + | PackageManager.GET_SERVICES | PackageManager.GET_PROVIDERS)) != 0) { + applicationInfo = generateApplicationInfo(p, flags, state, userId); + } else { + applicationInfo = null; + } + + PackageInfo pi = new PackageInfo(); + pi.packageName = p.packageName; + pi.splitNames = p.splitNames; + pi.versionCode = p.mVersionCode; + pi.versionCodeMajor = p.mVersionCodeMajor; + pi.baseRevisionCode = p.baseRevisionCode; + pi.splitRevisionCodes = p.splitRevisionCodes; + pi.versionName = p.mVersionName; + pi.sharedUserId = p.mSharedUserId; + pi.sharedUserLabel = p.mSharedUserLabel; + pi.applicationInfo = generateApplicationInfo(p, flags, state, userId); + pi.installLocation = p.installLocation; + pi.isStub = p.isStub; + pi.coreApp = p.coreApp; + if ((pi.applicationInfo.flags&ApplicationInfo.FLAG_SYSTEM) != 0 + || (pi.applicationInfo.flags&ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { + pi.requiredForAllUsers = p.mRequiredForAllUsers; + } + pi.restrictedAccountType = p.mRestrictedAccountType; + pi.requiredAccountType = p.mRequiredAccountType; + pi.overlayTarget = p.mOverlayTarget; + pi.targetOverlayableName = p.mOverlayTargetName; + pi.overlayCategory = p.mOverlayCategory; + pi.overlayPriority = p.mOverlayPriority; + pi.mOverlayIsStatic = p.mOverlayIsStatic; + pi.compileSdkVersion = p.mCompileSdkVersion; + pi.compileSdkVersionCodename = p.mCompileSdkVersionCodename; + pi.firstInstallTime = firstInstallTime; + pi.lastUpdateTime = lastUpdateTime; + if ((flags&PackageManager.GET_GIDS) != 0) { + pi.gids = gids; + } + if ((flags&PackageManager.GET_CONFIGURATIONS) != 0) { + int N = p.configPreferences != null ? p.configPreferences.size() : 0; + if (N > 0) { + pi.configPreferences = new ConfigurationInfo[N]; + p.configPreferences.toArray(pi.configPreferences); + } + N = p.reqFeatures != null ? p.reqFeatures.size() : 0; + if (N > 0) { + pi.reqFeatures = new FeatureInfo[N]; + p.reqFeatures.toArray(pi.reqFeatures); + } + N = p.featureGroups != null ? p.featureGroups.size() : 0; + if (N > 0) { + pi.featureGroups = new FeatureGroupInfo[N]; + p.featureGroups.toArray(pi.featureGroups); + } + } + if ((flags & PackageManager.GET_ACTIVITIES) != 0) { + final int N = p.activities.size(); + if (N > 0) { + int num = 0; + final ActivityInfo[] res = new ActivityInfo[N]; + for (int i = 0; i < N; i++) { + final Activity a = p.activities.get(i); + if (isMatch(state, a.info, flags)) { + if (PackageManager.APP_DETAILS_ACTIVITY_CLASS_NAME.equals(a.className)) { + continue; + } + res[num++] = generateActivityInfo(a, flags, state, userId, applicationInfo); + } + } + pi.activities = ArrayUtils.trimToSize(res, num); + } + } + if ((flags & PackageManager.GET_RECEIVERS) != 0) { + final int N = p.receivers.size(); + if (N > 0) { + int num = 0; + final ActivityInfo[] res = new ActivityInfo[N]; + for (int i = 0; i < N; i++) { + final Activity a = p.receivers.get(i); + if (isMatch(state, a.info, flags)) { + res[num++] = generateActivityInfo(a, flags, state, userId, applicationInfo); + } + } + pi.receivers = ArrayUtils.trimToSize(res, num); + } + } + if ((flags & PackageManager.GET_SERVICES) != 0) { + final int N = p.services.size(); + if (N > 0) { + int num = 0; + final ServiceInfo[] res = new ServiceInfo[N]; + for (int i = 0; i < N; i++) { + final Service s = p.services.get(i); + if (isMatch(state, s.info, flags)) { + res[num++] = generateServiceInfo(s, flags, state, userId, applicationInfo); + } + } + pi.services = ArrayUtils.trimToSize(res, num); + } + } + if ((flags & PackageManager.GET_PROVIDERS) != 0) { + final int N = p.providers.size(); + if (N > 0) { + int num = 0; + final ProviderInfo[] res = new ProviderInfo[N]; + for (int i = 0; i < N; i++) { + final Provider pr = p.providers.get(i); + if (isMatch(state, pr.info, flags)) { + res[num++] = generateProviderInfo(pr, flags, state, userId, + applicationInfo); + } + } + pi.providers = ArrayUtils.trimToSize(res, num); + } + } + if ((flags&PackageManager.GET_INSTRUMENTATION) != 0) { + int N = p.instrumentation.size(); + if (N > 0) { + pi.instrumentation = new InstrumentationInfo[N]; + for (int i=0; i 0) { + pi.permissions = new PermissionInfo[N]; + for (int i=0; i 0) { + pi.requestedPermissions = new String[N]; + pi.requestedPermissionsFlags = new int[N]; + for (int i=0; i sSplitNameComparator = new SplitNameComparator(); + + /** + * Used to sort a set of APKs based on their split names, always placing the + * base APK (with {@code null} split name) first. + */ + private static class SplitNameComparator implements Comparator { + @Override + public int compare(String lhs, String rhs) { + if (lhs == null) { + return -1; + } else if (rhs == null) { + return 1; + } else { + return lhs.compareTo(rhs); + } + } + } + + /** + * Parse only lightweight details about the package at the given location. + * Automatically detects if the package is a monolithic style (single APK + * file) or cluster style (directory of APKs). + *

+ * This performs checking on cluster style packages, such as + * requiring identical package name and version codes, a single base APK, + * and unique split names. + * + * @see PackageParser#parsePackage(File, int) + */ + @UnsupportedAppUsage + public static PackageLite parsePackageLite(File packageFile, int flags) + throws PackageParserException { + if (packageFile.isDirectory()) { + return parseClusterPackageLite(packageFile, flags); + } else { + return parseMonolithicPackageLite(packageFile, flags); + } + } + + private static PackageLite parseMonolithicPackageLite(File packageFile, int flags) + throws PackageParserException { + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parseApkLite"); + final ApkLite baseApk = parseApkLite(packageFile, flags); + final String packagePath = packageFile.getAbsolutePath(); + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + return new PackageLite(packagePath, baseApk.codePath, baseApk, null, null, null, null, null, + null); + } + + static PackageLite parseClusterPackageLite(File packageDir, int flags) + throws PackageParserException { + final File[] files = packageDir.listFiles(); + if (ArrayUtils.isEmpty(files)) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK, + "No packages found in split"); + } + // Apk directory is directly nested under the current directory + if (files.length == 1 && files[0].isDirectory()) { + return parseClusterPackageLite(files[0], flags); + } + + String packageName = null; + int versionCode = 0; + + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parseApkLite"); + final ArrayMap apks = new ArrayMap<>(); + for (File file : files) { + if (isApkFile(file)) { + final ApkLite lite = parseApkLite(file, flags); + + // Assert that all package names and version codes are + // consistent with the first one we encounter. + if (packageName == null) { + packageName = lite.packageName; + versionCode = lite.versionCode; + } else { + if (!packageName.equals(lite.packageName)) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Inconsistent package " + lite.packageName + " in " + file + + "; expected " + packageName); + } + if (versionCode != lite.versionCode) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Inconsistent version " + lite.versionCode + " in " + file + + "; expected " + versionCode); + } + } + + // Assert that each split is defined only once + if (apks.put(lite.splitName, lite) != null) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Split name " + lite.splitName + + " defined more than once; most recent was " + file); + } + } + } + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + + final ApkLite baseApk = apks.remove(null); + if (baseApk == null) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Missing base APK in " + packageDir); + } + + // Always apply deterministic ordering based on splitName + final int size = apks.size(); + + String[] splitNames = null; + boolean[] isFeatureSplits = null; + String[] usesSplitNames = null; + String[] configForSplits = null; + String[] splitCodePaths = null; + int[] splitRevisionCodes = null; + String[] splitClassLoaderNames = null; + if (size > 0) { + splitNames = new String[size]; + isFeatureSplits = new boolean[size]; + usesSplitNames = new String[size]; + configForSplits = new String[size]; + splitCodePaths = new String[size]; + splitRevisionCodes = new int[size]; + + splitNames = apks.keySet().toArray(splitNames); + Arrays.sort(splitNames, sSplitNameComparator); + + for (int i = 0; i < size; i++) { + final ApkLite apk = apks.get(splitNames[i]); + usesSplitNames[i] = apk.usesSplitName; + isFeatureSplits[i] = apk.isFeatureSplit; + configForSplits[i] = apk.configForSplit; + splitCodePaths[i] = apk.codePath; + splitRevisionCodes[i] = apk.revisionCode; + } + } + + final String codePath = packageDir.getAbsolutePath(); + return new PackageLite(codePath, baseApk.codePath, baseApk, splitNames, isFeatureSplits, + usesSplitNames, configForSplits, splitCodePaths, splitRevisionCodes); + } + + /** + * 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). + *

+ * This performs checking on cluster style packages, such as + * requiring identical package name and version codes, a single base APK, + * and unique split names. + *

+ * Note that this does not perform signature verification; that + * must be done separately in {@link #collectCertificates(Package, 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 #parsePackageLite(File, int) + */ + @UnsupportedAppUsage + public Package parsePackage(File packageFile, int flags, boolean useCaches) + throws PackageParserException { + if (packageFile.isDirectory()) { + return parseClusterPackage(packageFile, flags); + } else { + return parseMonolithicPackage(packageFile, flags); + } + } + + /** + * Equivalent to {@link #parsePackage(File, int, boolean)} with {@code useCaches == false}. + */ + @UnsupportedAppUsage + public Package parsePackage(File packageFile, int flags) throws PackageParserException { + return parsePackage(packageFile, flags, false /* useCaches */); + } + + /** + * Parse all APKs contained in the given directory, treating them as a + * single package. This also performs checking, such as requiring + * identical package name and version codes, a single base APK, and unique + * split names. + *

+ * Note that this does not perform signature verification; that + * must be done separately in + * {@link #collectCertificates(Package, boolean)} . + */ + private Package parseClusterPackage(File packageDir, int flags) throws PackageParserException { + final PackageLite lite = parseClusterPackageLite(packageDir, 0); + if (mOnlyCoreApps && !lite.coreApp) { + throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + "Not a coreApp: " + packageDir); + } + + // Build the split dependency tree. + SparseArray 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) { + throw new PackageParserException(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); + final Package pkg = parseBaseApk(baseApk, assets, flags); + if (pkg == null) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK, + "Failed to parse base APK: " + baseApk); + } + + if (!ArrayUtils.isEmpty(lite.splitNames)) { + final int num = lite.splitNames.length; + pkg.splitNames = lite.splitNames; + pkg.splitCodePaths = lite.splitCodePaths; + pkg.splitRevisionCodes = lite.splitRevisionCodes; + pkg.splitFlags = new int[num]; + pkg.splitPrivateFlags = new int[num]; + pkg.applicationInfo.splitNames = pkg.splitNames; + pkg.applicationInfo.splitDependencies = splitDependencies; + pkg.applicationInfo.splitClassLoaderNames = new String[num]; + + for (int i = 0; i < num; i++) { + final AssetManager splitAssets = assetLoader.getSplitAssetManager(i); + parseSplitApk(pkg, i, splitAssets, flags); + } + } + + pkg.setCodePath(lite.codePath); + pkg.setUse32bitAbi(lite.use32bitAbi); + return pkg; + } finally { + IoUtils.closeQuietly(assetLoader); + } + } + + /** + * Parse the given APK file, treating it as as a single monolithic package. + *

+ * Note that this does not perform signature verification; that + * must be done separately in + * {@link #collectCertificates(Package, boolean)}. + */ + @UnsupportedAppUsage + public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException { + final PackageLite lite = parseMonolithicPackageLite(apkFile, flags); + if (mOnlyCoreApps) { + if (!lite.coreApp) { + throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + "Not a coreApp: " + apkFile); + } + } + + final SplitAssetLoader assetLoader = new DefaultSplitAssetLoader(lite, flags); + try { + final Package pkg = parseBaseApk(apkFile, assetLoader.getBaseAssetManager(), flags); + pkg.setCodePath(apkFile.getCanonicalPath()); + pkg.setUse32bitAbi(lite.use32bitAbi); + return pkg; + } catch (IOException e) { + throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "Failed to get path: " + apkFile, e); + } finally { + IoUtils.closeQuietly(assetLoader); + } + } + + private Package parseBaseApk(File apkFile, AssetManager assets, int flags) + throws PackageParserException { + final String apkPath = apkFile.getAbsolutePath(); + + String volumeUuid = null; + if (apkPath.startsWith(MNT_EXPAND)) { + final int end = apkPath.indexOf('/', MNT_EXPAND.length()); + volumeUuid = apkPath.substring(MNT_EXPAND.length(), end); + } + + mParseError = PackageManager.INSTALL_SUCCEEDED; + mArchiveSourcePath = apkFile.getAbsolutePath(); + + if (DEBUG_JAR) Slog.d(TAG, "Scanning base APK: " + apkPath); + + XmlResourceParser parser = null; + try { + final int cookie = assets.findCookieForPath(apkPath); + if (cookie == 0) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Failed adding asset path: " + apkPath); + } + parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME); + final Resources res = new Resources(assets, mMetrics, null); + + final String[] outError = new String[1]; + final Package pkg = parseBaseApk(apkPath, res, parser, flags, outError); + if (pkg == null) { + throw new PackageParserException(mParseError, + apkPath + " (at " + parser.getPositionDescription() + "): " + outError[0]); + } + + pkg.setVolumeUuid(volumeUuid); + pkg.setApplicationVolumeUuid(volumeUuid); + pkg.setBaseCodePath(apkPath); + pkg.setSigningDetails(SigningDetails.UNKNOWN); + + return pkg; + + } catch (PackageParserException e) { + throw e; + } catch (Exception e) { + throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "Failed to read manifest from " + apkPath, e); + } finally { + IoUtils.closeQuietly(parser); + } + } + + private void parseSplitApk(Package pkg, int splitIndex, AssetManager assets, int flags) + throws PackageParserException { + final String apkPath = pkg.splitCodePaths[splitIndex]; + + mParseError = PackageManager.INSTALL_SUCCEEDED; + mArchiveSourcePath = apkPath; + + if (DEBUG_JAR) Slog.d(TAG, "Scanning split APK: " + apkPath); + + final Resources res; + XmlResourceParser parser = null; + try { + // This must always succeed, as the path has been added to the AssetManager before. + final int cookie = assets.findCookieForPath(apkPath); + if (cookie == 0) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Failed adding asset path: " + apkPath); + } + + parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME); + res = new Resources(assets, mMetrics, null); + + final String[] outError = new String[1]; + pkg = parseSplitApk(pkg, res, parser, flags, splitIndex, outError); + if (pkg == null) { + throw new PackageParserException(mParseError, + apkPath + " (at " + parser.getPositionDescription() + "): " + outError[0]); + } + + } catch (PackageParserException e) { + throw e; + } catch (Exception e) { + throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "Failed to read manifest from " + apkPath, e); + } finally { + IoUtils.closeQuietly(parser); + } + } + + /** + * Parse the manifest of a split APK. + *

+ * 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 Package parseSplitApk(Package pkg, Resources res, XmlResourceParser parser, int flags, + int splitIndex, String[] outError) throws XmlPullParserException, IOException, + PackageParserException { + AttributeSet attrs = parser; + + // We parsed manifest tag earlier; just skip past it + parsePackageSplitNames(parser, attrs); + + mParseInstrumentationArgs = null; + + int type; + + boolean foundApp = false; + + int outerDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals(TAG_APPLICATION)) { + if (foundApp) { + if (RIGID_PARSER) { + outError[0] = " has more than one "; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } else { + Slog.w(TAG, " has more than one "); + XmlUtils.skipCurrentTag(parser); + continue; + } + } + + foundApp = true; + if (!parseSplitApplication(pkg, res, parser, flags, splitIndex, outError)) { + return null; + } + + } else if (RIGID_PARSER) { + outError[0] = "Bad element under : " + + parser.getName(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + + } else { + Slog.w(TAG, "Unknown element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } + } + + if (!foundApp) { + outError[0] = " does not contain an "; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY; + } + + return pkg; + } + + /** Parses the public keys from the set of signatures. */ + public static ArraySet toSigningKeys(Signature[] signatures) + throws CertificateException { + ArraySet keys = new ArraySet<>(signatures.length); + for (int i = 0; i < signatures.length; i++) { + keys.add(signatures[i].getPublicKey()); + } + return keys; + } + + /** + * Collect certificates from all the APKs described in the given package, + * populating {@link Package#mSigningDetails}. Also asserts that all APK + * contents are signed correctly and consistently. + */ + @UnsupportedAppUsage + public static void collectCertificates(Package pkg, boolean skipVerify) + throws PackageParserException { + collectCertificatesInternal(pkg, skipVerify); + final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0; + for (int i = 0; i < childCount; i++) { + Package childPkg = pkg.childPackages.get(i); + childPkg.mSigningDetails = pkg.mSigningDetails; + } + } + + private static void collectCertificatesInternal(Package pkg, boolean skipVerify) + throws PackageParserException { + pkg.mSigningDetails = SigningDetails.UNKNOWN; + + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "collectCertificates"); + try { + collectCertificates(pkg, new File(pkg.baseCodePath), skipVerify); + + if (!ArrayUtils.isEmpty(pkg.splitCodePaths)) { + for (int i = 0; i < pkg.splitCodePaths.length; i++) { + collectCertificates(pkg, new File(pkg.splitCodePaths[i]), skipVerify); + } + } + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private static void collectCertificates(Package pkg, File apkFile, boolean skipVerify) + throws PackageParserException { + final String apkPath = apkFile.getAbsolutePath(); + + int minSignatureScheme = ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk( + pkg.applicationInfo.targetSdkVersion); + if (pkg.applicationInfo.isStaticSharedLibrary()) { + // must use v2 signing scheme + minSignatureScheme = SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2; + } + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final ParseResult result; + if (skipVerify) { + // systemDir APKs are already trusted, save time by not verifying + result = ApkSignatureVerifier.unsafeGetCertsWithoutVerification( + input, apkPath, minSignatureScheme, false); + } else { + result = ApkSignatureVerifier.verify(input, apkPath, minSignatureScheme); + } + if (result.isError()) { + throw new PackageParserException(result.getErrorCode(), result.getErrorMessage(), + result.getException()); + } + + // 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. + final android.content.pm.SigningDetails verified = result.getResult(); + if (pkg.mSigningDetails == SigningDetails.UNKNOWN) { + pkg.mSigningDetails = new SigningDetails(verified.getSignatures(), + verified.getSignatureSchemeVersion(), + verified.getPublicKeys(), + verified.getPastSigningCertificates()); + } else { + if (!Signature.areExactArraysMatch(pkg.mSigningDetails.signatures, + verified.getSignatures())) { + throw new PackageParserException( + INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, + apkPath + " has mismatched certificates"); + } + } + } + + private static AssetManager newConfiguredAssetManager() { + AssetManager assetManager = new AssetManager(); + assetManager.setConfiguration(0, 0, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); + return assetManager; + } + + /** + * Utility method that retrieves lightweight details about a single APK + * file, including package name, split name, and install location. + * + * @param apkFile path to a single APK + * @param flags optional parse flags, such as + * {@link #PARSE_COLLECT_CERTIFICATES} + */ + public static ApkLite parseApkLite(File apkFile, int flags) + throws PackageParserException { + return parseApkLiteInner(apkFile, null, null, flags); + } + + /** + * Utility method that retrieves lightweight details about a single APK + * file, including package name, split name, and install location. + * + * @param fd already open file descriptor of an apk file + * @param debugPathName arbitrary text name for this file, for debug output + * @param flags optional parse flags, such as + * {@link #PARSE_COLLECT_CERTIFICATES} + */ + public static ApkLite parseApkLite(FileDescriptor fd, String debugPathName, int flags) + throws PackageParserException { + return parseApkLiteInner(null, fd, debugPathName, flags); + } + + private static ApkLite parseApkLiteInner(File apkFile, FileDescriptor fd, String debugPathName, + int flags) throws PackageParserException { + final String apkPath = fd != null ? debugPathName : apkFile.getAbsolutePath(); + + XmlResourceParser parser = null; + ApkAssets apkAssets = null; + try { + try { + apkAssets = fd != null + ? ApkAssets.loadFromFd(fd, debugPathName, 0 /* flags */, null /* assets */) + : ApkAssets.loadFromPath(apkPath); + } catch (IOException e) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK, + "Failed to parse " + apkPath); + } + + parser = apkAssets.openXml(ANDROID_MANIFEST_FILENAME); + + final SigningDetails signingDetails; + if ((flags & PARSE_COLLECT_CERTIFICATES) != 0) { + // TODO: factor signature related items out of Package object + final Package tempPkg = new Package((String) null); + final boolean skipVerify = (flags & PARSE_IS_SYSTEM_DIR) != 0; + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "collectCertificates"); + try { + collectCertificates(tempPkg, apkFile, skipVerify); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + signingDetails = tempPkg.mSigningDetails; + } else { + signingDetails = SigningDetails.UNKNOWN; + } + + final AttributeSet attrs = parser; + return parseApkLite(apkPath, parser, attrs, signingDetails); + + } catch (XmlPullParserException | IOException | RuntimeException e) { + Slog.w(TAG, "Failed to parse " + apkPath, e); + throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "Failed to parse " + apkPath, e); + } finally { + IoUtils.closeQuietly(parser); + if (apkAssets != null) { + try { + apkAssets.close(); + } catch (Throwable ignored) { + } + } + // TODO(b/72056911): Implement AutoCloseable on ApkAssets. + } + } + + public static String validateName(String name, boolean requireSeparator, + boolean requireFilename) { + final int N = name.length(); + boolean hasSep = false; + boolean front = true; + for (int i=0; i= '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 "bad character '" + c + "'"; + } + if (requireFilename && !FileUtils.isValidExtFilename(name)) { + return "Invalid filename"; + } + return hasSep || !requireSeparator + ? null : "must have at least one '.' separator"; + } + + /** + * @deprecated Use {@link android.content.pm.parsing.ApkLiteParseUtils#parsePackageSplitNames} + */ + @Deprecated + public static Pair parsePackageSplitNames(XmlPullParser parser, + AttributeSet attrs) throws IOException, XmlPullParserException, + PackageParserException { + + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + } + + if (type != XmlPullParser.START_TAG) { + throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + "No start tag found"); + } + if (!parser.getName().equals(TAG_MANIFEST)) { + throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + "No tag"); + } + + final String packageName = attrs.getAttributeValue(null, "package"); + if (!"android".equals(packageName)) { + final String error = validateName(packageName, true, true); + if (error != null) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME, + "Invalid manifest package: " + error); + } + } + + String splitName = attrs.getAttributeValue(null, "split"); + if (splitName != null) { + if (splitName.length() == 0) { + splitName = null; + } else { + final String error = validateName(splitName, false, false); + if (error != null) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME, + "Invalid manifest split: " + error); + } + } + } + + return Pair.create(packageName.intern(), + (splitName != null) ? splitName.intern() : splitName); + } + + private static ApkLite parseApkLite(String codePath, XmlPullParser parser, AttributeSet attrs, + SigningDetails signingDetails) + throws IOException, XmlPullParserException, PackageParserException { + final Pair packageSplit = parsePackageSplitNames(parser, attrs); + + int installLocation = PARSE_DEFAULT_INSTALL_LOCATION; + int versionCode = 0; + int versionCodeMajor = 0; + int targetSdkVersion = DEFAULT_TARGET_SDK_VERSION; + int minSdkVersion = DEFAULT_MIN_SDK_VERSION; + int revisionCode = 0; + boolean coreApp = false; + boolean debuggable = false; + boolean profilableByShell = false; + boolean multiArch = false; + boolean use32bitAbi = false; + boolean extractNativeLibs = true; + boolean isolatedSplits = false; + boolean isFeatureSplit = false; + boolean isSplitRequired = false; + boolean useEmbeddedDex = false; + String configForSplit = null; + String usesSplitName = null; + String targetPackage = null; + boolean overlayIsStatic = false; + int overlayPriority = 0; + int rollbackDataPolicy = 0; + + String requiredSystemPropertyName = null; + String requiredSystemPropertyValue = null; + + for (int i = 0; i < attrs.getAttributeCount(); i++) { + final String attr = attrs.getAttributeName(i); + if (attr.equals("installLocation")) { + installLocation = attrs.getAttributeIntValue(i, + PARSE_DEFAULT_INSTALL_LOCATION); + } else if (attr.equals("versionCode")) { + versionCode = attrs.getAttributeIntValue(i, 0); + } else if (attr.equals("versionCodeMajor")) { + versionCodeMajor = attrs.getAttributeIntValue(i, 0); + } else if (attr.equals("revisionCode")) { + revisionCode = attrs.getAttributeIntValue(i, 0); + } else if (attr.equals("coreApp")) { + coreApp = attrs.getAttributeBooleanValue(i, false); + } else if (attr.equals("isolatedSplits")) { + isolatedSplits = attrs.getAttributeBooleanValue(i, false); + } else if (attr.equals("configForSplit")) { + configForSplit = attrs.getAttributeValue(i); + } else if (attr.equals("isFeatureSplit")) { + isFeatureSplit = attrs.getAttributeBooleanValue(i, false); + } else if (attr.equals("isSplitRequired")) { + isSplitRequired = attrs.getAttributeBooleanValue(i, false); + } + } + + // Only search the tree when the tag is the direct child of tag + int type; + final int searchDepth = parser.getDepth() + 1; + + final List verifiers = new ArrayList(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() >= searchDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getDepth() != searchDepth) { + continue; + } + + if (TAG_PACKAGE_VERIFIER.equals(parser.getName())) { + final VerifierInfo verifier = parseVerifier(attrs); + if (verifier != null) { + verifiers.add(verifier); + } + } else if (TAG_APPLICATION.equals(parser.getName())) { + for (int i = 0; i < attrs.getAttributeCount(); ++i) { + final String attr = attrs.getAttributeName(i); + if ("debuggable".equals(attr)) { + debuggable = attrs.getAttributeBooleanValue(i, false); + } + if ("multiArch".equals(attr)) { + multiArch = attrs.getAttributeBooleanValue(i, false); + } + if ("use32bitAbi".equals(attr)) { + use32bitAbi = attrs.getAttributeBooleanValue(i, false); + } + if ("extractNativeLibs".equals(attr)) { + extractNativeLibs = attrs.getAttributeBooleanValue(i, true); + } + if ("useEmbeddedDex".equals(attr)) { + useEmbeddedDex = attrs.getAttributeBooleanValue(i, false); + } + if (attr.equals("rollbackDataPolicy")) { + rollbackDataPolicy = attrs.getAttributeIntValue(i, 0); + } + } + } else if (PackageParser.TAG_OVERLAY.equals(parser.getName())) { + for (int i = 0; i < attrs.getAttributeCount(); ++i) { + final String attr = attrs.getAttributeName(i); + if ("requiredSystemPropertyName".equals(attr)) { + requiredSystemPropertyName = attrs.getAttributeValue(i); + } else if ("requiredSystemPropertyValue".equals(attr)) { + requiredSystemPropertyValue = attrs.getAttributeValue(i); + } else if ("targetPackage".equals(attr)) { + targetPackage = attrs.getAttributeValue(i);; + } else if ("isStatic".equals(attr)) { + overlayIsStatic = attrs.getAttributeBooleanValue(i, false); + } else if ("priority".equals(attr)) { + overlayPriority = attrs.getAttributeIntValue(i, 0); + } + } + } else if (TAG_USES_SPLIT.equals(parser.getName())) { + if (usesSplitName != null) { + Slog.w(TAG, "Only one permitted. Ignoring others."); + continue; + } + + usesSplitName = attrs.getAttributeValue(ANDROID_RESOURCES, "name"); + if (usesSplitName == null) { + throw new PackageParserException( + PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + " tag requires 'android:name' attribute"); + } + } else if (TAG_USES_SDK.equals(parser.getName())) { + for (int i = 0; i < attrs.getAttributeCount(); ++i) { + final String attr = attrs.getAttributeName(i); + if ("targetSdkVersion".equals(attr)) { + targetSdkVersion = attrs.getAttributeIntValue(i, + DEFAULT_TARGET_SDK_VERSION); + } + if ("minSdkVersion".equals(attr)) { + minSdkVersion = attrs.getAttributeIntValue(i, DEFAULT_MIN_SDK_VERSION); + } + } + } + } + + // Check to see if overlay should be excluded based on system property condition + if (!checkRequiredSystemProperties(requiredSystemPropertyName, + requiredSystemPropertyValue)) { + Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and " + + codePath + ": overlay ignored due to required system property: " + + requiredSystemPropertyName + " with value: " + requiredSystemPropertyValue); + targetPackage = null; + overlayIsStatic = false; + overlayPriority = 0; + } + + return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit, + configForSplit, usesSplitName, isSplitRequired, versionCode, versionCodeMajor, + revisionCode, installLocation, verifiers, signingDetails, coreApp, debuggable, + profilableByShell, multiArch, use32bitAbi, useEmbeddedDex, extractNativeLibs, + isolatedSplits, targetPackage, overlayIsStatic, overlayPriority, minSdkVersion, + targetSdkVersion, rollbackDataPolicy); + } + + /** + * Parses a child package and adds it to the parent if successful. If you add + * new tags that need to be supported by child packages make sure to add them + * to {@link #CHILD_PACKAGE_TAGS}. + * + * @param parentPkg The parent that contains the child + * @param res Resources against which to resolve values + * @param parser Parser of the manifest + * @param flags Flags about how to parse + * @param outError Human readable error if parsing fails + * @return True of parsing succeeded. + * + * @throws XmlPullParserException + * @throws IOException + */ + private boolean parseBaseApkChild(Package parentPkg, Resources res, XmlResourceParser parser, + int flags, String[] outError) throws XmlPullParserException, IOException { + // Make sure we have a valid child package name + String childPackageName = parser.getAttributeValue(null, "package"); + if (validateName(childPackageName, true, false) != null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME; + return false; + } + + // Child packages must be unique + if (childPackageName.equals(parentPkg.packageName)) { + String message = "Child package name cannot be equal to parent package name: " + + parentPkg.packageName; + Slog.w(TAG, message); + outError[0] = message; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + // Child packages must be unique + if (parentPkg.hasChildPackage(childPackageName)) { + String message = "Duplicate child package:" + childPackageName; + Slog.w(TAG, message); + outError[0] = message; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + // Go ahead and parse the child + Package childPkg = new Package(childPackageName); + + // Child package inherits parent version code/name/target SDK + childPkg.mVersionCode = parentPkg.mVersionCode; + childPkg.baseRevisionCode = parentPkg.baseRevisionCode; + childPkg.mVersionName = parentPkg.mVersionName; + childPkg.applicationInfo.targetSdkVersion = parentPkg.applicationInfo.targetSdkVersion; + childPkg.applicationInfo.minSdkVersion = parentPkg.applicationInfo.minSdkVersion; + + childPkg = parseBaseApkCommon(childPkg, CHILD_PACKAGE_TAGS, res, parser, flags, outError); + if (childPkg == null) { + // If we got null then error was set during child parsing + return false; + } + + // Set the parent-child relation + if (parentPkg.childPackages == null) { + parentPkg.childPackages = new ArrayList<>(); + } + parentPkg.childPackages.add(childPkg); + childPkg.parentPackage = parentPkg; + + return true; + } + + /** + * Parse the manifest of a base APK. 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 + * @param outError Human readable error message + * @return Parsed package or null on error. + * + * @throws XmlPullParserException + * @throws IOException + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + private Package parseBaseApk(String apkPath, Resources res, XmlResourceParser parser, int flags, + String[] outError) throws XmlPullParserException, IOException { + final String splitName; + final String pkgName; + + try { + Pair packageSplit = parsePackageSplitNames(parser, parser); + pkgName = packageSplit.first; + splitName = packageSplit.second; + + if (!TextUtils.isEmpty(splitName)) { + outError[0] = "Expected base APK, but found split " + splitName; + mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME; + return null; + } + } catch (PackageParserException e) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME; + return null; + } + + final Package pkg = new Package(pkgName); + + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifest); + + pkg.mVersionCode = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_versionCode, 0); + pkg.mVersionCodeMajor = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_versionCodeMajor, 0); + pkg.applicationInfo.setVersionCode(pkg.getLongVersionCode()); + pkg.baseRevisionCode = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_revisionCode, 0); + pkg.mVersionName = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifest_versionName, 0); + if (pkg.mVersionName != null) { + pkg.mVersionName = pkg.mVersionName.intern(); + } + + pkg.coreApp = parser.getAttributeBooleanValue(null, "coreApp", false); + + final boolean isolatedSplits = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifest_isolatedSplits, false); + if (isolatedSplits) { + pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_ISOLATED_SPLIT_LOADING; + } + + pkg.mCompileSdkVersion = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_compileSdkVersion, 0); + pkg.applicationInfo.compileSdkVersion = pkg.mCompileSdkVersion; + pkg.mCompileSdkVersionCodename = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifest_compileSdkVersionCodename, 0); + if (pkg.mCompileSdkVersionCodename != null) { + pkg.mCompileSdkVersionCodename = pkg.mCompileSdkVersionCodename.intern(); + } + pkg.applicationInfo.compileSdkVersionCodename = pkg.mCompileSdkVersionCodename; + + sa.recycle(); + + return parseBaseApkCommon(pkg, null, res, parser, flags, outError); + } + + /** + * This is the common parsing routing for handling parent and child + * packages in a base APK. The difference between parent and child + * parsing is that some tags are not supported by child packages as + * well as some manifest attributes are ignored. The implementation + * assumes the calling code has already handled the manifest tag if needed + * (this applies to the parent only). + * + * @param pkg The package which to populate + * @param acceptedTags Which tags to handle, null to handle all + * @param res Resources against which to resolve values + * @param parser Parser of the manifest + * @param flags Flags about how to parse + * @param outError Human readable error if parsing fails + * @return The package if parsing succeeded or null. + * + * @throws XmlPullParserException + * @throws IOException + */ + private Package parseBaseApkCommon(Package pkg, Set acceptedTags, Resources res, + XmlResourceParser parser, int flags, String[] outError) throws XmlPullParserException, + IOException { + mParseInstrumentationArgs = null; + + int type; + boolean foundApp = false; + + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifest); + + int maxSdkVersion = 0; + if (PackageManager.ENABLE_SHARED_UID_MIGRATION) { + maxSdkVersion = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_sharedUserMaxSdkVersion, 0); + } + if (maxSdkVersion == 0 || maxSdkVersion >= Build.VERSION.RESOURCES_SDK_INT) { + String str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifest_sharedUserId, 0); + if (str != null && str.length() > 0) { + String nameError = validateName(str, true, true); + if (nameError != null && !"android".equals(pkg.packageName)) { + outError[0] = " specifies bad sharedUserId name \"" + + str + "\": " + nameError; + mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID; + return null; + } + pkg.mSharedUserId = str.intern(); + pkg.mSharedUserLabel = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifest_sharedUserLabel, 0); + } + } + + pkg.installLocation = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_installLocation, + PARSE_DEFAULT_INSTALL_LOCATION); + pkg.applicationInfo.installLocation = pkg.installLocation; + + final int targetSandboxVersion = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_targetSandboxVersion, + PARSE_DEFAULT_TARGET_SANDBOX); + pkg.applicationInfo.targetSandboxVersion = targetSandboxVersion; + + /* Set the global "on SD card" flag */ + if ((flags & PARSE_EXTERNAL_STORAGE) != 0) { + pkg.applicationInfo.flags |= ApplicationInfo.FLAG_EXTERNAL_STORAGE; + } + + // Resource boolean are -1, so 1 means we don't know the value. + int supportsSmallScreens = 1; + int supportsNormalScreens = 1; + int supportsLargeScreens = 1; + int supportsXLargeScreens = 1; + int resizeable = 1; + int anyDensity = 1; + + int outerDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + + if (acceptedTags != null && !acceptedTags.contains(tagName)) { + Slog.w(TAG, "Skipping unsupported element under : " + + tagName + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } + + if (tagName.equals(TAG_APPLICATION)) { + if (foundApp) { + if (RIGID_PARSER) { + outError[0] = " has more than one "; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } else { + Slog.w(TAG, " has more than one "); + XmlUtils.skipCurrentTag(parser); + continue; + } + } + + foundApp = true; + if (!parseBaseApplication(pkg, res, parser, flags, outError)) { + return null; + } + } else if (tagName.equals(TAG_OVERLAY)) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestResourceOverlay); + pkg.mOverlayTarget = sa.getString( + com.android.internal.R.styleable.AndroidManifestResourceOverlay_targetPackage); + pkg.mOverlayTargetName = sa.getString( + com.android.internal.R.styleable.AndroidManifestResourceOverlay_targetName); + pkg.mOverlayCategory = sa.getString( + com.android.internal.R.styleable.AndroidManifestResourceOverlay_category); + pkg.mOverlayPriority = sa.getInt( + com.android.internal.R.styleable.AndroidManifestResourceOverlay_priority, + 0); + pkg.mOverlayIsStatic = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestResourceOverlay_isStatic, + false); + final String propName = sa.getString( + com.android.internal.R.styleable + .AndroidManifestResourceOverlay_requiredSystemPropertyName); + final String propValue = sa.getString( + com.android.internal.R.styleable + .AndroidManifestResourceOverlay_requiredSystemPropertyValue); + sa.recycle(); + + if (pkg.mOverlayTarget == null) { + outError[0] = " does not specify a target package"; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + if (pkg.mOverlayPriority < 0 || pkg.mOverlayPriority > 9999) { + outError[0] = " priority must be between 0 and 9999"; + mParseError = + PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + // check to see if overlay should be excluded based on system property condition + if (!checkRequiredSystemProperties(propName, propValue)) { + Slog.i(TAG, "Skipping target and overlay pair " + pkg.mOverlayTarget + " and " + + pkg.baseCodePath+ ": overlay ignored due to required system property: " + + propName + " with value: " + propValue); + mParseError = PackageManager.INSTALL_PARSE_FAILED_SKIPPED; + return null; + } + + pkg.applicationInfo.privateFlags |= + ApplicationInfo.PRIVATE_FLAG_IS_RESOURCE_OVERLAY; + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals(TAG_KEY_SETS)) { + if (!parseKeySets(pkg, res, parser, outError)) { + return null; + } + } else if (tagName.equals(TAG_PERMISSION_GROUP)) { + if (!parsePermissionGroup(pkg, flags, res, parser, outError)) { + return null; + } + } else if (tagName.equals(TAG_PERMISSION)) { + if (!parsePermission(pkg, res, parser, outError)) { + return null; + } + } else if (tagName.equals(TAG_PERMISSION_TREE)) { + if (!parsePermissionTree(pkg, res, parser, outError)) { + return null; + } + } else if (tagName.equals(TAG_USES_PERMISSION)) { + if (!parseUsesPermission(pkg, res, parser)) { + return null; + } + } else if (tagName.equals(TAG_USES_PERMISSION_SDK_M) + || tagName.equals(TAG_USES_PERMISSION_SDK_23)) { + if (!parseUsesPermission(pkg, res, parser)) { + return null; + } + } else if (tagName.equals(TAG_USES_CONFIGURATION)) { + ConfigurationInfo cPref = new ConfigurationInfo(); + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestUsesConfiguration); + cPref.reqTouchScreen = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqTouchScreen, + Configuration.TOUCHSCREEN_UNDEFINED); + cPref.reqKeyboardType = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqKeyboardType, + Configuration.KEYBOARD_UNDEFINED); + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqHardKeyboard, + false)) { + cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD; + } + cPref.reqNavigation = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqNavigation, + Configuration.NAVIGATION_UNDEFINED); + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqFiveWayNav, + false)) { + cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV; + } + sa.recycle(); + pkg.configPreferences = ArrayUtils.add(pkg.configPreferences, cPref); + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals(TAG_USES_FEATURE)) { + FeatureInfo fi = parseUsesFeature(res, parser); + pkg.reqFeatures = ArrayUtils.add(pkg.reqFeatures, fi); + + if (fi.name == null) { + ConfigurationInfo cPref = new ConfigurationInfo(); + cPref.reqGlEsVersion = fi.reqGlEsVersion; + pkg.configPreferences = ArrayUtils.add(pkg.configPreferences, cPref); + } + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals(TAG_FEATURE_GROUP)) { + FeatureGroupInfo group = new FeatureGroupInfo(); + ArrayList features = null; + 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 String innerTagName = parser.getName(); + if (innerTagName.equals("uses-feature")) { + FeatureInfo featureInfo = parseUsesFeature(res, parser); + // FeatureGroups are stricter and mandate that + // any declared are mandatory. + featureInfo.flags |= FeatureInfo.FLAG_REQUIRED; + features = ArrayUtils.add(features, featureInfo); + } else { + Slog.w(TAG, "Unknown element under : " + innerTagName + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + } + XmlUtils.skipCurrentTag(parser); + } + + if (features != null) { + group.features = new FeatureInfo[features.size()]; + group.features = features.toArray(group.features); + } + pkg.featureGroups = ArrayUtils.add(pkg.featureGroups, group); + + } else if (tagName.equals(TAG_USES_SDK)) { + if (SDK_VERSION > 0) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestUsesSdk); + + int minVers = 1; + String minCode = null; + int targetVers = 0; + String targetCode = null; + + TypedValue val = sa.peekValue( + com.android.internal.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( + com.android.internal.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; + } + + sa.recycle(); + + final int minSdkVersion = PackageParser.computeMinSdkVersion(minVers, minCode, + SDK_VERSION, SDK_CODENAMES, outError); + if (minSdkVersion < 0) { + mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; + return null; + } + + final int targetSdkVersion = PackageParser.computeTargetSdkVersion(targetVers, + targetCode, SDK_CODENAMES, outError); + if (targetSdkVersion < 0) { + mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; + return null; + } + + pkg.applicationInfo.minSdkVersion = minSdkVersion; + pkg.applicationInfo.targetSdkVersion = targetSdkVersion; + } + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals(TAG_SUPPORT_SCREENS)) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestSupportsScreens); + + pkg.applicationInfo.requiresSmallestWidthDp = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_requiresSmallestWidthDp, + 0); + pkg.applicationInfo.compatibleWidthLimitDp = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_compatibleWidthLimitDp, + 0); + pkg.applicationInfo.largestWidthLimitDp = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_largestWidthLimitDp, + 0); + + // This is a trick to get a boolean and still able to detect + // if a value was actually set. + supportsSmallScreens = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_smallScreens, + supportsSmallScreens); + supportsNormalScreens = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_normalScreens, + supportsNormalScreens); + supportsLargeScreens = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_largeScreens, + supportsLargeScreens); + supportsXLargeScreens = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_xlargeScreens, + supportsXLargeScreens); + resizeable = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_resizeable, + resizeable); + anyDensity = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_anyDensity, + anyDensity); + + sa.recycle(); + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals(TAG_PROTECTED_BROADCAST)) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestProtectedBroadcast); + + // Note: don't allow this value to be a reference to a resource + // that may change. + String name = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestProtectedBroadcast_name); + + sa.recycle(); + + if (name != null) { + if (pkg.protectedBroadcasts == null) { + pkg.protectedBroadcasts = new ArrayList(); + } + if (!pkg.protectedBroadcasts.contains(name)) { + pkg.protectedBroadcasts.add(name.intern()); + } + } + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals(TAG_INSTRUMENTATION)) { + if (parseInstrumentation(pkg, res, parser, outError) == null) { + return null; + } + } else if (tagName.equals(TAG_ORIGINAL_PACKAGE)) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestOriginalPackage); + + String orig =sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestOriginalPackage_name, 0); + if (!pkg.packageName.equals(orig)) { + if (pkg.mOriginalPackages == null) { + pkg.mOriginalPackages = new ArrayList(); + pkg.mRealPackage = pkg.packageName; + } + pkg.mOriginalPackages.add(orig); + } + + sa.recycle(); + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals(TAG_ADOPT_PERMISSIONS)) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestOriginalPackage); + + String name = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestOriginalPackage_name, 0); + + sa.recycle(); + + if (name != null) { + if (pkg.mAdoptPermissions == null) { + pkg.mAdoptPermissions = new ArrayList(); + } + pkg.mAdoptPermissions.add(name); + } + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals(TAG_USES_GL_TEXTURE)) { + // Just skip this tag + XmlUtils.skipCurrentTag(parser); + continue; + + } else if (tagName.equals(TAG_COMPATIBLE_SCREENS)) { + // Just skip this tag + XmlUtils.skipCurrentTag(parser); + continue; + } else if (tagName.equals(TAG_SUPPORTS_INPUT)) {// + XmlUtils.skipCurrentTag(parser); + continue; + + } else if (tagName.equals(TAG_EAT_COMMENT)) { + // Just skip this tag + XmlUtils.skipCurrentTag(parser); + continue; + + } else if (tagName.equals(TAG_PACKAGE)) { + if (!MULTI_PACKAGE_APK_ENABLED) { + XmlUtils.skipCurrentTag(parser); + continue; + } + if (!parseBaseApkChild(pkg, res, parser, flags, outError)) { + // If parsing a child failed the error is already set + return null; + } + + } else if (tagName.equals(TAG_RESTRICT_UPDATE)) { + if ((flags & PARSE_IS_SYSTEM_DIR) != 0) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestRestrictUpdate); + final String hash = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestRestrictUpdate_hash, 0); + sa.recycle(); + + pkg.restrictUpdateHash = null; + 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.restrictUpdateHash = hashBytes; + } + } + + XmlUtils.skipCurrentTag(parser); + + } else if (RIGID_PARSER) { + outError[0] = "Bad element under : " + + parser.getName(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + + } else { + Slog.w(TAG, "Unknown element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } + } + + if (!foundApp && pkg.instrumentation.size() == 0) { + outError[0] = " does not contain an or "; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY; + } + + final int NP = PackageParser.NEW_PERMISSIONS.length; + StringBuilder newPermsMsg = null; + for (int ip=0; ip= npi.sdkVersion) { + break; + } + if (!pkg.requestedPermissions.contains(npi.name)) { + if (newPermsMsg == null) { + newPermsMsg = new StringBuilder(128); + newPermsMsg.append(pkg.packageName); + newPermsMsg.append(": compat added "); + } else { + newPermsMsg.append(' '); + } + newPermsMsg.append(npi.name); + pkg.requestedPermissions.add(npi.name); + pkg.implicitPermissions.add(npi.name); + } + } + if (newPermsMsg != null) { + Slog.i(TAG, newPermsMsg.toString()); + } + + // Must build permission info manually for legacy code, which can be called before + // Appication is available through the app process, so the normal API doesn't work. + List splitPermissionParcelables; + try { + splitPermissionParcelables = ActivityThread.getPermissionManager() + .getSplitPermissions(); + } catch (RemoteException e) { + splitPermissionParcelables = Collections.emptyList(); + } + + int splitPermissionsSize = splitPermissionParcelables.size(); + List splitPermissions = + new ArrayList<>(splitPermissionsSize); + for (int index = 0; index < splitPermissionsSize; index++) { + SplitPermissionInfoParcelable splitPermissionParcelable = + splitPermissionParcelables.get(index); + splitPermissions.add(new PermissionManager.SplitPermissionInfo( + splitPermissionParcelable.getSplitPermission(), + splitPermissionParcelable.getNewPermissions(), + splitPermissionParcelable.getTargetSdk() + )); + } + + final int listSize = splitPermissions.size(); + for (int is = 0; is < listSize; is++) { + final PermissionManager.SplitPermissionInfo spi = splitPermissions.get(is); + if (pkg.applicationInfo.targetSdkVersion >= spi.getTargetSdk() + || !pkg.requestedPermissions.contains(spi.getSplitPermission())) { + continue; + } + final List newPerms = spi.getNewPermissions(); + for (int in = 0; in < newPerms.size(); in++) { + final String perm = newPerms.get(in); + if (!pkg.requestedPermissions.contains(perm)) { + pkg.requestedPermissions.add(perm); + pkg.implicitPermissions.add(perm); + } + } + } + + if (supportsSmallScreens < 0 || (supportsSmallScreens > 0 + && pkg.applicationInfo.targetSdkVersion + >= android.os.Build.VERSION_CODES.DONUT)) { + pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS; + } + if (supportsNormalScreens != 0) { + pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_NORMAL_SCREENS; + } + if (supportsLargeScreens < 0 || (supportsLargeScreens > 0 + && pkg.applicationInfo.targetSdkVersion + >= android.os.Build.VERSION_CODES.DONUT)) { + pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS; + } + if (supportsXLargeScreens < 0 || (supportsXLargeScreens > 0 + && pkg.applicationInfo.targetSdkVersion + >= android.os.Build.VERSION_CODES.GINGERBREAD)) { + pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_XLARGE_SCREENS; + } + if (resizeable < 0 || (resizeable > 0 + && pkg.applicationInfo.targetSdkVersion + >= android.os.Build.VERSION_CODES.DONUT)) { + pkg.applicationInfo.flags |= ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS; + } + if (anyDensity < 0 || (anyDensity > 0 + && pkg.applicationInfo.targetSdkVersion + >= android.os.Build.VERSION_CODES.DONUT)) { + pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES; + } + + // 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.applicationInfo.usesCompatibilityMode()) { + adjustPackageToBeUnresizeableAndUnpipable(pkg); + } + + return pkg; + } + + /** + * Returns {@code true} if both the property name and value are empty or if the given system + * property is set to the specified value. Properties can be one or more, and if properties are + * more than one, they must be separated by comma, and count of names and values must be equal, + * and also every given system property must be set to the corresponding value. + * In all other cases, returns {@code false} + */ + public static boolean checkRequiredSystemProperties(@Nullable String rawPropNames, + @Nullable String rawPropValues) { + if (TextUtils.isEmpty(rawPropNames) || TextUtils.isEmpty(rawPropValues)) { + if (!TextUtils.isEmpty(rawPropNames) || !TextUtils.isEmpty(rawPropValues)) { + // malformed condition - incomplete + Slog.w(TAG, "Disabling overlay - incomplete property :'" + rawPropNames + + "=" + rawPropValues + "' - require both requiredSystemPropertyName" + + " AND requiredSystemPropertyValue to be specified."); + return false; + } + // no valid condition set - so no exclusion criteria, overlay will be included. + return true; + } + + final String[] propNames = rawPropNames.split(","); + final String[] propValues = rawPropValues.split(","); + + if (propNames.length != propValues.length) { + Slog.w(TAG, "Disabling overlay - property :'" + rawPropNames + + "=" + rawPropValues + "' - require both requiredSystemPropertyName" + + " AND requiredSystemPropertyValue lists to have the same size."); + return false; + } + for (int i = 0; i < propNames.length; i++) { + // Check property value: make sure it is both set and equal to expected value + final String currValue = SystemProperties.get(propNames[i]); + if (!TextUtils.equals(currValue, propValues[i])) { + return false; + } + } + return true; + } + + /** + * 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 void adjustPackageToBeUnresizeableAndUnpipable(Package pkg) { + for (Activity a : pkg.activities) { + a.info.resizeMode = RESIZE_MODE_UNRESIZEABLE; + a.info.flags &= ~FLAG_SUPPORTS_PICTURE_IN_PICTURE; + } + } + + /** + + /** + * 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); + } + + /** + * Computes the targetSdkVersion to use at runtime. If the package is not + * compatible with this platform, populates {@code outError[0]} with an + * error message. + *

+ * If {@code targetCode} is not specified, e.g. the value is {@code null}, + * then the {@code targetVers} will be returned unmodified. + *

+ * Otherwise, the behavior varies based on whether the current platform + * is a pre-release version, e.g. the {@code platformSdkCodenames} array + * has length > 0: + *

    + *
  • If this is a pre-release platform and the value specified by + * {@code targetCode} is contained within the array of allowed pre-release + * codenames, this method will return {@link Build.VERSION_CODES#CUR_DEVELOPMENT}. + *
  • If this is a released platform, this method will return -1 to + * indicate that the package is not compatible with this platform. + *
+ * + * @param targetVers targetSdkVersion number, if specified in the + * application manifest, or 0 otherwise + * @param targetCode targetSdkVersion code, if specified in the application + * manifest, or {@code null} otherwise + * @param platformSdkCodenames array of allowed pre-release SDK codenames + * for this platform + * @param outError output array to populate with error, if applicable + * @return the targetSdkVersion to use at runtime, or -1 if the package is + * not compatible with this platform + * @hide Exposed for unit testing only. + */ + public static int computeTargetSdkVersion(@IntRange(from = 0) int targetVers, + @Nullable String targetCode, @NonNull String[] platformSdkCodenames, + @NonNull String[] outError) { + // If it's a release SDK, return the version number unmodified. + if (targetCode == null) { + return targetVers; + } + + // If it's a pre-release SDK and the codename matches this platform, it + // definitely targets this SDK. + if (matchTargetCode(platformSdkCodenames, targetCode)) { + return Build.VERSION_CODES.CUR_DEVELOPMENT; + } + + // Otherwise, we're looking at an incompatible pre-release SDK. + if (platformSdkCodenames.length > 0) { + outError[0] = "Requires development platform " + targetCode + + " (current platform is any of " + + Arrays.toString(platformSdkCodenames) + ")"; + } else { + outError[0] = "Requires development platform " + targetCode + + " but this is a release platform."; + } + return -1; + } + + /** + * Computes the minSdkVersion to use at runtime. If the package is not + * compatible with this platform, populates {@code outError[0]} with an + * error message. + *

+ * If {@code minCode} is not specified, e.g. the value is {@code null}, + * then behavior varies based on the {@code platformSdkVersion}: + *

    + *
  • If the platform SDK version is greater than or equal to the + * {@code minVers}, returns the {@code mniVers} unmodified. + *
  • Otherwise, returns -1 to indicate that the package is not + * compatible with this platform. + *
+ *

+ * Otherwise, the behavior varies based on whether the current platform + * is a pre-release version, e.g. the {@code platformSdkCodenames} array + * has length > 0: + *

    + *
  • If this is a pre-release platform and the value specified by + * {@code targetCode} is contained within the array of allowed pre-release + * codenames, this method will return {@link Build.VERSION_CODES#CUR_DEVELOPMENT}. + *
  • If this is a released platform, this method will return -1 to + * indicate that the package is not compatible with this platform. + *
+ * + * @param minVers minSdkVersion number, if specified in the application + * manifest, or 1 otherwise + * @param minCode minSdkVersion code, if specified in the application + * manifest, or {@code null} otherwise + * @param platformSdkVersion platform SDK version number, typically + * Build.VERSION.SDK_INT + * @param platformSdkCodenames array of allowed prerelease SDK codenames + * for this platform + * @param outError output array to populate with error, if applicable + * @return the minSdkVersion to use at runtime, or -1 if the package is not + * compatible with this platform + * @hide Exposed for unit testing only. + */ + public static int computeMinSdkVersion(@IntRange(from = 1) int minVers, + @Nullable String minCode, @IntRange(from = 1) int platformSdkVersion, + @NonNull String[] platformSdkCodenames, @NonNull String[] outError) { + // If it's a release SDK, make sure we meet the minimum SDK requirement. + if (minCode == null) { + if (minVers <= platformSdkVersion) { + return minVers; + } + + // We don't meet the minimum SDK requirement. + outError[0] = "Requires newer sdk version #" + minVers + + " (current version is #" + platformSdkVersion + ")"; + return -1; + } + + // 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 Build.VERSION_CODES.CUR_DEVELOPMENT; + } + + // Otherwise, we're looking at an incompatible pre-release SDK. + if (platformSdkCodenames.length > 0) { + outError[0] = "Requires development platform " + minCode + + " (current platform is any of " + + Arrays.toString(platformSdkCodenames) + ")"; + } else { + outError[0] = "Requires development platform " + minCode + + " but this is a release platform."; + } + return -1; + } + + private FeatureInfo parseUsesFeature(Resources res, AttributeSet attrs) { + FeatureInfo fi = new FeatureInfo(); + TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestUsesFeature); + // Note: don't allow this value to be a reference to a resource + // that may change. + fi.name = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestUsesFeature_name); + fi.version = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesFeature_version, 0); + if (fi.name == null) { + fi.reqGlEsVersion = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesFeature_glEsVersion, + FeatureInfo.GL_ES_VERSION_UNDEFINED); + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesFeature_required, true)) { + fi.flags |= FeatureInfo.FLAG_REQUIRED; + } + sa.recycle(); + return fi; + } + + private boolean parseUsesStaticLibrary(Package pkg, Resources res, XmlResourceParser parser, + String[] outError) throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestUsesStaticLibrary); + + // Note: don't allow this value to be a reference to a resource that may change. + String lname = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestUsesLibrary_name); + final int version = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesStaticLibrary_version, -1); + String certSha256Digest = sa.getNonResourceString(com.android.internal.R.styleable + .AndroidManifestUsesStaticLibrary_certDigest); + sa.recycle(); + + // Since an APK providing a static shared lib can only provide the lib - fail if malformed + if (lname == null || version < 0 || certSha256Digest == null) { + outError[0] = "Bad uses-static-library declaration name: " + lname + " version: " + + version + " certDigest" + certSha256Digest; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + XmlUtils.skipCurrentTag(parser); + return false; + } + + // Can depend only on one version of the same library + if (pkg.usesStaticLibraries != null && pkg.usesStaticLibraries.contains(lname)) { + outError[0] = "Depending on multiple versions of static library " + lname; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + XmlUtils.skipCurrentTag(parser); + return false; + } + + 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.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O_MR1) { + additionalCertSha256Digests = parseAdditionalCertificates(res, parser, outError); + if (additionalCertSha256Digests == null) { + return false; + } + } else { + XmlUtils.skipCurrentTag(parser); + } + + final String[] certSha256Digests = new String[additionalCertSha256Digests.length + 1]; + certSha256Digests[0] = certSha256Digest; + System.arraycopy(additionalCertSha256Digests, 0, certSha256Digests, + 1, additionalCertSha256Digests.length); + + pkg.usesStaticLibraries = ArrayUtils.add(pkg.usesStaticLibraries, lname); + pkg.usesStaticLibrariesVersions = ArrayUtils.appendLong( + pkg.usesStaticLibrariesVersions, version, true); + pkg.usesStaticLibrariesCertDigests = ArrayUtils.appendElement(String[].class, + pkg.usesStaticLibrariesCertDigests, certSha256Digests, true); + + return true; + } + + private String[] parseAdditionalCertificates(Resources resources, XmlResourceParser parser, + String[] outError) throws XmlPullParserException, IOException { + String[] certSha256Digests = EmptyArray.STRING; + + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + final String nodeName = parser.getName(); + if (nodeName.equals("additional-certificate")) { + final TypedArray sa = resources.obtainAttributes(parser, com.android.internal. + R.styleable.AndroidManifestAdditionalCertificate); + String certSha256Digest = sa.getNonResourceString(com.android.internal. + R.styleable.AndroidManifestAdditionalCertificate_certDigest); + sa.recycle(); + + if (TextUtils.isEmpty(certSha256Digest)) { + outError[0] = "Bad additional-certificate declaration with empty" + + " certDigest:" + certSha256Digest; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + XmlUtils.skipCurrentTag(parser); + sa.recycle(); + return null; + } + + // 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); + } else { + XmlUtils.skipCurrentTag(parser); + } + } + + return certSha256Digests; + } + + private boolean parseUsesPermission(Package pkg, Resources res, XmlResourceParser parser) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestUsesPermission); + + // Note: don't allow this value to be a reference to a resource + // that may change. + String name = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestUsesPermission_name); + + int maxSdkVersion = 0; + TypedValue val = sa.peekValue( + com.android.internal.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( + com.android.internal.R.styleable.AndroidManifestUsesPermission_requiredFeature, 0); + + final String requiredNotfeature = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestUsesPermission_requiredNotFeature, 0); + + sa.recycle(); + + XmlUtils.skipCurrentTag(parser); + + if (name == null) { + return true; + } + + if ((maxSdkVersion != 0) && (maxSdkVersion < Build.VERSION.RESOURCES_SDK_INT)) { + return true; + } + + // Only allow requesting this permission if the platform supports the given feature. + if (requiredFeature != null && mCallback != null && !mCallback.hasFeature(requiredFeature)) { + return true; + } + + // Only allow requesting this permission if the platform doesn't support the given feature. + if (requiredNotfeature != null && mCallback != null + && mCallback.hasFeature(requiredNotfeature)) { + return true; + } + + int index = pkg.requestedPermissions.indexOf(name); + if (index == -1) { + pkg.requestedPermissions.add(name.intern()); + } else { + Slog.w(TAG, "Ignoring duplicate uses-permissions/uses-permissions-sdk-m: " + + name + " in package: " + pkg.packageName + " at: " + + parser.getPositionDescription()); + } + + return true; + } + + public static String buildClassName(String pkg, CharSequence clsSeq, + String[] outError) { + if (clsSeq == null || clsSeq.length() <= 0) { + outError[0] = "Empty class name in package " + pkg; + return null; + } + String cls = clsSeq.toString(); + char c = cls.charAt(0); + if (c == '.') { + return pkg + cls; + } + if (cls.indexOf('.') < 0) { + StringBuilder b = new StringBuilder(pkg); + b.append('.'); + b.append(cls); + return b.toString(); + } + return cls; + } + + private static String buildCompoundName(String pkg, + CharSequence procSeq, String type, String[] outError) { + String proc = procSeq.toString(); + char c = proc.charAt(0); + if (pkg != null && c == ':') { + if (proc.length() < 2) { + outError[0] = "Bad " + type + " name " + proc + " in package " + pkg + + ": must be at least two characters"; + return null; + } + String subName = proc.substring(1); + String nameError = validateName(subName, false, false); + if (nameError != null) { + outError[0] = "Invalid " + type + " name " + proc + " in package " + + pkg + ": " + nameError; + return null; + } + return pkg + proc; + } + String nameError = validateName(proc, true, false); + if (nameError != null && !"system".equals(proc)) { + outError[0] = "Invalid " + type + " name " + proc + " in package " + + pkg + ": " + nameError; + return null; + } + return proc; + } + + public static String buildProcessName(String pkg, String defProc, + CharSequence procSeq, int flags, String[] separateProcesses, + String[] outError) { + if ((flags&PARSE_IGNORE_PROCESSES) != 0 && !"system".equals(procSeq)) { + return defProc != null ? defProc : pkg; + } + if (separateProcesses != null) { + for (int i=separateProcesses.length-1; i>=0; i--) { + String sp = separateProcesses[i]; + if (sp.equals(pkg) || sp.equals(defProc) || sp.equals(procSeq)) { + return pkg; + } + } + } + if (procSeq == null || procSeq.length() <= 0) { + return defProc; + } + return TextUtils.safeIntern(buildCompoundName(pkg, procSeq, "process", outError)); + } + + public static String buildTaskAffinityName(String pkg, String defProc, + CharSequence procSeq, String[] outError) { + if (procSeq == null) { + return defProc; + } + if (procSeq.length() <= 0) { + return null; + } + return buildCompoundName(pkg, procSeq, "taskAffinity", outError); + } + + private boolean parseKeySets(Package owner, Resources res, + XmlResourceParser parser, String[] outError) + 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 publicKeys = new ArrayMap(); + ArraySet upgradeKeySets = new ArraySet(); + ArrayMap> definedKeySets = new ArrayMap>(); + ArraySet 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(); + if (tagName.equals("key-set")) { + if (currentKeySet != null) { + outError[0] = "Improperly nested 'key-set' tag at " + + parser.getPositionDescription(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + final TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestKeySet); + final String keysetName = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestKeySet_name); + definedKeySets.put(keysetName, new ArraySet()); + currentKeySet = keysetName; + currentKeySetDepth = parser.getDepth(); + sa.recycle(); + } else if (tagName.equals("public-key")) { + if (currentKeySet == null) { + outError[0] = "Improperly nested 'key-set' tag at " + + parser.getPositionDescription(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + final TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestPublicKey); + final String publicKeyName = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestPublicKey_name); + final String encodedKey = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestPublicKey_value); + if (encodedKey == null && publicKeys.get(publicKeyName) == null) { + outError[0] = "'public-key' " + publicKeyName + " must define a public-key value" + + " on first use at " + parser.getPositionDescription(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + sa.recycle(); + return false; + } else if (encodedKey != null) { + PublicKey currentKey = 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."); + sa.recycle(); + 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 { + outError[0] = "Value of 'public-key' " + publicKeyName + + " conflicts with previously defined value at " + + parser.getPositionDescription(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + sa.recycle(); + return false; + } + } + definedKeySets.get(currentKeySet).add(publicKeyName); + sa.recycle(); + XmlUtils.skipCurrentTag(parser); + } else if (tagName.equals("upgrade-key-set")) { + final TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestUpgradeKeySet); + String name = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestUpgradeKeySet_name); + upgradeKeySets.add(name); + sa.recycle(); + XmlUtils.skipCurrentTag(parser); + } else if (RIGID_PARSER) { + outError[0] = "Bad element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } else { + Slog.w(TAG, "Unknown element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } + } + Set publicKeyNames = publicKeys.keySet(); + if (publicKeyNames.removeAll(definedKeySets.keySet())) { + outError[0] = "Package" + owner.packageName + " AndroidManifext.xml " + + "'key-set' and 'public-key' names must be distinct."; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + owner.mKeySetMapping = new ArrayMap>(); + for (ArrayMap.Entry> e: definedKeySets.entrySet()) { + final String keySetName = e.getKey(); + if (e.getValue().size() == 0) { + Slog.w(TAG, "Package" + owner.packageName + " AndroidManifext.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" + owner.packageName + " AndroidManifext.xml " + + "'key-set' " + keySetName + " contained improper 'public-key'" + + " tags. Not including in package's defined key-sets."); + continue; + } + owner.mKeySetMapping.put(keySetName, new ArraySet()); + for (String s : e.getValue()) { + owner.mKeySetMapping.get(keySetName).add(publicKeys.get(s)); + } + } + if (owner.mKeySetMapping.keySet().containsAll(upgradeKeySets)) { + owner.mUpgradeKeySets = upgradeKeySets; + } else { + outError[0] ="Package" + owner.packageName + " AndroidManifext.xml " + + "does not define all 'upgrade-key-set's ."; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + return true; + } + + private boolean parsePermissionGroup(Package owner, int flags, Resources res, + XmlResourceParser parser, String[] outError) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestPermissionGroup); + + int requestDetailResourceId = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestPermissionGroup_requestDetail, 0); + int backgroundRequestResourceId = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestPermissionGroup_backgroundRequest, + 0); + int backgroundRequestDetailResourceId = sa.getResourceId( + com.android.internal.R.styleable + .AndroidManifestPermissionGroup_backgroundRequestDetail, 0); + + PermissionGroup perm = new PermissionGroup(owner, requestDetailResourceId, + backgroundRequestResourceId, backgroundRequestDetailResourceId); + + if (!parsePackageItemInfo(owner, perm.info, outError, + "", sa, true /*nameRequired*/, + com.android.internal.R.styleable.AndroidManifestPermissionGroup_name, + com.android.internal.R.styleable.AndroidManifestPermissionGroup_label, + com.android.internal.R.styleable.AndroidManifestPermissionGroup_icon, + com.android.internal.R.styleable.AndroidManifestPermissionGroup_roundIcon, + com.android.internal.R.styleable.AndroidManifestPermissionGroup_logo, + com.android.internal.R.styleable.AndroidManifestPermissionGroup_banner)) { + sa.recycle(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + perm.info.descriptionRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestPermissionGroup_description, + 0); + perm.info.requestRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestPermissionGroup_request, 0); + perm.info.flags = sa.getInt( + com.android.internal.R.styleable.AndroidManifestPermissionGroup_permissionGroupFlags, 0); + perm.info.priority = sa.getInt( + com.android.internal.R.styleable.AndroidManifestPermissionGroup_priority, 0); + + sa.recycle(); + + if (!parseAllMetaData(res, parser, "", perm, + outError)) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.permissionGroups.add(perm); + + return true; + } + + private boolean parsePermission(Package owner, Resources res, + XmlResourceParser parser, String[] outError) + throws XmlPullParserException, IOException { + + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestPermission); + + String backgroundPermission = null; + if (sa.hasValue( + com.android.internal.R.styleable.AndroidManifestPermission_backgroundPermission)) { + if ("android".equals(owner.packageName)) { + backgroundPermission = sa.getNonResourceString( + com.android.internal.R.styleable + .AndroidManifestPermission_backgroundPermission); + } else { + Slog.w(TAG, owner.packageName + " defines a background permission. Only the " + + "'android' package can do that."); + } + } + + Permission perm = new Permission(owner, backgroundPermission); + if (!parsePackageItemInfo(owner, perm.info, outError, + "", sa, true /*nameRequired*/, + com.android.internal.R.styleable.AndroidManifestPermission_name, + com.android.internal.R.styleable.AndroidManifestPermission_label, + com.android.internal.R.styleable.AndroidManifestPermission_icon, + com.android.internal.R.styleable.AndroidManifestPermission_roundIcon, + com.android.internal.R.styleable.AndroidManifestPermission_logo, + com.android.internal.R.styleable.AndroidManifestPermission_banner)) { + sa.recycle(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + // Note: don't allow this value to be a reference to a resource + // that may change. + perm.info.group = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestPermission_permissionGroup); + if (perm.info.group != null) { + perm.info.group = perm.info.group.intern(); + } + + perm.info.descriptionRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestPermission_description, + 0); + + perm.info.requestRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestPermission_request, 0); + + perm.info.protectionLevel = sa.getInt( + com.android.internal.R.styleable.AndroidManifestPermission_protectionLevel, + PermissionInfo.PROTECTION_NORMAL); + + perm.info.flags = sa.getInt( + com.android.internal.R.styleable.AndroidManifestPermission_permissionFlags, 0); + + // For now only platform runtime permissions can be restricted + if (!perm.info.isRuntime() || !"android".equals(perm.info.packageName)) { + perm.info.flags &= ~PermissionInfo.FLAG_HARD_RESTRICTED; + perm.info.flags &= ~PermissionInfo.FLAG_SOFT_RESTRICTED; + } else { + // The platform does not get to specify conflicting permissions + if ((perm.info.flags & PermissionInfo.FLAG_HARD_RESTRICTED) != 0 + && (perm.info.flags & PermissionInfo.FLAG_SOFT_RESTRICTED) != 0) { + throw new IllegalStateException("Permission cannot be both soft and hard" + + " restricted: " + perm.info.name); + } + } + + sa.recycle(); + + if (perm.info.protectionLevel == -1) { + outError[0] = " does not specify protectionLevel"; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + perm.info.protectionLevel = PermissionInfo.fixProtectionLevel(perm.info.protectionLevel); + + if (perm.info.getProtectionFlags() != 0) { + if ( (perm.info.protectionLevel&PermissionInfo.PROTECTION_FLAG_INSTANT) == 0 + && (perm.info.protectionLevel&PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY) == 0 + && (perm.info.protectionLevel&PermissionInfo.PROTECTION_MASK_BASE) != + PermissionInfo.PROTECTION_SIGNATURE) { + outError[0] = " protectionLevel specifies a non-instant flag but is " + + "not based on signature type"; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + } + + if (!parseAllMetaData(res, parser, "", perm, outError)) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.permissions.add(perm); + + return true; + } + + private boolean parsePermissionTree(Package owner, Resources res, + XmlResourceParser parser, String[] outError) + throws XmlPullParserException, IOException { + Permission perm = new Permission(owner, (String) null); + + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestPermissionTree); + + if (!parsePackageItemInfo(owner, perm.info, outError, + "", sa, true /*nameRequired*/, + com.android.internal.R.styleable.AndroidManifestPermissionTree_name, + com.android.internal.R.styleable.AndroidManifestPermissionTree_label, + com.android.internal.R.styleable.AndroidManifestPermissionTree_icon, + com.android.internal.R.styleable.AndroidManifestPermissionTree_roundIcon, + com.android.internal.R.styleable.AndroidManifestPermissionTree_logo, + com.android.internal.R.styleable.AndroidManifestPermissionTree_banner)) { + sa.recycle(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + sa.recycle(); + + int index = perm.info.name.indexOf('.'); + if (index > 0) { + index = perm.info.name.indexOf('.', index+1); + } + if (index < 0) { + outError[0] = " name has less than three segments: " + + perm.info.name; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + perm.info.descriptionRes = 0; + perm.info.requestRes = 0; + perm.info.protectionLevel = PermissionInfo.PROTECTION_NORMAL; + perm.tree = true; + + if (!parseAllMetaData(res, parser, "", perm, + outError)) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.permissions.add(perm); + + return true; + } + + private Instrumentation parseInstrumentation(Package owner, Resources res, + XmlResourceParser parser, String[] outError) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestInstrumentation); + + if (mParseInstrumentationArgs == null) { + mParseInstrumentationArgs = new ParsePackageItemArgs(owner, outError, + com.android.internal.R.styleable.AndroidManifestInstrumentation_name, + com.android.internal.R.styleable.AndroidManifestInstrumentation_label, + com.android.internal.R.styleable.AndroidManifestInstrumentation_icon, + com.android.internal.R.styleable.AndroidManifestInstrumentation_roundIcon, + com.android.internal.R.styleable.AndroidManifestInstrumentation_logo, + com.android.internal.R.styleable.AndroidManifestInstrumentation_banner); + mParseInstrumentationArgs.tag = ""; + } + + mParseInstrumentationArgs.sa = sa; + + Instrumentation a = new Instrumentation(mParseInstrumentationArgs, + new InstrumentationInfo()); + if (outError[0] != null) { + sa.recycle(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + String str; + // Note: don't allow this value to be a reference to a resource + // that may change. + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestInstrumentation_targetPackage); + a.info.targetPackage = str != null ? str.intern() : null; + + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestInstrumentation_targetProcesses); + a.info.targetProcesses = str != null ? str.intern() : null; + + a.info.handleProfiling = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestInstrumentation_handleProfiling, + false); + + a.info.functionalTest = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestInstrumentation_functionalTest, + false); + + sa.recycle(); + + if (a.info.targetPackage == null) { + outError[0] = " does not specify targetPackage"; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + if (!parseAllMetaData(res, parser, "", a, + outError)) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return null; + } + + owner.instrumentation.add(a); + + return a; + } + + /** + * Parse the {@code application} XML tree at the current parse location in a + * base APK manifest. + *

+ * When adding new features, carefully consider if they should also be + * supported by split APKs. + */ + @UnsupportedAppUsage + private boolean parseBaseApplication(Package owner, Resources res, + XmlResourceParser parser, int flags, String[] outError) + throws XmlPullParserException, IOException { + final ApplicationInfo ai = owner.applicationInfo; + final String pkgName = owner.applicationInfo.packageName; + + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestApplication); + + ai.iconRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestApplication_icon, 0); + ai.roundIconRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestApplication_roundIcon, 0); + + if (!parsePackageItemInfo(owner, ai, outError, + "", sa, false /*nameRequired*/, + com.android.internal.R.styleable.AndroidManifestApplication_name, + com.android.internal.R.styleable.AndroidManifestApplication_label, + com.android.internal.R.styleable.AndroidManifestApplication_icon, + com.android.internal.R.styleable.AndroidManifestApplication_roundIcon, + com.android.internal.R.styleable.AndroidManifestApplication_logo, + com.android.internal.R.styleable.AndroidManifestApplication_banner)) { + sa.recycle(); + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + if (ai.name != null) { + ai.className = ai.name; + } + + String manageSpaceActivity = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestApplication_manageSpaceActivity, + Configuration.NATIVE_CONFIG_VERSION); + if (manageSpaceActivity != null) { + ai.manageSpaceActivityName = buildClassName(pkgName, manageSpaceActivity, + outError); + } + + boolean allowBackup = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_allowBackup, true); + if (allowBackup) { + ai.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP; + + // backupAgent, killAfterRestore, fullBackupContent, backupInForeground, + // and restoreAnyVersion are only relevant if backup is possible for the + // given application. + String backupAgent = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestApplication_backupAgent, + Configuration.NATIVE_CONFIG_VERSION); + if (backupAgent != null) { + ai.backupAgentName = buildClassName(pkgName, backupAgent, outError); + if (DEBUG_BACKUP) { + Slog.v(TAG, "android:backupAgent = " + ai.backupAgentName + + " from " + pkgName + "+" + backupAgent); + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_killAfterRestore, + true)) { + ai.flags |= ApplicationInfo.FLAG_KILL_AFTER_RESTORE; + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_restoreAnyVersion, + false)) { + ai.flags |= ApplicationInfo.FLAG_RESTORE_ANY_VERSION; + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_fullBackupOnly, + false)) { + ai.flags |= ApplicationInfo.FLAG_FULL_BACKUP_ONLY; + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_backupInForeground, + false)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_BACKUP_IN_FOREGROUND; + } + } + + TypedValue v = sa.peekValue( + com.android.internal.R.styleable.AndroidManifestApplication_fullBackupContent); + if (v != null && (ai.fullBackupContent = v.resourceId) == 0) { + if (DEBUG_BACKUP) { + Slog.v(TAG, "fullBackupContent specified as boolean=" + + (v.data == 0 ? "false" : "true")); + } + // "false" => -1, "true" => 0 + ai.fullBackupContent = (v.data == 0 ? -1 : 0); + } + if (DEBUG_BACKUP) { + Slog.v(TAG, "fullBackupContent=" + ai.fullBackupContent + " for " + pkgName); + } + } + + ai.theme = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestApplication_theme, 0); + ai.descriptionRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestApplication_description, 0); + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_persistent, + false)) { + // Check if persistence is based on a feature being present + final String requiredFeature = sa.getNonResourceString(com.android.internal.R.styleable + .AndroidManifestApplication_persistentWhenFeatureAvailable); + if (requiredFeature == null || mCallback.hasFeature(requiredFeature)) { + ai.flags |= ApplicationInfo.FLAG_PERSISTENT; + } + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_requiredForAllUsers, + false)) { + owner.mRequiredForAllUsers = true; + } + + String restrictedAccountType = sa.getString(com.android.internal.R.styleable + .AndroidManifestApplication_restrictedAccountType); + if (restrictedAccountType != null && restrictedAccountType.length() > 0) { + owner.mRestrictedAccountType = restrictedAccountType; + } + + String requiredAccountType = sa.getString(com.android.internal.R.styleable + .AndroidManifestApplication_requiredAccountType); + if (requiredAccountType != null && requiredAccountType.length() > 0) { + owner.mRequiredAccountType = requiredAccountType; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_debuggable, + false)) { + ai.flags |= ApplicationInfo.FLAG_DEBUGGABLE; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_vmSafeMode, + false)) { + ai.flags |= ApplicationInfo.FLAG_VM_SAFE_MODE; + } + + owner.baseHardwareAccelerated = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_hardwareAccelerated, + owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH); + if (owner.baseHardwareAccelerated) { + ai.flags |= ApplicationInfo.FLAG_HARDWARE_ACCELERATED; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_hasCode, + true)) { + ai.flags |= ApplicationInfo.FLAG_HAS_CODE; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_allowTaskReparenting, + false)) { + ai.flags |= ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_allowClearUserData, + true)) { + ai.flags |= ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA; + } + + // The parent package controls installation, hence specify test only installs. + if (owner.parentPackage == null) { + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_testOnly, + false)) { + ai.flags |= ApplicationInfo.FLAG_TEST_ONLY; + } + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_largeHeap, + false)) { + ai.flags |= ApplicationInfo.FLAG_LARGE_HEAP; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_usesCleartextTraffic, + owner.applicationInfo.targetSdkVersion < Build.VERSION_CODES.P)) { + ai.flags |= ApplicationInfo.FLAG_USES_CLEARTEXT_TRAFFIC; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_supportsRtl, + false /* default is no RTL support*/)) { + ai.flags |= ApplicationInfo.FLAG_SUPPORTS_RTL; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_multiArch, + false)) { + ai.flags |= ApplicationInfo.FLAG_MULTIARCH; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_extractNativeLibs, + true)) { + ai.flags |= ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS; + } + + if (sa.getBoolean( + R.styleable.AndroidManifestApplication_useEmbeddedDex, + false)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_USE_EMBEDDED_DEX; + } + + if (sa.getBoolean( + R.styleable.AndroidManifestApplication_defaultToDeviceProtectedStorage, + false)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE; + } + if (sa.getBoolean( + R.styleable.AndroidManifestApplication_directBootAware, + false)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_DIRECT_BOOT_AWARE; + } + + if (sa.hasValueOrEmpty(R.styleable.AndroidManifestApplication_resizeableActivity)) { + if (sa.getBoolean(R.styleable.AndroidManifestApplication_resizeableActivity, true)) { + ai.privateFlags |= PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE; + } else { + ai.privateFlags |= PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE; + } + } else if (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.N) { + ai.privateFlags |= PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION; + } + + if (sa.getBoolean( + com.android.internal.R.styleable + .AndroidManifestApplication_allowClearUserDataOnFailedRestore, + true)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE; + } + + if (sa.getBoolean( + R.styleable.AndroidManifestApplication_allowAudioPlaybackCapture, + owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.Q)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_ALLOW_AUDIO_PLAYBACK_CAPTURE; + } + + if (sa.getBoolean( + R.styleable.AndroidManifestApplication_requestLegacyExternalStorage, + owner.applicationInfo.targetSdkVersion < Build.VERSION_CODES.Q)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE; + } + + if (sa.getBoolean( + R.styleable.AndroidManifestApplication_allowNativeHeapPointerTagging, true)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_ALLOW_NATIVE_HEAP_POINTER_TAGGING; + } + + ai.maxAspectRatio = sa.getFloat(R.styleable.AndroidManifestApplication_maxAspectRatio, 0); + ai.minAspectRatio = sa.getFloat(R.styleable.AndroidManifestApplication_minAspectRatio, 0); + + ai.networkSecurityConfigRes = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestApplication_networkSecurityConfig, + 0); + ai.category = sa.getInt( + com.android.internal.R.styleable.AndroidManifestApplication_appCategory, + ApplicationInfo.CATEGORY_UNDEFINED); + + String str; + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestApplication_permission, 0); + ai.permission = (str != null && str.length() > 0) ? str.intern() : null; + + if (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.FROYO) { + str = sa.getNonConfigurationString( + com.android.internal.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. + str = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestApplication_taskAffinity); + } + ai.taskAffinity = buildTaskAffinityName(ai.packageName, ai.packageName, + str, outError); + String factory = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestApplication_appComponentFactory); + if (factory != null) { + ai.appComponentFactory = buildClassName(ai.packageName, factory, outError); + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_usesNonSdkApi, false)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_USES_NON_SDK_API; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_hasFragileUserData, + false)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_HAS_FRAGILE_USER_DATA; + } + + if (outError[0] == null) { + CharSequence pname; + if (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.FROYO) { + pname = sa.getNonConfigurationString( + com.android.internal.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( + com.android.internal.R.styleable.AndroidManifestApplication_process); + } + ai.processName = buildProcessName(ai.packageName, null, pname, + flags, mSeparateProcesses, outError); + + ai.enabled = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_enabled, true); + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_isGame, false)) { + ai.flags |= ApplicationInfo.FLAG_IS_GAME; + } + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_cantSaveState, + false)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE; + + // A heavy-weight application can not be in a custom process. + // We can do direct compare because we intern all strings. + if (ai.processName != null && !ai.processName.equals(ai.packageName)) { + outError[0] = "cantSaveState applications can not use custom processes"; + } + } + } + + ai.uiOptions = sa.getInt( + com.android.internal.R.styleable.AndroidManifestApplication_uiOptions, 0); + + ai.classLoaderName = sa.getString( + com.android.internal.R.styleable.AndroidManifestApplication_classLoader); + if (ai.classLoaderName != null + && !ClassLoaderFactory.isValidClassLoaderName(ai.classLoaderName)) { + outError[0] = "Invalid class loader name: " + ai.classLoaderName; + } + + ai.zygotePreloadName = sa.getString( + com.android.internal.R.styleable.AndroidManifestApplication_zygotePreloadName); + + sa.recycle(); + + if (outError[0] != null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + final int innerDepth = parser.getDepth(); + // IMPORTANT: These must only be cached for a single to avoid components + // getting added to the wrong package. + final CachedComponentArgs cachedArgs = new CachedComponentArgs(); + int type; + boolean hasActivityOrder = false; + boolean hasReceiverOrder = false; + boolean hasServiceOrder = false; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("activity")) { + Activity a = parseActivity(owner, res, parser, flags, outError, cachedArgs, false, + owner.baseHardwareAccelerated); + if (a == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + hasActivityOrder |= (a.order != 0); + owner.activities.add(a); + + } else if (tagName.equals("receiver")) { + Activity a = parseActivity(owner, res, parser, flags, outError, cachedArgs, + true, false); + if (a == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + hasReceiverOrder |= (a.order != 0); + owner.receivers.add(a); + + } else if (tagName.equals("service")) { + Service s = parseService(owner, res, parser, flags, outError, cachedArgs); + if (s == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + hasServiceOrder |= (s.order != 0); + owner.services.add(s); + + } else if (tagName.equals("provider")) { + Provider p = parseProvider(owner, res, parser, flags, outError, cachedArgs); + if (p == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.providers.add(p); + + } else if (tagName.equals("activity-alias")) { + Activity a = parseActivityAlias(owner, res, parser, flags, outError, cachedArgs); + if (a == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + hasActivityOrder |= (a.order != 0); + owner.activities.add(a); + + } else if (parser.getName().equals("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) + if ((owner.mAppMetaData = parseMetaData(res, parser, owner.mAppMetaData, + outError)) == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + } else if (tagName.equals("static-library")) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestStaticLibrary); + + // Note: don't allow this value to be a reference to a resource + // that may change. + final String lname = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestStaticLibrary_name); + final int version = sa.getInt( + com.android.internal.R.styleable.AndroidManifestStaticLibrary_version, -1); + final int versionMajor = sa.getInt( + com.android.internal.R.styleable.AndroidManifestStaticLibrary_versionMajor, + 0); + + sa.recycle(); + + // Since the app canot run without a static lib - fail if malformed + if (lname == null || version < 0) { + outError[0] = "Bad static-library declaration name: " + lname + + " version: " + version; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + XmlUtils.skipCurrentTag(parser); + return false; + } + + if (owner.mSharedUserId != null) { + outError[0] = "sharedUserId not allowed in static shared library"; + mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID; + XmlUtils.skipCurrentTag(parser); + return false; + } + + if (owner.staticSharedLibName != null) { + outError[0] = "Multiple static-shared libs for package " + pkgName; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + XmlUtils.skipCurrentTag(parser); + return false; + } + + owner.staticSharedLibName = lname.intern(); + if (version >= 0) { + owner.staticSharedLibVersion = + PackageInfo.composeLongVersionCode(versionMajor, version); + } else { + owner.staticSharedLibVersion = version; + } + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_STATIC_SHARED_LIBRARY; + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals("library")) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestLibrary); + + // Note: don't allow this value to be a reference to a resource + // that may change. + String lname = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestLibrary_name); + + sa.recycle(); + + if (lname != null) { + lname = lname.intern(); + if (!ArrayUtils.contains(owner.libraryNames, lname)) { + owner.libraryNames = ArrayUtils.add( + owner.libraryNames, lname); + } + } + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals("uses-static-library")) { + if (!parseUsesStaticLibrary(owner, res, parser, outError)) { + return false; + } + + } else if (tagName.equals("uses-library")) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestUsesLibrary); + + // Note: don't allow this value to be a reference to a resource + // that may change. + String lname = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestUsesLibrary_name); + boolean req = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesLibrary_required, + true); + + sa.recycle(); + + if (lname != null) { + lname = lname.intern(); + if (req) { + owner.usesLibraries = ArrayUtils.add(owner.usesLibraries, lname); + } else { + owner.usesOptionalLibraries = ArrayUtils.add( + owner.usesOptionalLibraries, lname); + } + } + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals("uses-package")) { + // Dependencies for app installers; we don't currently try to + // enforce this. + XmlUtils.skipCurrentTag(parser); + } else if (tagName.equals("profileable")) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestProfileable); + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProfileable_shell, false)) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PROFILEABLE_BY_SHELL; + } + XmlUtils.skipCurrentTag(parser); + } else { + if (!RIGID_PARSER) { + Slog.w(TAG, "Unknown element under : " + tagName + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } else { + outError[0] = "Bad element under : " + tagName; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + } + } + + if (TextUtils.isEmpty(owner.staticSharedLibName)) { + // Add a hidden app detail activity to normal apps which forwards user to App Details + // page. + Activity a = generateAppDetailsHiddenActivity(owner, flags, outError, + owner.baseHardwareAccelerated); + owner.activities.add(a); + } + + if (hasActivityOrder) { + Collections.sort(owner.activities, (a1, a2) -> Integer.compare(a2.order, a1.order)); + } + if (hasReceiverOrder) { + Collections.sort(owner.receivers, (r1, r2) -> Integer.compare(r2.order, r1.order)); + } + if (hasServiceOrder) { + Collections.sort(owner.services, (s1, s2) -> Integer.compare(s2.order, s1.order)); + } + // Must be ran 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(owner); + setMinAspectRatio(owner); + setSupportsSizeChanges(owner); + + if (hasDomainURLs(owner)) { + owner.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS; + } else { + owner.applicationInfo.privateFlags &= ~ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS; + } + + return true; + } + + /** + * Check if one of the IntentFilter as both actions DEFAULT / VIEW and a HTTP/HTTPS data URI + */ + private static boolean hasDomainURLs(Package pkg) { + if (pkg == null || pkg.activities == null) return false; + final ArrayList activities = pkg.activities; + final int countActivities = activities.size(); + for (int n=0; n filters = activity.intents; + if (filters == null) continue; + final int countFilters = filters.size(); + for (int m=0; msplit APK manifest. + *

+ * 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 boolean parseSplitApplication(Package owner, Resources res, XmlResourceParser parser, + int flags, int splitIndex, String[] outError) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestApplication); + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_hasCode, true)) { + owner.splitFlags[splitIndex] |= ApplicationInfo.FLAG_HAS_CODE; + } + + final String classLoaderName = sa.getString( + com.android.internal.R.styleable.AndroidManifestApplication_classLoader); + if (classLoaderName == null || ClassLoaderFactory.isValidClassLoaderName(classLoaderName)) { + owner.applicationInfo.splitClassLoaderNames[splitIndex] = classLoaderName; + } else { + outError[0] = "Invalid class loader name: " + classLoaderName; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + final int innerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + ComponentInfo parsedComponent = null; + + // IMPORTANT: These must only be cached for a single to avoid components + // getting added to the wrong package. + final CachedComponentArgs cachedArgs = new CachedComponentArgs(); + String tagName = parser.getName(); + if (tagName.equals("activity")) { + Activity a = parseActivity(owner, res, parser, flags, outError, cachedArgs, false, + owner.baseHardwareAccelerated); + if (a == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.activities.add(a); + parsedComponent = a.info; + + } else if (tagName.equals("receiver")) { + Activity a = parseActivity(owner, res, parser, flags, outError, cachedArgs, + true, false); + if (a == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.receivers.add(a); + parsedComponent = a.info; + + } else if (tagName.equals("service")) { + Service s = parseService(owner, res, parser, flags, outError, cachedArgs); + if (s == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.services.add(s); + parsedComponent = s.info; + + } else if (tagName.equals("provider")) { + Provider p = parseProvider(owner, res, parser, flags, outError, cachedArgs); + if (p == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.providers.add(p); + parsedComponent = p.info; + + } else if (tagName.equals("activity-alias")) { + Activity a = parseActivityAlias(owner, res, parser, flags, outError, cachedArgs); + if (a == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + owner.activities.add(a); + parsedComponent = a.info; + + } else if (parser.getName().equals("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) + if ((owner.mAppMetaData = parseMetaData(res, parser, owner.mAppMetaData, + outError)) == null) { + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + + } else if (tagName.equals("uses-static-library")) { + if (!parseUsesStaticLibrary(owner, res, parser, outError)) { + return false; + } + + } else if (tagName.equals("uses-library")) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestUsesLibrary); + + // Note: don't allow this value to be a reference to a resource + // that may change. + String lname = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestUsesLibrary_name); + boolean req = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestUsesLibrary_required, + true); + + sa.recycle(); + + if (lname != null) { + lname = lname.intern(); + if (req) { + // Upgrade to treat as stronger constraint + owner.usesLibraries = ArrayUtils.add(owner.usesLibraries, lname); + owner.usesOptionalLibraries = ArrayUtils.remove( + owner.usesOptionalLibraries, lname); + } else { + // Ignore if someone already defined as required + if (!ArrayUtils.contains(owner.usesLibraries, lname)) { + owner.usesOptionalLibraries = ArrayUtils.add( + owner.usesOptionalLibraries, lname); + } + } + } + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals("uses-package")) { + // Dependencies for app installers; we don't currently try to + // enforce this. + XmlUtils.skipCurrentTag(parser); + + } else { + if (!RIGID_PARSER) { + Slog.w(TAG, "Unknown element under : " + tagName + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } else { + outError[0] = "Bad element under : " + tagName; + mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + return false; + } + } + + if (parsedComponent != null && parsedComponent.splitName == 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. + parsedComponent.splitName = owner.splitNames[splitIndex]; + } + } + + return true; + } + + private static boolean parsePackageItemInfo(Package owner, PackageItemInfo outInfo, + String[] outError, String tag, TypedArray sa, boolean nameRequired, + int nameRes, int labelRes, int iconRes, int roundIconRes, int logoRes, int bannerRes) { + // This case can only happen in unit tests where we sometimes need to create fakes + // of various package parser data structures. + if (sa == null) { + outError[0] = tag + " does not contain any attributes"; + return false; + } + + String name = sa.getNonConfigurationString(nameRes, 0); + if (name == null) { + if (nameRequired) { + outError[0] = tag + " does not specify android:name"; + return false; + } + } else { + String outInfoName + = buildClassName(owner.applicationInfo.packageName, name, outError); + if (PackageManager.APP_DETAILS_ACTIVITY_CLASS_NAME.equals(outInfoName)) { + outError[0] = tag + " invalid android:name"; + return false; + } + outInfo.name = outInfoName; + if (outInfoName == null) { + return false; + } + } + + int roundIconVal = sUseRoundIcon ? sa.getResourceId(roundIconRes, 0) : 0; + if (roundIconVal != 0) { + outInfo.icon = roundIconVal; + outInfo.nonLocalizedLabel = null; + } else { + int iconVal = sa.getResourceId(iconRes, 0); + if (iconVal != 0) { + outInfo.icon = iconVal; + outInfo.nonLocalizedLabel = null; + } + } + + int logoVal = sa.getResourceId(logoRes, 0); + if (logoVal != 0) { + outInfo.logo = logoVal; + } + + int bannerVal = sa.getResourceId(bannerRes, 0); + if (bannerVal != 0) { + outInfo.banner = bannerVal; + } + + TypedValue v = sa.peekValue(labelRes); + if (v != null && (outInfo.labelRes=v.resourceId) == 0) { + outInfo.nonLocalizedLabel = v.coerceToString(); + } + + outInfo.packageName = owner.packageName; + + return true; + } + + /** + * 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. + */ + private @NonNull PackageParser.Activity generateAppDetailsHiddenActivity( + PackageParser.Package owner, int flags, String[] outError, + boolean hardwareAccelerated) { + + // Build custom App Details activity info instead of parsing it from xml + Activity a = new Activity(owner, PackageManager.APP_DETAILS_ACTIVITY_CLASS_NAME, + new ActivityInfo()); + a.owner = owner; + a.setPackageName(owner.packageName); + + a.info.theme = android.R.style.Theme_NoDisplay; + a.info.exported = true; + a.info.name = PackageManager.APP_DETAILS_ACTIVITY_CLASS_NAME; + a.info.processName = owner.applicationInfo.processName; + a.info.uiOptions = a.info.applicationInfo.uiOptions; + a.info.taskAffinity = buildTaskAffinityName(owner.packageName, owner.packageName, + ":app_details", outError); + a.info.enabled = true; + a.info.launchMode = ActivityInfo.LAUNCH_MULTIPLE; + a.info.documentLaunchMode = ActivityInfo.DOCUMENT_LAUNCH_NONE; + a.info.maxRecents = ActivityTaskManager.getDefaultAppRecentsLimitStatic(); + a.info.configChanges = getActivityConfigChanges(0, 0); + a.info.softInputMode = 0; + a.info.persistableMode = ActivityInfo.PERSIST_NEVER; + a.info.screenOrientation = SCREEN_ORIENTATION_UNSPECIFIED; + a.info.resizeMode = RESIZE_MODE_FORCE_RESIZEABLE; + a.info.lockTaskLaunchMode = 0; + a.info.directBootAware = false; + a.info.rotationAnimation = ROTATION_ANIMATION_UNSPECIFIED; + a.info.colorMode = ActivityInfo.COLOR_MODE_DEFAULT; + if (hardwareAccelerated) { + a.info.flags |= ActivityInfo.FLAG_HARDWARE_ACCELERATED; + } + return a; + } + + private Activity parseActivity(Package owner, Resources res, + XmlResourceParser parser, int flags, String[] outError, CachedComponentArgs cachedArgs, + boolean receiver, boolean hardwareAccelerated) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestActivity); + + if (cachedArgs.mActivityArgs == null) { + cachedArgs.mActivityArgs = new ParseComponentArgs(owner, outError, + R.styleable.AndroidManifestActivity_name, + R.styleable.AndroidManifestActivity_label, + R.styleable.AndroidManifestActivity_icon, + R.styleable.AndroidManifestActivity_roundIcon, + R.styleable.AndroidManifestActivity_logo, + R.styleable.AndroidManifestActivity_banner, + mSeparateProcesses, + R.styleable.AndroidManifestActivity_process, + R.styleable.AndroidManifestActivity_description, + R.styleable.AndroidManifestActivity_enabled); + } + + cachedArgs.mActivityArgs.tag = receiver ? "" : ""; + cachedArgs.mActivityArgs.sa = sa; + cachedArgs.mActivityArgs.flags = flags; + + Activity a = new Activity(cachedArgs.mActivityArgs, new ActivityInfo()); + if (outError[0] != null) { + sa.recycle(); + return null; + } + + boolean setExported = sa.hasValue(R.styleable.AndroidManifestActivity_exported); + if (setExported) { + a.info.exported = sa.getBoolean(R.styleable.AndroidManifestActivity_exported, false); + } + + a.info.theme = sa.getResourceId(R.styleable.AndroidManifestActivity_theme, 0); + + a.info.uiOptions = sa.getInt(R.styleable.AndroidManifestActivity_uiOptions, + a.info.applicationInfo.uiOptions); + + String parentName = sa.getNonConfigurationString( + R.styleable.AndroidManifestActivity_parentActivityName, + Configuration.NATIVE_CONFIG_VERSION); + if (parentName != null) { + String parentClassName = buildClassName(a.info.packageName, parentName, outError); + if (outError[0] == null) { + a.info.parentActivityName = parentClassName; + } else { + Log.e(TAG, "Activity " + a.info.name + " specified invalid parentActivityName " + + parentName); + outError[0] = null; + } + } + + String str; + str = sa.getNonConfigurationString(R.styleable.AndroidManifestActivity_permission, 0); + if (str == null) { + a.info.permission = owner.applicationInfo.permission; + } else { + a.info.permission = str.length() > 0 ? str.toString().intern() : null; + } + + str = sa.getNonConfigurationString( + R.styleable.AndroidManifestActivity_taskAffinity, + Configuration.NATIVE_CONFIG_VERSION); + a.info.taskAffinity = buildTaskAffinityName(owner.applicationInfo.packageName, + owner.applicationInfo.taskAffinity, str, outError); + + a.info.splitName = + sa.getNonConfigurationString(R.styleable.AndroidManifestActivity_splitName, 0); + + a.info.flags = 0; + if (sa.getBoolean( + R.styleable.AndroidManifestActivity_multiprocess, false)) { + a.info.flags |= ActivityInfo.FLAG_MULTIPROCESS; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_finishOnTaskLaunch, false)) { + a.info.flags |= ActivityInfo.FLAG_FINISH_ON_TASK_LAUNCH; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_clearTaskOnLaunch, false)) { + a.info.flags |= ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_noHistory, false)) { + a.info.flags |= ActivityInfo.FLAG_NO_HISTORY; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_alwaysRetainTaskState, false)) { + a.info.flags |= ActivityInfo.FLAG_ALWAYS_RETAIN_TASK_STATE; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_stateNotNeeded, false)) { + a.info.flags |= ActivityInfo.FLAG_STATE_NOT_NEEDED; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_excludeFromRecents, false)) { + a.info.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_allowTaskReparenting, + (owner.applicationInfo.flags&ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING) != 0)) { + a.info.flags |= ActivityInfo.FLAG_ALLOW_TASK_REPARENTING; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_finishOnCloseSystemDialogs, false)) { + a.info.flags |= ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_showOnLockScreen, false) + || sa.getBoolean(R.styleable.AndroidManifestActivity_showForAllUsers, false)) { + a.info.flags |= ActivityInfo.FLAG_SHOW_FOR_ALL_USERS; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_immersive, false)) { + a.info.flags |= ActivityInfo.FLAG_IMMERSIVE; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_systemUserOnly, false)) { + a.info.flags |= ActivityInfo.FLAG_SYSTEM_USER_ONLY; + } + + if (!receiver) { + if (sa.getBoolean(R.styleable.AndroidManifestActivity_hardwareAccelerated, + hardwareAccelerated)) { + a.info.flags |= ActivityInfo.FLAG_HARDWARE_ACCELERATED; + } + + a.info.launchMode = sa.getInt( + R.styleable.AndroidManifestActivity_launchMode, ActivityInfo.LAUNCH_MULTIPLE); + a.info.documentLaunchMode = sa.getInt( + R.styleable.AndroidManifestActivity_documentLaunchMode, + ActivityInfo.DOCUMENT_LAUNCH_NONE); + a.info.maxRecents = sa.getInt( + R.styleable.AndroidManifestActivity_maxRecents, + ActivityTaskManager.getDefaultAppRecentsLimitStatic()); + a.info.configChanges = getActivityConfigChanges( + sa.getInt(R.styleable.AndroidManifestActivity_configChanges, 0), + sa.getInt(R.styleable.AndroidManifestActivity_recreateOnConfigChanges, 0)); + a.info.softInputMode = sa.getInt( + R.styleable.AndroidManifestActivity_windowSoftInputMode, 0); + + a.info.persistableMode = sa.getInteger( + R.styleable.AndroidManifestActivity_persistableMode, + ActivityInfo.PERSIST_ROOT_ONLY); + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_allowEmbedded, false)) { + a.info.flags |= ActivityInfo.FLAG_ALLOW_EMBEDDED; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_autoRemoveFromRecents, false)) { + a.info.flags |= ActivityInfo.FLAG_AUTO_REMOVE_FROM_RECENTS; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_relinquishTaskIdentity, false)) { + a.info.flags |= ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_resumeWhilePausing, false)) { + a.info.flags |= ActivityInfo.FLAG_RESUME_WHILE_PAUSING; + } + + a.info.screenOrientation = sa.getInt( + R.styleable.AndroidManifestActivity_screenOrientation, + SCREEN_ORIENTATION_UNSPECIFIED); + + setActivityResizeMode(a.info, sa, owner); + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_supportsPictureInPicture, + false)) { + a.info.flags |= FLAG_SUPPORTS_PICTURE_IN_PICTURE; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_alwaysFocusable, false)) { + a.info.flags |= FLAG_ALWAYS_FOCUSABLE; + } + + if (sa.hasValue(R.styleable.AndroidManifestActivity_maxAspectRatio) + && sa.getType(R.styleable.AndroidManifestActivity_maxAspectRatio) + == TypedValue.TYPE_FLOAT) { + a.setMaxAspectRatio(sa.getFloat(R.styleable.AndroidManifestActivity_maxAspectRatio, + 0 /*default*/)); + } + + if (sa.hasValue(R.styleable.AndroidManifestActivity_minAspectRatio) + && sa.getType(R.styleable.AndroidManifestActivity_minAspectRatio) + == TypedValue.TYPE_FLOAT) { + a.setMinAspectRatio(sa.getFloat(R.styleable.AndroidManifestActivity_minAspectRatio, + 0 /*default*/)); + } + + a.info.lockTaskLaunchMode = + sa.getInt(R.styleable.AndroidManifestActivity_lockTaskMode, 0); + + a.info.directBootAware = sa.getBoolean( + R.styleable.AndroidManifestActivity_directBootAware, + false); + + a.info.requestedVrComponent = + sa.getString(R.styleable.AndroidManifestActivity_enableVrMode); + + a.info.rotationAnimation = + sa.getInt(R.styleable.AndroidManifestActivity_rotationAnimation, ROTATION_ANIMATION_UNSPECIFIED); + + a.info.colorMode = sa.getInt(R.styleable.AndroidManifestActivity_colorMode, + ActivityInfo.COLOR_MODE_DEFAULT); + + if (sa.getBoolean( + R.styleable.AndroidManifestActivity_preferMinimalPostProcessing, false)) { + a.info.flags |= ActivityInfo.FLAG_PREFER_MINIMAL_POST_PROCESSING; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_showWhenLocked, false)) { + a.info.flags |= ActivityInfo.FLAG_SHOW_WHEN_LOCKED; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_turnScreenOn, false)) { + a.info.flags |= ActivityInfo.FLAG_TURN_SCREEN_ON; + } + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_inheritShowWhenLocked, false)) { + a.info.privateFlags |= ActivityInfo.FLAG_INHERIT_SHOW_WHEN_LOCKED; + } + } else { + a.info.launchMode = ActivityInfo.LAUNCH_MULTIPLE; + a.info.configChanges = 0; + + if (sa.getBoolean(R.styleable.AndroidManifestActivity_singleUser, false)) { + a.info.flags |= ActivityInfo.FLAG_SINGLE_USER; + } + + a.info.directBootAware = sa.getBoolean( + R.styleable.AndroidManifestActivity_directBootAware, + false); + } + + if (a.info.directBootAware) { + owner.applicationInfo.privateFlags |= + ApplicationInfo.PRIVATE_FLAG_PARTIALLY_DIRECT_BOOT_AWARE; + } + + // can't make this final; we may set it later via meta-data + boolean visibleToEphemeral = + sa.getBoolean(R.styleable.AndroidManifestActivity_visibleToInstantApps, false); + if (visibleToEphemeral) { + a.info.flags |= ActivityInfo.FLAG_VISIBLE_TO_INSTANT_APP; + owner.visibleToInstantApps = true; + } + + sa.recycle(); + + if (receiver && (owner.applicationInfo.privateFlags + &ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0) { + // A heavy-weight application can not have receives in its main process + // We can do direct compare because we intern all strings. + if (a.info.processName == owner.packageName) { + outError[0] = "Heavy-weight applications can not have receivers in main process"; + } + } + + if (outError[0] != null) { + return null; + } + + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getName().equals("intent-filter")) { + ActivityIntentInfo intent = new ActivityIntentInfo(a); + if (!parseIntent(res, parser, true /*allowGlobs*/, true /*allowAutoVerify*/, + intent, outError)) { + return null; + } + if (intent.countActions() == 0) { + Slog.w(TAG, "No actions in intent filter at " + + mArchiveSourcePath + " " + + parser.getPositionDescription()); + } else { + a.order = Math.max(intent.getOrder(), a.order); + a.intents.add(intent); + } + // adjust activity flags when we implicitly expose it via a browsable filter + final int visibility = visibleToEphemeral + ? IntentFilter.VISIBILITY_EXPLICIT + : !receiver && isImplicitlyExposedIntent(intent) + ? IntentFilter.VISIBILITY_IMPLICIT + : IntentFilter.VISIBILITY_NONE; + intent.setVisibilityToInstantApp(visibility); + if (intent.isVisibleToInstantApp()) { + a.info.flags |= ActivityInfo.FLAG_VISIBLE_TO_INSTANT_APP; + } + if (intent.isImplicitlyVisibleToInstantApp()) { + a.info.flags |= ActivityInfo.FLAG_IMPLICITLY_VISIBLE_TO_INSTANT_APP; + } + if (LOG_UNSAFE_BROADCASTS && receiver + && (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O)) { + for (int i = 0; i < intent.countActions(); i++) { + final String action = intent.getAction(i); + if (action == null || !action.startsWith("android.")) continue; + if (!SAFE_BROADCASTS.contains(action)) { + Slog.w(TAG, "Broadcast " + action + " may never be delivered to " + + owner.packageName + " as requested at: " + + parser.getPositionDescription()); + } + } + } + } else if (!receiver && parser.getName().equals("preferred")) { + ActivityIntentInfo intent = new ActivityIntentInfo(a); + if (!parseIntent(res, parser, false /*allowGlobs*/, false /*allowAutoVerify*/, + intent, outError)) { + return null; + } + if (intent.countActions() == 0) { + Slog.w(TAG, "No actions in preferred at " + + mArchiveSourcePath + " " + + parser.getPositionDescription()); + } else { + if (owner.preferredActivityFilters == null) { + owner.preferredActivityFilters = new ArrayList(); + } + owner.preferredActivityFilters.add(intent); + } + // adjust activity flags when we implicitly expose it via a browsable filter + final int visibility = visibleToEphemeral + ? IntentFilter.VISIBILITY_EXPLICIT + : !receiver && isImplicitlyExposedIntent(intent) + ? IntentFilter.VISIBILITY_IMPLICIT + : IntentFilter.VISIBILITY_NONE; + intent.setVisibilityToInstantApp(visibility); + if (intent.isVisibleToInstantApp()) { + a.info.flags |= ActivityInfo.FLAG_VISIBLE_TO_INSTANT_APP; + } + if (intent.isImplicitlyVisibleToInstantApp()) { + a.info.flags |= ActivityInfo.FLAG_IMPLICITLY_VISIBLE_TO_INSTANT_APP; + } + } else if (parser.getName().equals("meta-data")) { + if ((a.metaData = parseMetaData(res, parser, a.metaData, + outError)) == null) { + return null; + } + } else if (!receiver && parser.getName().equals("layout")) { + parseLayout(res, parser, a); + } else { + if (!RIGID_PARSER) { + Slog.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); + if (receiver) { + Slog.w(TAG, "Unknown element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + } else { + Slog.w(TAG, "Unknown element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + } + XmlUtils.skipCurrentTag(parser); + continue; + } else { + if (receiver) { + outError[0] = "Bad element under : " + parser.getName(); + } else { + outError[0] = "Bad element under : " + parser.getName(); + } + return null; + } + } + } + + resolveWindowLayout(a); + + if (!setExported) { + a.info.exported = a.intents.size() > 0; + } + + return a; + } + + private void setActivityResizeMode(ActivityInfo aInfo, TypedArray sa, Package owner) { + final boolean appExplicitDefault = (owner.applicationInfo.privateFlags + & (PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE + | PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE)) != 0; + + if (sa.hasValue(R.styleable.AndroidManifestActivity_resizeableActivity) + || appExplicitDefault) { + // Activity or app explicitly set if it is resizeable or not; + final boolean appResizeable = (owner.applicationInfo.privateFlags + & PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE) != 0; + if (sa.getBoolean(R.styleable.AndroidManifestActivity_resizeableActivity, + appResizeable)) { + aInfo.resizeMode = RESIZE_MODE_RESIZEABLE; + } else { + aInfo.resizeMode = RESIZE_MODE_UNRESIZEABLE; + } + return; + } + + if ((owner.applicationInfo.privateFlags + & PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION) != 0) { + // The activity or app didn't explicitly set the resizing option, however we want to + // make it resize due to the sdk version it is targeting. + aInfo.resizeMode = RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION; + return; + } + + // resize preference isn't set and target sdk version doesn't support resizing apps by + // default. For the app to be resizeable if it isn't fixed orientation or immersive. + if (aInfo.isFixedOrientationPortrait()) { + aInfo.resizeMode = RESIZE_MODE_FORCE_RESIZABLE_PORTRAIT_ONLY; + } else if (aInfo.isFixedOrientationLandscape()) { + aInfo.resizeMode = RESIZE_MODE_FORCE_RESIZABLE_LANDSCAPE_ONLY; + } else if (aInfo.isFixedOrientation()) { + aInfo.resizeMode = RESIZE_MODE_FORCE_RESIZABLE_PRESERVE_ORIENTATION; + } else { + aInfo.resizeMode = RESIZE_MODE_FORCE_RESIZEABLE; + } + } + + /** + * Sets every the max aspect ratio of every child activity that doesn't already have an aspect + * ratio set. + */ + private void setMaxAspectRatio(Package owner) { + // 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 = owner.applicationInfo.targetSdkVersion < O + ? DEFAULT_PRE_O_MAX_ASPECT_RATIO : 0; + + if (owner.applicationInfo.maxAspectRatio != 0) { + // Use the application max aspect ration as default if set. + maxAspectRatio = owner.applicationInfo.maxAspectRatio; + } else if (owner.mAppMetaData != null + && owner.mAppMetaData.containsKey(METADATA_MAX_ASPECT_RATIO)) { + maxAspectRatio = owner.mAppMetaData.getFloat(METADATA_MAX_ASPECT_RATIO, maxAspectRatio); + } + + for (Activity activity : owner.activities) { + // If the max aspect ratio for the activity has already been set, skip. + if (activity.hasMaxAspectRatio()) { + 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.metaData != null + ? activity.metaData.getFloat(METADATA_MAX_ASPECT_RATIO, maxAspectRatio) + : maxAspectRatio; + + activity.setMaxAspectRatio(activityAspectRatio); + } + } + + /** + * Sets every the min aspect ratio of every child activity that doesn't already have an aspect + * ratio set. + */ + private void setMinAspectRatio(Package owner) { + // Use the application max aspect ration as default if set. + final float minAspectRatio = owner.applicationInfo.minAspectRatio; + + for (Activity activity : owner.activities) { + if (activity.hasMinAspectRatio()) { + continue; + } + activity.setMinAspectRatio(minAspectRatio); + } + } + + private void setSupportsSizeChanges(Package owner) { + final boolean supportsSizeChanges = owner.mAppMetaData != null + && owner.mAppMetaData.getBoolean(METADATA_SUPPORTS_SIZE_CHANGES, false); + + for (Activity activity : owner.activities) { + if (supportsSizeChanges || (activity.metaData != null + && activity.metaData.getBoolean(METADATA_SUPPORTS_SIZE_CHANGES, false))) { + activity.info.supportsSizeChanges = true; + } + } + } + + /** + * @param configChanges The bit mask of configChanges fetched from AndroidManifest.xml. + * @param recreateOnConfigChanges The bit mask recreateOnConfigChanges fetched from + * AndroidManifest.xml. + * @hide Exposed for unit testing only. + */ + public static int getActivityConfigChanges(int configChanges, int recreateOnConfigChanges) { + return configChanges | ((~recreateOnConfigChanges) & RECREATE_ON_CONFIG_CHANGES_MASK); + } + + private void parseLayout(Resources res, AttributeSet attrs, Activity a) { + TypedArray sw = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestLayout); + int width = -1; + float widthFraction = -1f; + int height = -1; + float heightFraction = -1f; + final int widthType = sw.getType( + com.android.internal.R.styleable.AndroidManifestLayout_defaultWidth); + if (widthType == TypedValue.TYPE_FRACTION) { + widthFraction = sw.getFraction( + com.android.internal.R.styleable.AndroidManifestLayout_defaultWidth, + 1, 1, -1); + } else if (widthType == TypedValue.TYPE_DIMENSION) { + width = sw.getDimensionPixelSize( + com.android.internal.R.styleable.AndroidManifestLayout_defaultWidth, + -1); + } + final int heightType = sw.getType( + com.android.internal.R.styleable.AndroidManifestLayout_defaultHeight); + if (heightType == TypedValue.TYPE_FRACTION) { + heightFraction = sw.getFraction( + com.android.internal.R.styleable.AndroidManifestLayout_defaultHeight, + 1, 1, -1); + } else if (heightType == TypedValue.TYPE_DIMENSION) { + height = sw.getDimensionPixelSize( + com.android.internal.R.styleable.AndroidManifestLayout_defaultHeight, + -1); + } + int gravity = sw.getInt( + com.android.internal.R.styleable.AndroidManifestLayout_gravity, + Gravity.CENTER); + int minWidth = sw.getDimensionPixelSize( + com.android.internal.R.styleable.AndroidManifestLayout_minWidth, + -1); + int minHeight = sw.getDimensionPixelSize( + com.android.internal.R.styleable.AndroidManifestLayout_minHeight, + -1); + sw.recycle(); + a.info.windowLayout = new ActivityInfo.WindowLayout(width, widthFraction, + height, heightFraction, gravity, minWidth, minHeight); + } + + /** + * Resolves values in {@link ActivityInfo.WindowLayout}. + * + *

{@link ActivityInfo.WindowLayout#windowLayoutAffinity} has a fallback metadata used in + * Android R and some variants of pre-R. + */ + private void resolveWindowLayout(Activity activity) { + // There isn't a metadata for us to fall back. Whatever is in layout is correct. + if (activity.metaData == null + || !activity.metaData.containsKey(METADATA_ACTIVITY_WINDOW_LAYOUT_AFFINITY)) { + return; + } + + final ActivityInfo aInfo = activity.info; + // Layout already specifies a value. We should just use that one. + if (aInfo.windowLayout != null && aInfo.windowLayout.windowLayoutAffinity != null) { + return; + } + + String windowLayoutAffinity = activity.metaData.getString( + METADATA_ACTIVITY_WINDOW_LAYOUT_AFFINITY); + if (aInfo.windowLayout == null) { + aInfo.windowLayout = new ActivityInfo.WindowLayout(-1 /* width */, + -1 /* widthFraction */, -1 /* height */, -1 /* heightFraction */, + Gravity.NO_GRAVITY, -1 /* minWidth */, -1 /* minHeight */); + } + aInfo.windowLayout.windowLayoutAffinity = windowLayoutAffinity; + } + + private Activity parseActivityAlias(Package owner, Resources res, + XmlResourceParser parser, int flags, String[] outError, + CachedComponentArgs cachedArgs) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestActivityAlias); + + String targetActivity = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestActivityAlias_targetActivity, + Configuration.NATIVE_CONFIG_VERSION); + if (targetActivity == null) { + outError[0] = " does not specify android:targetActivity"; + sa.recycle(); + return null; + } + + targetActivity = buildClassName(owner.applicationInfo.packageName, + targetActivity, outError); + if (targetActivity == null) { + sa.recycle(); + return null; + } + + if (cachedArgs.mActivityAliasArgs == null) { + cachedArgs.mActivityAliasArgs = new ParseComponentArgs(owner, outError, + com.android.internal.R.styleable.AndroidManifestActivityAlias_name, + com.android.internal.R.styleable.AndroidManifestActivityAlias_label, + com.android.internal.R.styleable.AndroidManifestActivityAlias_icon, + com.android.internal.R.styleable.AndroidManifestActivityAlias_roundIcon, + com.android.internal.R.styleable.AndroidManifestActivityAlias_logo, + com.android.internal.R.styleable.AndroidManifestActivityAlias_banner, + mSeparateProcesses, + 0, + com.android.internal.R.styleable.AndroidManifestActivityAlias_description, + com.android.internal.R.styleable.AndroidManifestActivityAlias_enabled); + cachedArgs.mActivityAliasArgs.tag = ""; + } + + cachedArgs.mActivityAliasArgs.sa = sa; + cachedArgs.mActivityAliasArgs.flags = flags; + + Activity target = null; + + final int NA = owner.activities.size(); + for (int i=0; i 0 ? str.toString().intern() : null; + } + + String parentName = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestActivityAlias_parentActivityName, + Configuration.NATIVE_CONFIG_VERSION); + if (parentName != null) { + String parentClassName = buildClassName(a.info.packageName, parentName, outError); + if (outError[0] == null) { + a.info.parentActivityName = parentClassName; + } else { + Log.e(TAG, "Activity alias " + a.info.name + + " specified invalid parentActivityName " + parentName); + outError[0] = null; + } + } + + // TODO add visibleToInstantApps attribute to activity alias + final boolean visibleToEphemeral = + ((a.info.flags & ActivityInfo.FLAG_VISIBLE_TO_INSTANT_APP) != 0); + + sa.recycle(); + + if (outError[0] != null) { + return null; + } + + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getName().equals("intent-filter")) { + ActivityIntentInfo intent = new ActivityIntentInfo(a); + if (!parseIntent(res, parser, true /*allowGlobs*/, true /*allowAutoVerify*/, + intent, outError)) { + return null; + } + if (intent.countActions() == 0) { + Slog.w(TAG, "No actions in intent filter at " + + mArchiveSourcePath + " " + + parser.getPositionDescription()); + } else { + a.order = Math.max(intent.getOrder(), a.order); + a.intents.add(intent); + } + // adjust activity flags when we implicitly expose it via a browsable filter + final int visibility = visibleToEphemeral + ? IntentFilter.VISIBILITY_EXPLICIT + : isImplicitlyExposedIntent(intent) + ? IntentFilter.VISIBILITY_IMPLICIT + : IntentFilter.VISIBILITY_NONE; + intent.setVisibilityToInstantApp(visibility); + if (intent.isVisibleToInstantApp()) { + a.info.flags |= ActivityInfo.FLAG_VISIBLE_TO_INSTANT_APP; + } + if (intent.isImplicitlyVisibleToInstantApp()) { + a.info.flags |= ActivityInfo.FLAG_IMPLICITLY_VISIBLE_TO_INSTANT_APP; + } + } else if (parser.getName().equals("meta-data")) { + if ((a.metaData=parseMetaData(res, parser, a.metaData, + outError)) == null) { + return null; + } + } else { + if (!RIGID_PARSER) { + Slog.w(TAG, "Unknown element under : " + parser.getName() + + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } else { + outError[0] = "Bad element under : " + parser.getName(); + return null; + } + } + } + + if (!setExported) { + a.info.exported = a.intents.size() > 0; + } + + return a; + } + + private Provider parseProvider(Package owner, Resources res, + XmlResourceParser parser, int flags, String[] outError, + CachedComponentArgs cachedArgs) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestProvider); + + if (cachedArgs.mProviderArgs == null) { + cachedArgs.mProviderArgs = new ParseComponentArgs(owner, outError, + com.android.internal.R.styleable.AndroidManifestProvider_name, + com.android.internal.R.styleable.AndroidManifestProvider_label, + com.android.internal.R.styleable.AndroidManifestProvider_icon, + com.android.internal.R.styleable.AndroidManifestProvider_roundIcon, + com.android.internal.R.styleable.AndroidManifestProvider_logo, + com.android.internal.R.styleable.AndroidManifestProvider_banner, + mSeparateProcesses, + com.android.internal.R.styleable.AndroidManifestProvider_process, + com.android.internal.R.styleable.AndroidManifestProvider_description, + com.android.internal.R.styleable.AndroidManifestProvider_enabled); + cachedArgs.mProviderArgs.tag = ""; + } + + cachedArgs.mProviderArgs.sa = sa; + cachedArgs.mProviderArgs.flags = flags; + + Provider p = new Provider(cachedArgs.mProviderArgs, new ProviderInfo()); + if (outError[0] != null) { + sa.recycle(); + return null; + } + + boolean providerExportedDefault = false; + + if (owner.applicationInfo.targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR1) { + // For compatibility, applications targeting API level 16 or lower + // should have their content providers exported by default, unless they + // specify otherwise. + providerExportedDefault = true; + } + + p.info.exported = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_exported, + providerExportedDefault); + + String cpname = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestProvider_authorities, 0); + + p.info.isSyncable = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_syncable, + false); + + String permission = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestProvider_permission, 0); + String str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestProvider_readPermission, 0); + if (str == null) { + str = permission; + } + if (str == null) { + p.info.readPermission = owner.applicationInfo.permission; + } else { + p.info.readPermission = + str.length() > 0 ? str.toString().intern() : null; + } + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestProvider_writePermission, 0); + if (str == null) { + str = permission; + } + if (str == null) { + p.info.writePermission = owner.applicationInfo.permission; + } else { + p.info.writePermission = + str.length() > 0 ? str.toString().intern() : null; + } + + p.info.grantUriPermissions = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_grantUriPermissions, + false); + + p.info.forceUriPermissions = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_forceUriPermissions, + false); + + p.info.multiprocess = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_multiprocess, + false); + + p.info.initOrder = sa.getInt( + com.android.internal.R.styleable.AndroidManifestProvider_initOrder, + 0); + + p.info.splitName = + sa.getNonConfigurationString(R.styleable.AndroidManifestProvider_splitName, 0); + + p.info.flags = 0; + + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestProvider_singleUser, + false)) { + p.info.flags |= ProviderInfo.FLAG_SINGLE_USER; + } + + p.info.directBootAware = sa.getBoolean( + R.styleable.AndroidManifestProvider_directBootAware, + false); + if (p.info.directBootAware) { + owner.applicationInfo.privateFlags |= + ApplicationInfo.PRIVATE_FLAG_PARTIALLY_DIRECT_BOOT_AWARE; + } + + final boolean visibleToEphemeral = + sa.getBoolean(R.styleable.AndroidManifestProvider_visibleToInstantApps, false); + if (visibleToEphemeral) { + p.info.flags |= ProviderInfo.FLAG_VISIBLE_TO_INSTANT_APP; + owner.visibleToInstantApps = true; + } + + sa.recycle(); + + if ((owner.applicationInfo.privateFlags&ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) + != 0) { + // A heavy-weight application can not have providers in its main process + // We can do direct compare because we intern all strings. + if (p.info.processName == owner.packageName) { + outError[0] = "Heavy-weight applications can not have providers in main process"; + return null; + } + } + + if (cpname == null) { + outError[0] = " does not include authorities attribute"; + return null; + } + if (cpname.length() <= 0) { + outError[0] = " has empty authorities attribute"; + return null; + } + p.info.authority = cpname.intern(); + + if (!parseProviderTags( + res, parser, visibleToEphemeral, p, outError)) { + return null; + } + + return p; + } + + private boolean parseProviderTags(Resources res, XmlResourceParser parser, + boolean visibleToEphemeral, Provider outInfo, String[] outError) + throws XmlPullParserException, IOException { + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getName().equals("intent-filter")) { + ProviderIntentInfo intent = new ProviderIntentInfo(outInfo); + if (!parseIntent(res, parser, true /*allowGlobs*/, false /*allowAutoVerify*/, + intent, outError)) { + return false; + } + if (visibleToEphemeral) { + intent.setVisibilityToInstantApp(IntentFilter.VISIBILITY_EXPLICIT); + outInfo.info.flags |= ProviderInfo.FLAG_VISIBLE_TO_INSTANT_APP; + } + outInfo.order = Math.max(intent.getOrder(), outInfo.order); + outInfo.intents.add(intent); + + } else if (parser.getName().equals("meta-data")) { + if ((outInfo.metaData=parseMetaData(res, parser, + outInfo.metaData, outError)) == null) { + return false; + } + + } else if (parser.getName().equals("grant-uri-permission")) { + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestGrantUriPermission); + + PatternMatcher pa = null; + + String str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestGrantUriPermission_path, 0); + if (str != null) { + pa = new PatternMatcher(str, PatternMatcher.PATTERN_LITERAL); + } + + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestGrantUriPermission_pathPrefix, 0); + if (str != null) { + pa = new PatternMatcher(str, PatternMatcher.PATTERN_PREFIX); + } + + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestGrantUriPermission_pathPattern, 0); + if (str != null) { + pa = new PatternMatcher(str, PatternMatcher.PATTERN_SIMPLE_GLOB); + } + + sa.recycle(); + + if (pa != null) { + if (outInfo.info.uriPermissionPatterns == null) { + outInfo.info.uriPermissionPatterns = new PatternMatcher[1]; + outInfo.info.uriPermissionPatterns[0] = pa; + } else { + final int N = outInfo.info.uriPermissionPatterns.length; + PatternMatcher[] newp = new PatternMatcher[N+1]; + System.arraycopy(outInfo.info.uriPermissionPatterns, 0, newp, 0, N); + newp[N] = pa; + outInfo.info.uriPermissionPatterns = newp; + } + outInfo.info.grantUriPermissions = true; + } else { + if (!RIGID_PARSER) { + Slog.w(TAG, "Unknown element under : " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } else { + outError[0] = "No path, pathPrefix, or pathPattern for "; + return false; + } + } + XmlUtils.skipCurrentTag(parser); + + } else if (parser.getName().equals("path-permission")) { + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestPathPermission); + + PathPermission pa = null; + + String permission = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestPathPermission_permission, 0); + String readPermission = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestPathPermission_readPermission, 0); + if (readPermission == null) { + readPermission = permission; + } + String writePermission = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestPathPermission_writePermission, 0); + if (writePermission == null) { + writePermission = permission; + } + + boolean havePerm = false; + if (readPermission != null) { + readPermission = readPermission.intern(); + havePerm = true; + } + if (writePermission != null) { + writePermission = writePermission.intern(); + havePerm = true; + } + + if (!havePerm) { + if (!RIGID_PARSER) { + Slog.w(TAG, "No readPermission or writePermssion for : " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } else { + outError[0] = "No readPermission or writePermssion for "; + return false; + } + } + + String path = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestPathPermission_path, 0); + if (path != null) { + pa = new PathPermission(path, + PatternMatcher.PATTERN_LITERAL, readPermission, writePermission); + } + + path = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestPathPermission_pathPrefix, 0); + if (path != null) { + pa = new PathPermission(path, + PatternMatcher.PATTERN_PREFIX, readPermission, writePermission); + } + + path = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestPathPermission_pathPattern, 0); + if (path != null) { + pa = new PathPermission(path, + PatternMatcher.PATTERN_SIMPLE_GLOB, readPermission, writePermission); + } + + path = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestPathPermission_pathAdvancedPattern, 0); + if (path != null) { + pa = new PathPermission(path, + PatternMatcher.PATTERN_ADVANCED_GLOB, readPermission, writePermission); + } + + sa.recycle(); + + if (pa != null) { + if (outInfo.info.pathPermissions == null) { + outInfo.info.pathPermissions = new PathPermission[1]; + outInfo.info.pathPermissions[0] = pa; + } else { + final int N = outInfo.info.pathPermissions.length; + PathPermission[] newp = new PathPermission[N+1]; + System.arraycopy(outInfo.info.pathPermissions, 0, newp, 0, N); + newp[N] = pa; + outInfo.info.pathPermissions = newp; + } + } else { + if (!RIGID_PARSER) { + Slog.w(TAG, "No path, pathPrefix, or pathPattern for : " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } + outError[0] = "No path, pathPrefix, or pathPattern for "; + return false; + } + XmlUtils.skipCurrentTag(parser); + + } else { + if (!RIGID_PARSER) { + Slog.w(TAG, "Unknown element under : " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } else { + outError[0] = "Bad element under : " + parser.getName(); + return false; + } + } + } + return true; + } + + private Service parseService(Package owner, Resources res, + XmlResourceParser parser, int flags, String[] outError, + CachedComponentArgs cachedArgs) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestService); + + if (cachedArgs.mServiceArgs == null) { + cachedArgs.mServiceArgs = new ParseComponentArgs(owner, outError, + com.android.internal.R.styleable.AndroidManifestService_name, + com.android.internal.R.styleable.AndroidManifestService_label, + com.android.internal.R.styleable.AndroidManifestService_icon, + com.android.internal.R.styleable.AndroidManifestService_roundIcon, + com.android.internal.R.styleable.AndroidManifestService_logo, + com.android.internal.R.styleable.AndroidManifestService_banner, + mSeparateProcesses, + com.android.internal.R.styleable.AndroidManifestService_process, + com.android.internal.R.styleable.AndroidManifestService_description, + com.android.internal.R.styleable.AndroidManifestService_enabled); + cachedArgs.mServiceArgs.tag = ""; + } + + cachedArgs.mServiceArgs.sa = sa; + cachedArgs.mServiceArgs.flags = flags; + + Service s = new Service(cachedArgs.mServiceArgs, new ServiceInfo()); + if (outError[0] != null) { + sa.recycle(); + return null; + } + + boolean setExported = sa.hasValue( + com.android.internal.R.styleable.AndroidManifestService_exported); + if (setExported) { + s.info.exported = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestService_exported, false); + } + + String str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestService_permission, 0); + if (str == null) { + s.info.permission = owner.applicationInfo.permission; + } else { + s.info.permission = str.length() > 0 ? str.toString().intern() : null; + } + + s.info.splitName = + sa.getNonConfigurationString(R.styleable.AndroidManifestService_splitName, 0); + + s.info.mForegroundServiceType = sa.getInt( + com.android.internal.R.styleable.AndroidManifestService_foregroundServiceType, + ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE); + + s.info.flags = 0; + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestService_stopWithTask, + false)) { + s.info.flags |= ServiceInfo.FLAG_STOP_WITH_TASK; + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestService_isolatedProcess, + false)) { + s.info.flags |= ServiceInfo.FLAG_ISOLATED_PROCESS; + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestService_externalService, + false)) { + s.info.flags |= ServiceInfo.FLAG_EXTERNAL_SERVICE; + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestService_useAppZygote, + false)) { + s.info.flags |= ServiceInfo.FLAG_USE_APP_ZYGOTE; + } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestService_singleUser, + false)) { + s.info.flags |= ServiceInfo.FLAG_SINGLE_USER; + } + + s.info.directBootAware = sa.getBoolean( + R.styleable.AndroidManifestService_directBootAware, + false); + if (s.info.directBootAware) { + owner.applicationInfo.privateFlags |= + ApplicationInfo.PRIVATE_FLAG_PARTIALLY_DIRECT_BOOT_AWARE; + } + + boolean visibleToEphemeral = + sa.getBoolean(R.styleable.AndroidManifestService_visibleToInstantApps, false); + if (visibleToEphemeral) { + s.info.flags |= ServiceInfo.FLAG_VISIBLE_TO_INSTANT_APP; + owner.visibleToInstantApps = true; + } + + sa.recycle(); + + if ((owner.applicationInfo.privateFlags&ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) + != 0) { + // A heavy-weight application can not have services in its main process + // We can do direct compare because we intern all strings. + if (s.info.processName == owner.packageName) { + outError[0] = "Heavy-weight applications can not have services in main process"; + return null; + } + } + + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getName().equals("intent-filter")) { + ServiceIntentInfo intent = new ServiceIntentInfo(s); + if (!parseIntent(res, parser, true /*allowGlobs*/, false /*allowAutoVerify*/, + intent, outError)) { + return null; + } + if (visibleToEphemeral) { + intent.setVisibilityToInstantApp(IntentFilter.VISIBILITY_EXPLICIT); + s.info.flags |= ServiceInfo.FLAG_VISIBLE_TO_INSTANT_APP; + } + s.order = Math.max(intent.getOrder(), s.order); + s.intents.add(intent); + } else if (parser.getName().equals("meta-data")) { + if ((s.metaData=parseMetaData(res, parser, s.metaData, + outError)) == null) { + return null; + } + } else { + if (!RIGID_PARSER) { + Slog.w(TAG, "Unknown element under : " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } else { + outError[0] = "Bad element under : " + parser.getName(); + return null; + } + } + } + + if (!setExported) { + s.info.exported = s.intents.size() > 0; + } + + return s; + } + + private boolean isImplicitlyExposedIntent(IntentInfo intent) { + return intent.hasCategory(Intent.CATEGORY_BROWSABLE) + || intent.hasAction(Intent.ACTION_SEND) + || intent.hasAction(Intent.ACTION_SENDTO) + || intent.hasAction(Intent.ACTION_SEND_MULTIPLE); + } + + private boolean parseAllMetaData(Resources res, XmlResourceParser parser, String tag, + Component outInfo, String[] outError) throws XmlPullParserException, IOException { + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + if (parser.getName().equals("meta-data")) { + if ((outInfo.metaData=parseMetaData(res, parser, + outInfo.metaData, outError)) == null) { + return false; + } + } else { + if (!RIGID_PARSER) { + Slog.w(TAG, "Unknown element under " + tag + ": " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + continue; + } else { + outError[0] = "Bad element under " + tag + ": " + parser.getName(); + return false; + } + } + } + return true; + } + + private Bundle parseMetaData(Resources res, + XmlResourceParser parser, Bundle data, String[] outError) + throws XmlPullParserException, IOException { + + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestMetaData); + + if (data == null) { + data = new Bundle(); + } + + String name = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestMetaData_name, 0); + if (name == null) { + outError[0] = " requires an android:name attribute"; + sa.recycle(); + return null; + } + + name = name.intern(); + + TypedValue v = sa.peekValue( + com.android.internal.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( + com.android.internal.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 (!RIGID_PARSER) { + Slog.w(TAG, " only supports string, integer, float, color, boolean, and resource reference types: " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + } else { + outError[0] = " only supports string, integer, float, color, boolean, and resource reference types"; + data = null; + } + } + } else { + outError[0] = " requires an android:value or android:resource attribute"; + data = null; + } + } + + sa.recycle(); + + XmlUtils.skipCurrentTag(parser); + + return data; + } + + private static VerifierInfo parseVerifier(AttributeSet attrs) { + String packageName = null; + String encodedPublicKey = null; + + final int attrCount = attrs.getAttributeCount(); + for (int i = 0; i < attrCount; i++) { + final int attrResId = attrs.getAttributeNameResource(i); + switch (attrResId) { + case com.android.internal.R.attr.name: + packageName = attrs.getAttributeValue(i); + break; + + case com.android.internal.R.attr.publicKey: + encodedPublicKey = attrs.getAttributeValue(i); + break; + } + } + + if (packageName == null || packageName.length() == 0) { + Slog.i(TAG, "verifier package name was null; skipping"); + return null; + } + + final PublicKey publicKey = parsePublicKey(encodedPublicKey); + if (publicKey == null) { + Slog.i(TAG, "Unable to parse verifier public key for " + packageName); + return null; + } + + return new VerifierInfo(packageName, publicKey); + } + + public static final PublicKey parsePublicKey(final String encodedPublicKey) { + if (encodedPublicKey == null) { + Slog.w(TAG, "Could not parse null public key"); + return null; + } + + try { + return parsePublicKey(Base64.decode(encodedPublicKey, Base64.DEFAULT)); + } catch (IllegalArgumentException e) { + Slog.w(TAG, "Could not parse verifier public key; invalid Base64"); + return null; + } + } + + public static final PublicKey parsePublicKey(final byte[] publicKey) { + if (publicKey == null) { + Slog.w(TAG, "Could not parse null public key"); + return null; + } + + EncodedKeySpec keySpec; + try { + keySpec = new X509EncodedKeySpec(publicKey); + } catch (IllegalArgumentException e) { + Slog.w(TAG, "Could not parse verifier public key; invalid Base64"); + return null; + } + + /* First try the key as an RSA key. */ + try { + final KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + Slog.wtf(TAG, "Could not parse public key: RSA KeyFactory not included in build"); + } catch (InvalidKeySpecException e) { + // Not a RSA public key. + } + + /* Now try it as a ECDSA key. */ + try { + final KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + Slog.wtf(TAG, "Could not parse public key: EC KeyFactory not included in build"); + } catch (InvalidKeySpecException e) { + // Not a ECDSA public key. + } + + /* Now try it as a DSA key. */ + try { + final KeyFactory keyFactory = KeyFactory.getInstance("DSA"); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + Slog.wtf(TAG, "Could not parse public key: DSA KeyFactory not included in build"); + } catch (InvalidKeySpecException e) { + // Not a DSA public key. + } + + /* Not a supported key type */ + return null; + } + + public static final String ANDROID_RESOURCES + = "http://schemas.android.com/apk/res/android"; + + private boolean parseIntent(Resources res, XmlResourceParser parser, boolean allowGlobs, + boolean allowAutoVerify, IntentInfo outInfo, String[] outError) + throws XmlPullParserException, IOException { + + TypedArray sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestIntentFilter); + + int priority = sa.getInt( + com.android.internal.R.styleable.AndroidManifestIntentFilter_priority, 0); + outInfo.setPriority(priority); + + int order = sa.getInt( + com.android.internal.R.styleable.AndroidManifestIntentFilter_order, 0); + outInfo.setOrder(order); + + TypedValue v = sa.peekValue( + com.android.internal.R.styleable.AndroidManifestIntentFilter_label); + if (v != null && (outInfo.labelRes=v.resourceId) == 0) { + outInfo.nonLocalizedLabel = v.coerceToString(); + } + + int roundIconVal = sUseRoundIcon ? sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestIntentFilter_roundIcon, 0) : 0; + if (roundIconVal != 0) { + outInfo.icon = roundIconVal; + } else { + outInfo.icon = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestIntentFilter_icon, 0); + } + + outInfo.logo = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestIntentFilter_logo, 0); + + outInfo.banner = sa.getResourceId( + com.android.internal.R.styleable.AndroidManifestIntentFilter_banner, 0); + + if (allowAutoVerify) { + outInfo.setAutoVerify(sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestIntentFilter_autoVerify, + false)); + } + + sa.recycle(); + + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String nodeName = parser.getName(); + if (nodeName.equals("action")) { + String value = parser.getAttributeValue( + ANDROID_RESOURCES, "name"); + if (value == null || value == "") { + outError[0] = "No value supplied for "; + return false; + } + XmlUtils.skipCurrentTag(parser); + + outInfo.addAction(value); + } else if (nodeName.equals("category")) { + String value = parser.getAttributeValue( + ANDROID_RESOURCES, "name"); + if (value == null || value == "") { + outError[0] = "No value supplied for "; + return false; + } + XmlUtils.skipCurrentTag(parser); + + outInfo.addCategory(value); + + } else if (nodeName.equals("data")) { + sa = res.obtainAttributes(parser, + com.android.internal.R.styleable.AndroidManifestData); + + String str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_mimeType, 0); + if (str != null) { + try { + outInfo.addDataType(str); + } catch (IntentFilter.MalformedMimeTypeException e) { + outError[0] = e.toString(); + sa.recycle(); + return false; + } + } + + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_scheme, 0); + if (str != null) { + outInfo.addDataScheme(str); + } + + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_ssp, 0); + if (str != null) { + outInfo.addDataSchemeSpecificPart(str, PatternMatcher.PATTERN_LITERAL); + } + + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_sspPrefix, 0); + if (str != null) { + outInfo.addDataSchemeSpecificPart(str, PatternMatcher.PATTERN_PREFIX); + } + + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_sspPattern, 0); + if (str != null) { + if (!allowGlobs) { + outError[0] = "sspPattern not allowed here; ssp must be literal"; + return false; + } + outInfo.addDataSchemeSpecificPart(str, PatternMatcher.PATTERN_SIMPLE_GLOB); + } + + String host = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_host, 0); + String port = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_port, 0); + if (host != null) { + outInfo.addDataAuthority(host, port); + } + + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_path, 0); + if (str != null) { + outInfo.addDataPath(str, PatternMatcher.PATTERN_LITERAL); + } + + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_pathPrefix, 0); + if (str != null) { + outInfo.addDataPath(str, PatternMatcher.PATTERN_PREFIX); + } + + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_pathPattern, 0); + if (str != null) { + if (!allowGlobs) { + outError[0] = "pathPattern not allowed here; path must be literal"; + return false; + } + outInfo.addDataPath(str, PatternMatcher.PATTERN_SIMPLE_GLOB); + } + + str = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestData_pathAdvancedPattern, 0); + if (str != null) { + if (!allowGlobs) { + outError[0] = "pathAdvancedPattern not allowed here; path must be literal"; + return false; + } + outInfo.addDataPath(str, PatternMatcher.PATTERN_ADVANCED_GLOB); + } + + sa.recycle(); + XmlUtils.skipCurrentTag(parser); + } else if (!RIGID_PARSER) { + Slog.w(TAG, "Unknown element under : " + + parser.getName() + " at " + mArchiveSourcePath + " " + + parser.getPositionDescription()); + XmlUtils.skipCurrentTag(parser); + } else { + outError[0] = "Bad element under : " + parser.getName(); + return false; + } + } + + outInfo.hasDefault = outInfo.hasCategory(Intent.CATEGORY_DEFAULT); + + if (DEBUG_PARSER) { + final StringBuilder cats = new StringBuilder("Intent d="); + cats.append(outInfo.hasDefault); + cats.append(", cat="); + + final Iterator it = outInfo.categoriesIterator(); + if (it != null) { + while (it.hasNext()) { + cats.append(' '); + cats.append(it.next()); + } + } + Slog.d(TAG, cats.toString()); + } + + return true; + } + + /** + * A container for signing-related data of an application package. + * @hide + */ + public static final class SigningDetails implements Parcelable { + + @IntDef({SigningDetails.SignatureSchemeVersion.UNKNOWN, + SigningDetails.SignatureSchemeVersion.JAR, + SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2, + SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3, + SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V4}) + public @interface SignatureSchemeVersion { + int UNKNOWN = 0; + int JAR = 1; + int SIGNING_BLOCK_V2 = 2; + int SIGNING_BLOCK_V3 = 3; + int SIGNING_BLOCK_V4 = 4; + } + + @Nullable + @UnsupportedAppUsage + public final Signature[] signatures; + @SignatureSchemeVersion + public final int signatureSchemeVersion; + @Nullable + public final ArraySet publicKeys; + + /** + * APK Signature Scheme v3 includes support for adding a proof-of-rotation record that + * contains two pieces of information: + * 1) the past signing certificates + * 2) the flags that APK wants to assign to each of the past signing certificates. + * + * This collection of {@code Signature} objects, each of which is formed from a former + * signing certificate of this APK before it was changed by signing certificate rotation, + * represents the first piece of information. It is the APK saying to the rest of the + * world: "hey if you trust the old cert, you can trust me!" This is useful, if for + * instance, the platform would like to determine whether or not to allow this APK to do + * something it would've allowed it to do under the old cert (like upgrade). + */ + @Nullable + public final Signature[] pastSigningCertificates; + + /** special value used to see if cert is in package - not exposed to callers */ + private static final int PAST_CERT_EXISTS = 0; + + @IntDef( + flag = true, + value = {CertCapabilities.INSTALLED_DATA, + CertCapabilities.SHARED_USER_ID, + CertCapabilities.PERMISSION, + CertCapabilities.ROLLBACK}) + public @interface CertCapabilities { + + /** accept data from already installed pkg with this cert */ + int INSTALLED_DATA = 1; + + /** accept sharedUserId with pkg with this cert */ + int SHARED_USER_ID = 2; + + /** grant SIGNATURE permissions to pkgs with this cert */ + int PERMISSION = 4; + + /** allow pkg to update to one signed by this certificate */ + int ROLLBACK = 8; + + /** allow pkg to continue to have auth access gated by this cert */ + int AUTH = 16; + } + + /** A representation of unknown signing details. Use instead of null. */ + public static final SigningDetails UNKNOWN = + new SigningDetails(null, SignatureSchemeVersion.UNKNOWN, null, null); + + @VisibleForTesting + public SigningDetails(Signature[] signatures, + @SignatureSchemeVersion int signatureSchemeVersion, + ArraySet keys, Signature[] pastSigningCertificates) { + this.signatures = signatures; + this.signatureSchemeVersion = signatureSchemeVersion; + this.publicKeys = keys; + this.pastSigningCertificates = pastSigningCertificates; + } + + public SigningDetails(Signature[] signatures, + @SignatureSchemeVersion int signatureSchemeVersion, + Signature[] pastSigningCertificates) + throws CertificateException { + this(signatures, signatureSchemeVersion, toSigningKeys(signatures), + pastSigningCertificates); + } + + public SigningDetails(Signature[] signatures, + @SignatureSchemeVersion int signatureSchemeVersion) + throws CertificateException { + this(signatures, signatureSchemeVersion, null); + } + + public SigningDetails(SigningDetails orig) { + if (orig != null) { + if (orig.signatures != null) { + this.signatures = orig.signatures.clone(); + } else { + this.signatures = null; + } + this.signatureSchemeVersion = orig.signatureSchemeVersion; + this.publicKeys = new ArraySet<>(orig.publicKeys); + if (orig.pastSigningCertificates != null) { + this.pastSigningCertificates = orig.pastSigningCertificates.clone(); + } else { + this.pastSigningCertificates = null; + } + } else { + this.signatures = null; + this.signatureSchemeVersion = SignatureSchemeVersion.UNKNOWN; + this.publicKeys = null; + this.pastSigningCertificates = null; + } + } + + /** + * Merges the signing lineage of this instance with the lineage in the provided {@code + * otherSigningDetails} when one has the same or an ancestor signer of the other. + * + *

Merging two signing lineages will result in a new {@code SigningDetails} instance + * containing the longest common lineage with the most restrictive capabilities. If the two + * lineages contain the same signers with the same capabilities then the instance on which + * this was invoked is returned without any changes. Similarly if neither instance has a + * lineage, or if neither has the same or an ancestor signer then this instance is returned. + * + * Following are some example results of this method for lineages with signers A, B, C, D: + * - lineage B merged with lineage A -> B returns lineage A -> B. + * - lineage A -> B merged with lineage B -> C returns lineage A -> B -> C + * - lineage A -> B with the {@code PERMISSION} capability revoked for A merged with + * lineage A -> B with the {@code SHARED_USER_ID} capability revoked for A returns + * lineage A -> B with both capabilities revoked for A. + * - lineage A -> B -> C merged with lineage A -> B -> D would return the original lineage + * A -> B -> C since the current signer of both instances is not the same or in the + * lineage of the other. + */ + public SigningDetails mergeLineageWith(SigningDetails otherSigningDetails) { + if (!hasPastSigningCertificates()) { + return otherSigningDetails.hasPastSigningCertificates() + && otherSigningDetails.hasAncestorOrSelf(this) ? otherSigningDetails : this; + } + if (!otherSigningDetails.hasPastSigningCertificates()) { + return this; + } + // Use the utility method to determine which SigningDetails instance is the descendant + // and to confirm that the signing lineage does not diverge. + SigningDetails descendantSigningDetails = getDescendantOrSelf(otherSigningDetails); + if (descendantSigningDetails == null) { + return this; + } + return descendantSigningDetails == this ? mergeLineageWithAncestorOrSelf( + otherSigningDetails) : otherSigningDetails.mergeLineageWithAncestorOrSelf(this); + } + + /** + * Merges the signing lineage of this instance with the lineage of the ancestor (or same) + * signer in the provided {@code otherSigningDetails}. + */ + private SigningDetails mergeLineageWithAncestorOrSelf(SigningDetails otherSigningDetails) { + // This method should only be called with instances that contain lineages. + int index = pastSigningCertificates.length - 1; + int otherIndex = otherSigningDetails.pastSigningCertificates.length - 1; + if (index < 0 || otherIndex < 0) { + return this; + } + + List mergedSignatures = new ArrayList<>(); + boolean capabilitiesModified = false; + // If this is a descendant lineage then add all of the descendant signer(s) to the + // merged lineage until the ancestor signer is reached. + while (index >= 0 && !pastSigningCertificates[index].equals( + otherSigningDetails.pastSigningCertificates[otherIndex])) { + mergedSignatures.add(new Signature(pastSigningCertificates[index--])); + } + // If the signing lineage was exhausted then the provided ancestor is not actually an + // ancestor of this lineage. + if (index < 0) { + return this; + } + + do { + // Add the common signer to the merged lineage with the most restrictive + // capabilities of the two lineages. + Signature signature = pastSigningCertificates[index--]; + Signature ancestorSignature = + otherSigningDetails.pastSigningCertificates[otherIndex--]; + Signature mergedSignature = new Signature(signature); + int mergedCapabilities = signature.getFlags() & ancestorSignature.getFlags(); + if (signature.getFlags() != mergedCapabilities) { + capabilitiesModified = true; + mergedSignature.setFlags(mergedCapabilities); + } + mergedSignatures.add(mergedSignature); + } while (index >= 0 && otherIndex >= 0 && pastSigningCertificates[index].equals( + otherSigningDetails.pastSigningCertificates[otherIndex])); + + // If both lineages still have elements then their lineages have diverged; since this is + // not supported return the invoking instance. + if (index >= 0 && otherIndex >= 0) { + return this; + } + + // Add any remaining elements from either lineage that is not yet exhausted to the + // the merged lineage. + while (otherIndex >= 0) { + mergedSignatures.add(new Signature( + otherSigningDetails.pastSigningCertificates[otherIndex--])); + } + while (index >= 0) { + mergedSignatures.add(new Signature(pastSigningCertificates[index--])); + } + + // if this lineage already contains all the elements in the ancestor and none of the + // capabilities were changed then just return this instance. + if (mergedSignatures.size() == pastSigningCertificates.length + && !capabilitiesModified) { + return this; + } + // Since the signatures were added to the merged lineage from newest to oldest reverse + // the list to ensure the oldest signer is at index 0. + Collections.reverse(mergedSignatures); + try { + return new SigningDetails(new Signature[]{new Signature(signatures[0])}, + signatureSchemeVersion, mergedSignatures.toArray(new Signature[0])); + } catch (CertificateException e) { + Slog.e(TAG, "Caught an exception creating the merged lineage: ", e); + return this; + } + } + + /** + * Returns whether this and the provided {@code otherSigningDetails} share a common + * ancestor. + * + *

The two SigningDetails have a common ancestor if any of the following conditions are + * met: + * - If neither has a lineage and their current signer(s) are equal. + * - If only one has a lineage and the signer of the other is the same or in the lineage. + * - If both have a lineage and their current signers are the same or one is in the lineage + * of the other, and their lineages do not diverge to different signers. + */ + public boolean hasCommonAncestor(SigningDetails otherSigningDetails) { + if (!hasPastSigningCertificates()) { + // If this instance does not have a lineage then it must either be in the ancestry + // of or the same signer of the otherSigningDetails. + return otherSigningDetails.hasAncestorOrSelf(this); + } + if (!otherSigningDetails.hasPastSigningCertificates()) { + return hasAncestorOrSelf(otherSigningDetails); + } + // If both have a lineage then use getDescendantOrSelf to obtain the descendant signing + // details; a null return from that method indicates there is no common lineage between + // the two or that they diverge at a point in the lineage. + return getDescendantOrSelf(otherSigningDetails) != null; + } + + /** + * Returns whether this instance is currently signed, or has ever been signed, with a + * signing certificate from the provided {@link Set} of {@code certDigests}. + * + *

The provided {@code certDigests} should contain the SHA-256 digest of the DER encoding + * of each trusted certificate with the digest characters in upper case. If this instance + * has multiple signers then all signers must be in the provided {@code Set}. If this + * instance has a signing lineage then this method will return true if any of the previous + * signers in the lineage match one of the entries in the {@code Set}. + */ + public boolean hasAncestorOrSelfWithDigest(Set certDigests) { + if (this == UNKNOWN || certDigests == null || certDigests.size() == 0) { + return false; + } + // If an app is signed by multiple signers then all of the signers must be in the Set. + if (signatures.length > 1) { + // If the Set has less elements than the number of signatures then immediately + // return false as there's no way to satisfy the requirement of all signatures being + // in the Set. + if (certDigests.size() < signatures.length) { + return false; + } + for (Signature signature : signatures) { + String signatureDigest = PackageUtils.computeSha256Digest( + signature.toByteArray()); + if (!certDigests.contains(signatureDigest)) { + return false; + } + } + return true; + } + + String signatureDigest = PackageUtils.computeSha256Digest(signatures[0].toByteArray()); + if (certDigests.contains(signatureDigest)) { + return true; + } + if (hasPastSigningCertificates()) { + // The last element in the pastSigningCertificates array is the current signer; + // since that was verified above just check all the signers in the lineage. + for (int i = 0; i < pastSigningCertificates.length - 1; i++) { + signatureDigest = PackageUtils.computeSha256Digest( + pastSigningCertificates[i].toByteArray()); + if (certDigests.contains(signatureDigest)) { + return true; + } + } + } + return false; + } + + /** + * Returns the SigningDetails with a descendant (or same) signer after verifying the + * descendant has the same, a superset, or a subset of the lineage of the ancestor. + * + *

If this instance and the provided {@code otherSigningDetails} do not share an + * ancestry, or if their lineages diverge then null is returned to indicate there is no + * valid descendant SigningDetails. + */ + private SigningDetails getDescendantOrSelf(SigningDetails otherSigningDetails) { + SigningDetails descendantSigningDetails; + SigningDetails ancestorSigningDetails; + if (hasAncestorOrSelf(otherSigningDetails)) { + // If the otherSigningDetails has the same signer or a signer in the lineage of this + // instance then treat this instance as the descendant. + descendantSigningDetails = this; + ancestorSigningDetails = otherSigningDetails; + } else if (otherSigningDetails.hasAncestor(this)) { + // The above check confirmed that the two instances do not have the same signer and + // the signer of otherSigningDetails is not in this instance's lineage; if this + // signer is in the otherSigningDetails lineage then treat this as the ancestor. + descendantSigningDetails = otherSigningDetails; + ancestorSigningDetails = this; + } else { + // The signers are not the same and neither has the current signer of the other in + // its lineage; return null to indicate there is no descendant signer. + return null; + } + // Once the descent (or same) signer is identified iterate through the ancestry until + // the current signer of the ancestor is found. + int descendantIndex = descendantSigningDetails.pastSigningCertificates.length - 1; + int ancestorIndex = ancestorSigningDetails.pastSigningCertificates.length - 1; + while (descendantIndex >= 0 + && !descendantSigningDetails.pastSigningCertificates[descendantIndex].equals( + ancestorSigningDetails.pastSigningCertificates[ancestorIndex])) { + descendantIndex--; + } + // Since the ancestry was verified above the descendant lineage should never be + // exhausted, but if for some reason the ancestor signer is not found then return null. + if (descendantIndex < 0) { + return null; + } + // Once the common ancestor (or same) signer is found iterate over the lineage of both + // to ensure that they are either the same or one is a subset of the other. + do { + descendantIndex--; + ancestorIndex--; + } while (descendantIndex >= 0 && ancestorIndex >= 0 + && descendantSigningDetails.pastSigningCertificates[descendantIndex].equals( + ancestorSigningDetails.pastSigningCertificates[ancestorIndex])); + + // If both lineages still have elements then they diverge and cannot be considered a + // valid common lineage. + if (descendantIndex >= 0 && ancestorIndex >= 0) { + return null; + } + // Since one or both of the lineages was exhausted they are either the same or one is a + // subset of the other; return the valid descendant. + return descendantSigningDetails; + } + + /** Returns true if the signing details have one or more signatures. */ + public boolean hasSignatures() { + return signatures != null && signatures.length > 0; + } + + /** Returns true if the signing details have past signing certificates. */ + public boolean hasPastSigningCertificates() { + return pastSigningCertificates != null && pastSigningCertificates.length > 0; + } + + /** + * Determines if the provided {@code oldDetails} is an ancestor of or the same as this one. + * If the {@code oldDetails} signing certificate appears in our pastSigningCertificates, + * then that means it has authorized a signing certificate rotation, which eventually leads + * to our certificate, and thus can be trusted. If this method evaluates to true, this + * SigningDetails object should be trusted if the previous one is. + */ + public boolean hasAncestorOrSelf(SigningDetails oldDetails) { + if (this == UNKNOWN || oldDetails == UNKNOWN) { + return false; + } + if (oldDetails.signatures.length > 1) { + + // multiple-signer packages cannot rotate signing certs, so we just compare current + // signers for an exact match + return signaturesMatchExactly(oldDetails); + } else { + + // we may have signing certificate rotation history, check to see if the oldDetails + // was one of our old signing certificates + return hasCertificate(oldDetails.signatures[0]); + } + } + + /** + * Similar to {@code hasAncestorOrSelf}. Returns true only if this {@code SigningDetails} + * is a descendant of {@code oldDetails}, not if they're the same. This is used to + * determine if this object is newer than the provided one. + */ + public boolean hasAncestor(SigningDetails oldDetails) { + if (this == UNKNOWN || oldDetails == UNKNOWN) { + return false; + } + if (this.hasPastSigningCertificates() && oldDetails.signatures.length == 1) { + + // the last entry in pastSigningCertificates is the current signer, ignore it + for (int i = 0; i < pastSigningCertificates.length - 1; i++) { + if (pastSigningCertificates[i].equals(oldDetails.signatures[0])) { + return true; + } + } + } + return false; + } + + /** + * Returns whether this {@code SigningDetails} has a signer in common with the provided + * {@code otherDetails} with the specified {@code flags} capabilities provided by this + * signer. + * + *

Note this method allows for the signing lineage to diverge, so this should only be + * used for instances where the only requirement is a common signer in the lineage with + * the specified capabilities. If the current signer of this instance is an ancestor of + * {@code otherDetails} then {@code true} is immediately returned since the current signer + * has all capabilities granted. + */ + public boolean hasCommonSignerWithCapability(SigningDetails otherDetails, + @CertCapabilities int flags) { + if (this == UNKNOWN || otherDetails == UNKNOWN) { + return false; + } + // If either is signed with more than one signer then both must be signed by the same + // signers to consider the capabilities granted. + if (signatures.length > 1 || otherDetails.signatures.length > 1) { + return signaturesMatchExactly(otherDetails); + } + // The Signature class does not use the granted capabilities in the hashCode + // computation, so a Set can be used to check for a common signer. + Set otherSignatures = new ArraySet<>(); + if (otherDetails.hasPastSigningCertificates()) { + otherSignatures.addAll(Arrays.asList(otherDetails.pastSigningCertificates)); + } else { + otherSignatures.addAll(Arrays.asList(otherDetails.signatures)); + } + // If the current signer of this instance is an ancestor of the other than return true + // since all capabilities are granted to the current signer. + if (otherSignatures.contains(signatures[0])) { + return true; + } + if (hasPastSigningCertificates()) { + // Since the current signer was checked above and the last signature in the + // pastSigningCertificates is the current signer skip checking the last element. + for (int i = 0; i < pastSigningCertificates.length - 1; i++) { + if (otherSignatures.contains(pastSigningCertificates[i])) { + // If the caller specified multiple capabilities ensure all are set. + if ((pastSigningCertificates[i].getFlags() & flags) == flags) { + return true; + } + } + } + } + return false; + } + + /** + * Determines if the provided {@code oldDetails} is an ancestor of this one, and whether or + * not this one grants it the provided capability, represented by the {@code flags} + * parameter. In the event of signing certificate rotation, a package may still interact + * with entities signed by its old signing certificate and not want to break previously + * functioning behavior. The {@code flags} value determines which capabilities the app + * signed by the newer signing certificate would like to continue to give to its previous + * signing certificate(s). + */ + public boolean checkCapability(SigningDetails oldDetails, @CertCapabilities int flags) { + if (this == UNKNOWN || oldDetails == UNKNOWN) { + return false; + } + if (oldDetails.signatures.length > 1) { + + // multiple-signer packages cannot rotate signing certs, so we must have an exact + // match, which also means all capabilities are granted + return signaturesMatchExactly(oldDetails); + } else { + + // we may have signing certificate rotation history, check to see if the oldDetails + // was one of our old signing certificates, and if we grant it the capability it's + // requesting + return hasCertificate(oldDetails.signatures[0], flags); + } + } + + /** + * A special case of {@code checkCapability} which re-encodes both sets of signing + * certificates to counteract a previous re-encoding. + */ + public boolean checkCapabilityRecover(SigningDetails oldDetails, + @CertCapabilities int flags) throws CertificateException { + if (oldDetails == UNKNOWN || this == UNKNOWN) { + return false; + } + if (hasPastSigningCertificates() && oldDetails.signatures.length == 1) { + + // signing certificates may have rotated, check entire history for effective match + for (int i = 0; i < pastSigningCertificates.length; i++) { + if (Signature.areEffectiveMatch( + oldDetails.signatures[0], + pastSigningCertificates[i]) + && pastSigningCertificates[i].getFlags() == flags) { + return true; + } + } + } else { + return Signature.areEffectiveArraysMatch(oldDetails.signatures, signatures); + } + return false; + } + + /** + * Determine if {@code signature} is in this SigningDetails' signing certificate history, + * including the current signer. Automatically returns false if this object has multiple + * signing certificates, since rotation is only supported for single-signers; this is + * enforced by {@code hasCertificateInternal}. + */ + public boolean hasCertificate(Signature signature) { + return hasCertificateInternal(signature, PAST_CERT_EXISTS); + } + + /** + * Determine if {@code signature} is in this SigningDetails' signing certificate history, + * including the current signer, and whether or not it has the given permission. + * Certificates which match our current signer automatically get all capabilities. + * Automatically returns false if this object has multiple signing certificates, since + * rotation is only supported for single-signers. + */ + public boolean hasCertificate(Signature signature, @CertCapabilities int flags) { + return hasCertificateInternal(signature, flags); + } + + /** Convenient wrapper for calling {@code hasCertificate} with certificate's raw bytes. */ + public boolean hasCertificate(byte[] certificate) { + Signature signature = new Signature(certificate); + return hasCertificate(signature); + } + + private boolean hasCertificateInternal(Signature signature, int flags) { + if (this == UNKNOWN) { + return false; + } + + // only single-signed apps can have pastSigningCertificates + if (hasPastSigningCertificates()) { + + // check all past certs, except for the current one, which automatically gets all + // capabilities, since it is the same as the current signature + for (int i = 0; i < pastSigningCertificates.length - 1; i++) { + if (pastSigningCertificates[i].equals(signature)) { + if (flags == PAST_CERT_EXISTS + || (flags & pastSigningCertificates[i].getFlags()) == flags) { + return true; + } + } + } + } + + // not in previous certs signing history, just check the current signer and make sure + // we are singly-signed + return signatures.length == 1 && signatures[0].equals(signature); + } + + /** + * Determines if the provided {@code sha256String} is an ancestor of this one, and whether + * or not this one grants it the provided capability, represented by the {@code flags} + * parameter. In the event of signing certificate rotation, a package may still interact + * with entities signed by its old signing certificate and not want to break previously + * functioning behavior. The {@code flags} value determines which capabilities the app + * signed by the newer signing certificate would like to continue to give to its previous + * signing certificate(s). + * + * @param sha256String A hex-encoded representation of a sha256 digest. In the case of an + * app with multiple signers, this represents the hex-encoded sha256 + * digest of the combined hex-encoded sha256 digests of each individual + * signing certificate according to {@link + * PackageUtils#computeSignaturesSha256Digest(Signature[])} + */ + public boolean checkCapability(String sha256String, @CertCapabilities int flags) { + if (this == UNKNOWN) { + return false; + } + + // first see if the hash represents a single-signer in our signing history + byte[] sha256Bytes = sha256String == null + ? null : HexEncoding.decode(sha256String, false /* allowSingleChar */); + if (hasSha256Certificate(sha256Bytes, flags)) { + return true; + } + + // Not in signing history, either represents multiple signatures or not a match. + // Multiple signers can't rotate, so no need to check flags, just see if the SHAs match. + // We already check the single-signer case above as part of hasSha256Certificate, so no + // need to verify we have multiple signers, just run the old check + // just consider current signing certs + final String[] mSignaturesSha256Digests = + PackageUtils.computeSignaturesSha256Digests(signatures); + final String mSignaturesSha256Digest = + PackageUtils.computeSignaturesSha256Digest(mSignaturesSha256Digests); + return mSignaturesSha256Digest.equals(sha256String); + } + + /** + * Determine if the {@code sha256Certificate} is in this SigningDetails' signing certificate + * history, including the current signer. Automatically returns false if this object has + * multiple signing certificates, since rotation is only supported for single-signers. + */ + public boolean hasSha256Certificate(byte[] sha256Certificate) { + return hasSha256CertificateInternal(sha256Certificate, PAST_CERT_EXISTS); + } + + /** + * Determine if the {@code sha256Certificate} certificate hash corresponds to a signing + * certificate in this SigningDetails' signing certificate history, including the current + * signer, and whether or not it has the given permission. Certificates which match our + * current signer automatically get all capabilities. Automatically returns false if this + * object has multiple signing certificates, since rotation is only supported for + * single-signers. + */ + public boolean hasSha256Certificate(byte[] sha256Certificate, @CertCapabilities int flags) { + return hasSha256CertificateInternal(sha256Certificate, flags); + } + + private boolean hasSha256CertificateInternal(byte[] sha256Certificate, int flags) { + if (this == UNKNOWN) { + return false; + } + if (hasPastSigningCertificates()) { + + // check all past certs, except for the last one, which automatically gets all + // capabilities, since it is the same as the current signature, and is checked below + for (int i = 0; i < pastSigningCertificates.length - 1; i++) { + byte[] digest = PackageUtils.computeSha256DigestBytes( + pastSigningCertificates[i].toByteArray()); + if (Arrays.equals(sha256Certificate, digest)) { + if (flags == PAST_CERT_EXISTS + || (flags & pastSigningCertificates[i].getFlags()) == flags) { + return true; + } + } + } + } + + // not in previous certs signing history, just check the current signer + if (signatures.length == 1) { + byte[] digest = + PackageUtils.computeSha256DigestBytes(signatures[0].toByteArray()); + return Arrays.equals(sha256Certificate, digest); + } + return false; + } + + /** Returns true if the signatures in this and other match exactly. */ + public boolean signaturesMatchExactly(SigningDetails other) { + return Signature.areExactArraysMatch(this.signatures, other.signatures); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + boolean isUnknown = UNKNOWN == this; + dest.writeBoolean(isUnknown); + if (isUnknown) { + return; + } + dest.writeTypedArray(this.signatures, flags); + dest.writeInt(this.signatureSchemeVersion); + dest.writeArraySet(this.publicKeys); + dest.writeTypedArray(this.pastSigningCertificates, flags); + } + + protected SigningDetails(Parcel in) { + final ClassLoader boot = Object.class.getClassLoader(); + this.signatures = in.createTypedArray(Signature.CREATOR); + this.signatureSchemeVersion = in.readInt(); + this.publicKeys = (ArraySet) in.readArraySet(boot); + this.pastSigningCertificates = in.createTypedArray(Signature.CREATOR); + } + + public static final @android.annotation.NonNull Creator CREATOR = new Creator() { + @Override + public SigningDetails createFromParcel(Parcel source) { + if (source.readBoolean()) { + return UNKNOWN; + } + return new SigningDetails(source); + } + + @Override + public SigningDetails[] newArray(int size) { + return new SigningDetails[size]; + } + }; + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof SigningDetails)) return false; + + SigningDetails that = (SigningDetails) o; + + if (signatureSchemeVersion != that.signatureSchemeVersion) return false; + if (!Signature.areExactArraysMatch(signatures, that.signatures)) return false; + if (publicKeys != null) { + if (!publicKeys.equals((that.publicKeys))) { + return false; + } + } else if (that.publicKeys != null) { + return false; + } + + // can't use Signature.areExactArraysMatch() because order matters with the past + // signing certs + if (!Arrays.equals(pastSigningCertificates, that.pastSigningCertificates)) { + return false; + } + // The capabilities for the past signing certs must match as well. + for (int i = 0; i < pastSigningCertificates.length; i++) { + if (pastSigningCertificates[i].getFlags() + != that.pastSigningCertificates[i].getFlags()) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int result = +Arrays.hashCode(signatures); + result = 31 * result + signatureSchemeVersion; + result = 31 * result + (publicKeys != null ? publicKeys.hashCode() : 0); + result = 31 * result + Arrays.hashCode(pastSigningCertificates); + return result; + } + + /** + * Builder of {@code SigningDetails} instances. + */ + public static class Builder { + private Signature[] mSignatures; + private int mSignatureSchemeVersion = SignatureSchemeVersion.UNKNOWN; + private Signature[] mPastSigningCertificates; + + @UnsupportedAppUsage + public Builder() { + } + + /** get signing certificates used to sign the current APK */ + @UnsupportedAppUsage + public Builder setSignatures(Signature[] signatures) { + mSignatures = signatures; + return this; + } + + /** set the signature scheme version used to sign the APK */ + @UnsupportedAppUsage + public Builder setSignatureSchemeVersion(int signatureSchemeVersion) { + mSignatureSchemeVersion = signatureSchemeVersion; + return this; + } + + /** set the signing certificates by which the APK proved it can be authenticated */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public Builder setPastSigningCertificates(Signature[] pastSigningCertificates) { + mPastSigningCertificates = pastSigningCertificates; + return this; + } + + private void checkInvariants() { + // must have signatures and scheme version set + if (mSignatures == null) { + throw new IllegalStateException("SigningDetails requires the current signing" + + " certificates."); + } + } + /** build a {@code SigningDetails} object */ + @UnsupportedAppUsage + public SigningDetails build() + throws CertificateException { + checkInvariants(); + return new SigningDetails(mSignatures, mSignatureSchemeVersion, + mPastSigningCertificates); + } + } + } + + /** + * Representation of a full package parsed from APK files on disk. A package + * consists of a single base APK, and zero or more split APKs. + * + * Deprecated internally. Use AndroidPackage instead. + */ + public final static class Package implements Parcelable { + + @UnsupportedAppUsage + public String packageName; + + // The package name declared in the manifest as the package can be + // renamed, for example static shared libs use synthetic package names. + public String manifestPackageName; + + /** Names of any split APKs, ordered by parsed splitName */ + public String[] splitNames; + + // TODO: work towards making these paths invariant + + public String volumeUuid; + + /** + * Path where this package was found on disk. For monolithic packages + * this is path to single base APK file; for cluster packages this is + * path to the cluster directory. + */ + public String codePath; + + /** Path of base APK */ + public String baseCodePath; + /** Paths of any split APKs, ordered by parsed splitName */ + public String[] splitCodePaths; + + /** Revision code of base APK */ + public int baseRevisionCode; + /** Revision codes of any split APKs, ordered by parsed splitName */ + public int[] splitRevisionCodes; + + /** Flags of any split APKs; ordered by parsed splitName */ + public int[] splitFlags; + + /** + * Private flags of any split APKs; ordered by parsed splitName. + * + * {@hide} + */ + public int[] splitPrivateFlags; + + public boolean baseHardwareAccelerated; + + // For now we only support one application per package. + @UnsupportedAppUsage + public ApplicationInfo applicationInfo = new ApplicationInfo(); + + @UnsupportedAppUsage + public final ArrayList permissions = new ArrayList(0); + @UnsupportedAppUsage + public final ArrayList permissionGroups = new ArrayList(0); + @UnsupportedAppUsage + public final ArrayList activities = new ArrayList(0); + @UnsupportedAppUsage + public final ArrayList receivers = new ArrayList(0); + @UnsupportedAppUsage + public final ArrayList providers = new ArrayList(0); + @UnsupportedAppUsage + public final ArrayList services = new ArrayList(0); + @UnsupportedAppUsage + public final ArrayList instrumentation = new ArrayList(0); + + @UnsupportedAppUsage + public final ArrayList requestedPermissions = new ArrayList(); + + /** Permissions requested but not in the manifest. */ + public final ArrayList implicitPermissions = new ArrayList<>(); + + @UnsupportedAppUsage + public ArrayList protectedBroadcasts; + + public Package parentPackage; + public ArrayList childPackages; + + public String staticSharedLibName = null; + public long staticSharedLibVersion = 0; + public ArrayList libraryNames = null; + @UnsupportedAppUsage + public ArrayList usesLibraries = null; + public ArrayList usesStaticLibraries = null; + public long[] usesStaticLibrariesVersions = null; + public String[][] usesStaticLibrariesCertDigests = null; + @UnsupportedAppUsage + public ArrayList usesOptionalLibraries = null; + @UnsupportedAppUsage + public String[] usesLibraryFiles = null; + public ArrayList usesLibraryInfos = null; + + public ArrayList preferredActivityFilters = null; + + public ArrayList mOriginalPackages = null; + public String mRealPackage = null; + public ArrayList mAdoptPermissions = null; + + // We store the application meta-data independently to avoid multiple unwanted references + @UnsupportedAppUsage + public Bundle mAppMetaData = null; + + // The version code declared for this package. + @UnsupportedAppUsage + public int mVersionCode; + + // The major version code declared for this package. + public int mVersionCodeMajor; + + // Return long containing mVersionCode and mVersionCodeMajor. + public long getLongVersionCode() { + return PackageInfo.composeLongVersionCode(mVersionCodeMajor, mVersionCode); + } + + // The version name declared for this package. + @UnsupportedAppUsage + public String mVersionName; + + // The shared user id that this package wants to use. + @UnsupportedAppUsage + public String mSharedUserId; + + // The shared user label that this package wants to use. + @UnsupportedAppUsage + public int mSharedUserLabel; + + // Signatures that were read from the package. + @UnsupportedAppUsage + @NonNull public SigningDetails mSigningDetails = SigningDetails.UNKNOWN; + + // For use by package manager service for quick lookup of + // preferred up order. + @UnsupportedAppUsage + public int mPreferredOrder = 0; + + // For use by package manager to keep track of when a package was last used. + public long[] mLastPackageUsageTimeInMills = + new long[PackageManager.NOTIFY_PACKAGE_USE_REASONS_COUNT]; + + // // User set enabled state. + // public int mSetEnabled = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + // + // // Whether the package has been stopped. + // public boolean mSetStopped = false; + + // Additional data supplied by callers. + @UnsupportedAppUsage + public Object mExtras; + + // Applications hardware preferences + @UnsupportedAppUsage + public ArrayList configPreferences = null; + + // Applications requested features + @UnsupportedAppUsage + public ArrayList reqFeatures = null; + + // Applications requested feature groups + public ArrayList featureGroups = null; + + @UnsupportedAppUsage + public int installLocation; + + public boolean coreApp; + + /* An app that's required for all users and cannot be uninstalled for a user */ + public boolean mRequiredForAllUsers; + + /* The restricted account authenticator type that is used by this application */ + public String mRestrictedAccountType; + + /* The required account type without which this application will not function */ + public String mRequiredAccountType; + + public String mOverlayTarget; + public String mOverlayTargetName; + public String mOverlayCategory; + public int mOverlayPriority; + public boolean mOverlayIsStatic; + + public int mCompileSdkVersion; + public String mCompileSdkVersionCodename; + + /** + * Data used to feed the KeySetManagerService + */ + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public ArraySet mUpgradeKeySets; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public ArrayMap> mKeySetMapping; + + /** + * The install time abi override for this package, if any. + * + * TODO: This seems like a horrible place to put the abiOverride because + * this isn't something the packageParser parsers. However, this fits in with + * the rest of the PackageManager where package scanning randomly pushes + * and prods fields out of {@code this.applicationInfo}. + */ + public String cpuAbiOverride; + /** + * The install time abi override to choose 32bit abi's when multiple abi's + * are present. This is only meaningfull for multiarch applications. + * The use32bitAbi attribute is ignored if cpuAbiOverride is also set. + */ + public boolean use32bitAbi; + + public byte[] restrictUpdateHash; + + /** Set if the app or any of its components are visible to instant applications. */ + public boolean visibleToInstantApps; + /** Whether or not the package is a stub and must be replaced by the full version. */ + public boolean isStub; + + @UnsupportedAppUsage + public Package(String packageName) { + this.packageName = packageName; + this.manifestPackageName = packageName; + applicationInfo.packageName = packageName; + applicationInfo.uid = -1; + } + + public void setApplicationVolumeUuid(String volumeUuid) { + final UUID storageUuid = StorageManager.convert(volumeUuid); + this.applicationInfo.volumeUuid = volumeUuid; + this.applicationInfo.storageUuid = storageUuid; + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).applicationInfo.volumeUuid = volumeUuid; + childPackages.get(i).applicationInfo.storageUuid = storageUuid; + } + } + } + + public void setApplicationInfoCodePath(String codePath) { + this.applicationInfo.setCodePath(codePath); + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).applicationInfo.setCodePath(codePath); + } + } + } + + /** @deprecated Forward locked apps no longer supported. Resource path not needed. */ + @Deprecated + public void setApplicationInfoResourcePath(String resourcePath) { + this.applicationInfo.setResourcePath(resourcePath); + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).applicationInfo.setResourcePath(resourcePath); + } + } + } + + /** @deprecated Forward locked apps no longer supported. Resource path not needed. */ + @Deprecated + public void setApplicationInfoBaseResourcePath(String resourcePath) { + this.applicationInfo.setBaseResourcePath(resourcePath); + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).applicationInfo.setBaseResourcePath(resourcePath); + } + } + } + + public void setApplicationInfoBaseCodePath(String baseCodePath) { + this.applicationInfo.setBaseCodePath(baseCodePath); + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).applicationInfo.setBaseCodePath(baseCodePath); + } + } + } + + public List getChildPackageNames() { + if (childPackages == null) { + return null; + } + final int childCount = childPackages.size(); + final List childPackageNames = new ArrayList<>(childCount); + for (int i = 0; i < childCount; i++) { + String childPackageName = childPackages.get(i).packageName; + childPackageNames.add(childPackageName); + } + return childPackageNames; + } + + public boolean hasChildPackage(String packageName) { + final int childCount = (childPackages != null) ? childPackages.size() : 0; + for (int i = 0; i < childCount; i++) { + if (childPackages.get(i).packageName.equals(packageName)) { + return true; + } + } + return false; + } + + public void setApplicationInfoSplitCodePaths(String[] splitCodePaths) { + this.applicationInfo.setSplitCodePaths(splitCodePaths); + // Children have no splits + } + + /** @deprecated Forward locked apps no longer supported. Resource path not needed. */ + @Deprecated + public void setApplicationInfoSplitResourcePaths(String[] resroucePaths) { + this.applicationInfo.setSplitResourcePaths(resroucePaths); + // Children have no splits + } + + public void setSplitCodePaths(String[] codePaths) { + this.splitCodePaths = codePaths; + } + + public void setCodePath(String codePath) { + this.codePath = codePath; + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).codePath = codePath; + } + } + } + + public void setBaseCodePath(String baseCodePath) { + this.baseCodePath = baseCodePath; + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).baseCodePath = baseCodePath; + } + } + } + + /** Sets signing details on the package and any of its children. */ + public void setSigningDetails(@NonNull SigningDetails signingDetails) { + mSigningDetails = signingDetails; + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).mSigningDetails = signingDetails; + } + } + } + + public void setVolumeUuid(String volumeUuid) { + this.volumeUuid = volumeUuid; + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).volumeUuid = volumeUuid; + } + } + } + + public void setApplicationInfoFlags(int mask, int flags) { + applicationInfo.flags = (applicationInfo.flags & ~mask) | (mask & flags); + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).applicationInfo.flags = + (applicationInfo.flags & ~mask) | (mask & flags); + } + } + } + + public void setUse32bitAbi(boolean use32bitAbi) { + this.use32bitAbi = use32bitAbi; + if (childPackages != null) { + final int packageCount = childPackages.size(); + for (int i = 0; i < packageCount; i++) { + childPackages.get(i).use32bitAbi = use32bitAbi; + } + } + } + + public boolean isLibrary() { + return staticSharedLibName != null || !ArrayUtils.isEmpty(libraryNames); + } + + public List getAllCodePaths() { + ArrayList paths = new ArrayList<>(); + paths.add(baseCodePath); + if (!ArrayUtils.isEmpty(splitCodePaths)) { + Collections.addAll(paths, splitCodePaths); + } + return paths; + } + + /** + * Filtered set of {@link #getAllCodePaths()} that excludes + * resource-only APKs. + */ + public List getAllCodePathsExcludingResourceOnly() { + ArrayList paths = new ArrayList<>(); + if ((applicationInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0) { + paths.add(baseCodePath); + } + if (!ArrayUtils.isEmpty(splitCodePaths)) { + for (int i = 0; i < splitCodePaths.length; i++) { + if ((splitFlags[i] & ApplicationInfo.FLAG_HAS_CODE) != 0) { + paths.add(splitCodePaths[i]); + } + } + } + return paths; + } + + @UnsupportedAppUsage + public void setPackageName(String newName) { + packageName = newName; + applicationInfo.packageName = newName; + for (int i=permissions.size()-1; i>=0; i--) { + permissions.get(i).setPackageName(newName); + } + for (int i=permissionGroups.size()-1; i>=0; i--) { + permissionGroups.get(i).setPackageName(newName); + } + for (int i=activities.size()-1; i>=0; i--) { + activities.get(i).setPackageName(newName); + } + for (int i=receivers.size()-1; i>=0; i--) { + receivers.get(i).setPackageName(newName); + } + for (int i=providers.size()-1; i>=0; i--) { + providers.get(i).setPackageName(newName); + } + for (int i=services.size()-1; i>=0; i--) { + services.get(i).setPackageName(newName); + } + for (int i=instrumentation.size()-1; i>=0; i--) { + instrumentation.get(i).setPackageName(newName); + } + } + + public boolean hasComponentClassName(String name) { + for (int i=activities.size()-1; i>=0; i--) { + if (name.equals(activities.get(i).className)) { + return true; + } + } + for (int i=receivers.size()-1; i>=0; i--) { + if (name.equals(receivers.get(i).className)) { + return true; + } + } + for (int i=providers.size()-1; i>=0; i--) { + if (name.equals(providers.get(i).className)) { + return true; + } + } + for (int i=services.size()-1; i>=0; i--) { + if (name.equals(services.get(i).className)) { + return true; + } + } + for (int i=instrumentation.size()-1; i>=0; i--) { + if (name.equals(instrumentation.get(i).className)) { + return true; + } + } + return false; + } + + /** @hide */ + public boolean isExternal() { + return applicationInfo.isExternal(); + } + + /** @hide */ + public boolean isForwardLocked() { + return false; + } + + /** @hide */ + public boolean isOem() { + return applicationInfo.isOem(); + } + + /** @hide */ + public boolean isVendor() { + return applicationInfo.isVendor(); + } + + /** @hide */ + public boolean isProduct() { + return applicationInfo.isProduct(); + } + + /** @hide */ + public boolean isSystemExt() { + return applicationInfo.isSystemExt(); + } + + /** @hide */ + public boolean isOdm() { + return applicationInfo.isOdm(); + } + + /** @hide */ + public boolean isPrivileged() { + return applicationInfo.isPrivilegedApp(); + } + + /** @hide */ + public boolean isSystem() { + return applicationInfo.isSystemApp(); + } + + /** @hide */ + public boolean isUpdatedSystemApp() { + return applicationInfo.isUpdatedSystemApp(); + } + + /** @hide */ + public boolean canHaveOatDir() { + // Nobody should be calling this method ever, but we can't rely on this. + // Thus no logic here and a reasonable return value. + return true; + } + + public boolean isMatch(int flags) { + if ((flags & PackageManager.MATCH_SYSTEM_ONLY) != 0) { + return isSystem(); + } + return true; + } + + public long getLatestPackageUseTimeInMills() { + long latestUse = 0L; + for (long use : mLastPackageUsageTimeInMills) { + latestUse = Math.max(latestUse, use); + } + return latestUse; + } + + public long getLatestForegroundPackageUseTimeInMills() { + int[] foregroundReasons = { + PackageManager.NOTIFY_PACKAGE_USE_ACTIVITY, + PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE + }; + + long latestUse = 0L; + for (int reason : foregroundReasons) { + latestUse = Math.max(latestUse, mLastPackageUsageTimeInMills[reason]); + } + return latestUse; + } + + public String toString() { + return "Package{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + packageName + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + public Package(Parcel dest) { + // We use the boot classloader for all classes that we load. + final ClassLoader boot = Object.class.getClassLoader(); + + packageName = dest.readString().intern(); + manifestPackageName = dest.readString(); + splitNames = dest.readStringArray(); + volumeUuid = dest.readString(); + codePath = dest.readString(); + baseCodePath = dest.readString(); + splitCodePaths = dest.readStringArray(); + baseRevisionCode = dest.readInt(); + splitRevisionCodes = dest.createIntArray(); + splitFlags = dest.createIntArray(); + splitPrivateFlags = dest.createIntArray(); + baseHardwareAccelerated = (dest.readInt() == 1); + applicationInfo = dest.readParcelable(boot, android.content.pm.ApplicationInfo.class); + if (applicationInfo.permission != null) { + applicationInfo.permission = applicationInfo.permission.intern(); + } + + // We don't serialize the "owner" package and the application info object for each of + // these components, in order to save space and to avoid circular dependencies while + // serialization. We need to fix them all up here. + dest.readParcelableList(permissions, boot, android.content.pm.PackageParser.Permission.class); + fixupOwner(permissions); + dest.readParcelableList(permissionGroups, boot, android.content.pm.PackageParser.PermissionGroup.class); + fixupOwner(permissionGroups); + dest.readParcelableList(activities, boot, android.content.pm.PackageParser.Activity.class); + fixupOwner(activities); + dest.readParcelableList(receivers, boot, android.content.pm.PackageParser.Activity.class); + fixupOwner(receivers); + dest.readParcelableList(providers, boot, android.content.pm.PackageParser.Provider.class); + fixupOwner(providers); + dest.readParcelableList(services, boot, android.content.pm.PackageParser.Service.class); + fixupOwner(services); + dest.readParcelableList(instrumentation, boot, android.content.pm.PackageParser.Instrumentation.class); + fixupOwner(instrumentation); + + dest.readStringList(requestedPermissions); + internStringArrayList(requestedPermissions); + dest.readStringList(implicitPermissions); + internStringArrayList(implicitPermissions); + protectedBroadcasts = dest.createStringArrayList(); + internStringArrayList(protectedBroadcasts); + + parentPackage = dest.readParcelable(boot, android.content.pm.PackageParser.Package.class); + + childPackages = new ArrayList<>(); + dest.readParcelableList(childPackages, boot, android.content.pm.PackageParser.Package.class); + if (childPackages.size() == 0) { + childPackages = null; + } + + staticSharedLibName = dest.readString(); + if (staticSharedLibName != null) { + staticSharedLibName = staticSharedLibName.intern(); + } + staticSharedLibVersion = dest.readLong(); + libraryNames = dest.createStringArrayList(); + internStringArrayList(libraryNames); + usesLibraries = dest.createStringArrayList(); + internStringArrayList(usesLibraries); + usesOptionalLibraries = dest.createStringArrayList(); + internStringArrayList(usesOptionalLibraries); + usesLibraryFiles = dest.readStringArray(); + + usesLibraryInfos = dest.createTypedArrayList(SharedLibraryInfo.CREATOR); + + final int libCount = dest.readInt(); + if (libCount > 0) { + usesStaticLibraries = new ArrayList<>(libCount); + dest.readStringList(usesStaticLibraries); + internStringArrayList(usesStaticLibraries); + usesStaticLibrariesVersions = new long[libCount]; + dest.readLongArray(usesStaticLibrariesVersions); + usesStaticLibrariesCertDigests = new String[libCount][]; + for (int i = 0; i < libCount; i++) { + usesStaticLibrariesCertDigests[i] = dest.createStringArray(); + } + } + + preferredActivityFilters = new ArrayList<>(); + dest.readParcelableList(preferredActivityFilters, boot, android.content.pm.PackageParser.ActivityIntentInfo.class); + if (preferredActivityFilters.size() == 0) { + preferredActivityFilters = null; + } + + mOriginalPackages = dest.createStringArrayList(); + mRealPackage = dest.readString(); + mAdoptPermissions = dest.createStringArrayList(); + mAppMetaData = dest.readBundle(); + mVersionCode = dest.readInt(); + mVersionCodeMajor = dest.readInt(); + mVersionName = dest.readString(); + if (mVersionName != null) { + mVersionName = mVersionName.intern(); + } + mSharedUserId = dest.readString(); + if (mSharedUserId != null) { + mSharedUserId = mSharedUserId.intern(); + } + mSharedUserLabel = dest.readInt(); + + mSigningDetails = dest.readParcelable(boot, android.content.pm.PackageParser.SigningDetails.class); + + mPreferredOrder = dest.readInt(); + + // long[] packageUsageTimeMillis is not persisted because it isn't information that + // is parsed from the APK. + + // Object mExtras is not persisted because it is not information that is read from + // the APK, rather, it is supplied by callers. + + + configPreferences = new ArrayList<>(); + dest.readParcelableList(configPreferences, boot, android.content.pm.ConfigurationInfo.class); + if (configPreferences.size() == 0) { + configPreferences = null; + } + + reqFeatures = new ArrayList<>(); + dest.readParcelableList(reqFeatures, boot, android.content.pm.FeatureInfo.class); + if (reqFeatures.size() == 0) { + reqFeatures = null; + } + + featureGroups = new ArrayList<>(); + dest.readParcelableList(featureGroups, boot, android.content.pm.FeatureGroupInfo.class); + if (featureGroups.size() == 0) { + featureGroups = null; + } + + installLocation = dest.readInt(); + coreApp = (dest.readInt() == 1); + mRequiredForAllUsers = (dest.readInt() == 1); + mRestrictedAccountType = dest.readString(); + mRequiredAccountType = dest.readString(); + mOverlayTarget = dest.readString(); + mOverlayTargetName = dest.readString(); + mOverlayCategory = dest.readString(); + mOverlayPriority = dest.readInt(); + mOverlayIsStatic = (dest.readInt() == 1); + mCompileSdkVersion = dest.readInt(); + mCompileSdkVersionCodename = dest.readString(); + mUpgradeKeySets = (ArraySet) dest.readArraySet(boot); + + mKeySetMapping = readKeySetMapping(dest); + + cpuAbiOverride = dest.readString(); + use32bitAbi = (dest.readInt() == 1); + restrictUpdateHash = dest.createByteArray(); + visibleToInstantApps = dest.readInt() == 1; + } + + private static void internStringArrayList(List list) { + if (list != null) { + final int N = list.size(); + for (int i = 0; i < N; ++i) { + list.set(i, list.get(i).intern()); + } + } + } + + /** + * Sets the package owner and the the {@code applicationInfo} for every component + * owner by this package. + */ + public void fixupOwner(List> list) { + if (list != null) { + for (Component c : list) { + c.owner = this; + if (c instanceof Activity) { + ((Activity) c).info.applicationInfo = this.applicationInfo; + } else if (c instanceof Service) { + ((Service) c).info.applicationInfo = this.applicationInfo; + } else if (c instanceof Provider) { + ((Provider) c).info.applicationInfo = this.applicationInfo; + } + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(packageName); + dest.writeString(manifestPackageName); + dest.writeStringArray(splitNames); + dest.writeString(volumeUuid); + dest.writeString(codePath); + dest.writeString(baseCodePath); + dest.writeStringArray(splitCodePaths); + dest.writeInt(baseRevisionCode); + dest.writeIntArray(splitRevisionCodes); + dest.writeIntArray(splitFlags); + dest.writeIntArray(splitPrivateFlags); + dest.writeInt(baseHardwareAccelerated ? 1 : 0); + dest.writeParcelable(applicationInfo, flags); + + dest.writeParcelableList(permissions, flags); + dest.writeParcelableList(permissionGroups, flags); + dest.writeParcelableList(activities, flags); + dest.writeParcelableList(receivers, flags); + dest.writeParcelableList(providers, flags); + dest.writeParcelableList(services, flags); + dest.writeParcelableList(instrumentation, flags); + + dest.writeStringList(requestedPermissions); + dest.writeStringList(implicitPermissions); + dest.writeStringList(protectedBroadcasts); + + // TODO: This doesn't work: b/64295061 + dest.writeParcelable(parentPackage, flags); + dest.writeParcelableList(childPackages, flags); + + dest.writeString(staticSharedLibName); + dest.writeLong(staticSharedLibVersion); + dest.writeStringList(libraryNames); + dest.writeStringList(usesLibraries); + dest.writeStringList(usesOptionalLibraries); + dest.writeStringArray(usesLibraryFiles); + dest.writeTypedList(usesLibraryInfos); + + if (ArrayUtils.isEmpty(usesStaticLibraries)) { + dest.writeInt(-1); + } else { + dest.writeInt(usesStaticLibraries.size()); + dest.writeStringList(usesStaticLibraries); + dest.writeLongArray(usesStaticLibrariesVersions); + for (String[] usesStaticLibrariesCertDigest : usesStaticLibrariesCertDigests) { + dest.writeStringArray(usesStaticLibrariesCertDigest); + } + } + + dest.writeParcelableList(preferredActivityFilters, flags); + + dest.writeStringList(mOriginalPackages); + dest.writeString(mRealPackage); + dest.writeStringList(mAdoptPermissions); + dest.writeBundle(mAppMetaData); + dest.writeInt(mVersionCode); + dest.writeInt(mVersionCodeMajor); + dest.writeString(mVersionName); + dest.writeString(mSharedUserId); + dest.writeInt(mSharedUserLabel); + + dest.writeParcelable(mSigningDetails, flags); + + dest.writeInt(mPreferredOrder); + + // long[] packageUsageTimeMillis is not persisted because it isn't information that + // is parsed from the APK. + + // Object mExtras is not persisted because it is not information that is read from + // the APK, rather, it is supplied by callers. + + dest.writeParcelableList(configPreferences, flags); + dest.writeParcelableList(reqFeatures, flags); + dest.writeParcelableList(featureGroups, flags); + + dest.writeInt(installLocation); + dest.writeInt(coreApp ? 1 : 0); + dest.writeInt(mRequiredForAllUsers ? 1 : 0); + dest.writeString(mRestrictedAccountType); + dest.writeString(mRequiredAccountType); + dest.writeString(mOverlayTarget); + dest.writeString(mOverlayTargetName); + dest.writeString(mOverlayCategory); + dest.writeInt(mOverlayPriority); + dest.writeInt(mOverlayIsStatic ? 1 : 0); + dest.writeInt(mCompileSdkVersion); + dest.writeString(mCompileSdkVersionCodename); + dest.writeArraySet(mUpgradeKeySets); + writeKeySetMapping(dest, mKeySetMapping); + dest.writeString(cpuAbiOverride); + dest.writeInt(use32bitAbi ? 1 : 0); + dest.writeByteArray(restrictUpdateHash); + dest.writeInt(visibleToInstantApps ? 1 : 0); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Package createFromParcel(Parcel in) { + return new Package(in); + } + + public Package[] newArray(int size) { + return new Package[size]; + } + }; + } + + public static abstract class Component { + @UnsupportedAppUsage + public final ArrayList intents; + @UnsupportedAppUsage + public final String className; + + @UnsupportedAppUsage + public Bundle metaData; + @UnsupportedAppUsage + public Package owner; + /** The order of this component in relation to its peers */ + public int order; + + ComponentName componentName; + String componentShortName; + + public Component(Package owner, ArrayList intents, String className) { + this.owner = owner; + this.intents = intents; + this.className = className; + } + + public Component(Package owner) { + this.owner = owner; + this.intents = null; + this.className = null; + } + + public Component(final ParsePackageItemArgs args, final PackageItemInfo outInfo) { + owner = args.owner; + intents = new ArrayList(0); + if (parsePackageItemInfo(args.owner, outInfo, args.outError, args.tag, args.sa, + true /*nameRequired*/, args.nameRes, args.labelRes, args.iconRes, + args.roundIconRes, args.logoRes, args.bannerRes)) { + className = outInfo.name; + } else { + className = null; + } + } + + public Component(final ParseComponentArgs args, final ComponentInfo outInfo) { + this(args, (PackageItemInfo)outInfo); + if (args.outError[0] != null) { + return; + } + + if (args.processRes != 0) { + CharSequence pname; + if (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.FROYO) { + pname = args.sa.getNonConfigurationString(args.processRes, + 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 = args.sa.getNonResourceString(args.processRes); + } + outInfo.processName = buildProcessName(owner.applicationInfo.packageName, + owner.applicationInfo.processName, pname, + args.flags, args.sepProcesses, args.outError); + } + + if (args.descriptionRes != 0) { + outInfo.descriptionRes = args.sa.getResourceId(args.descriptionRes, 0); + } + + outInfo.enabled = args.sa.getBoolean(args.enabledRes, true); + } + + public Component(Component clone) { + owner = clone.owner; + intents = clone.intents; + className = clone.className; + componentName = clone.componentName; + componentShortName = clone.componentShortName; + } + + @UnsupportedAppUsage + public ComponentName getComponentName() { + if (componentName != null) { + return componentName; + } + if (className != null) { + componentName = new ComponentName(owner.applicationInfo.packageName, + className); + } + return componentName; + } + + protected Component(Parcel in) { + className = in.readString(); + metaData = in.readBundle(); + intents = createIntentsList(in); + + owner = null; + } + + protected void writeToParcel(Parcel dest, int flags) { + dest.writeString(className); + dest.writeBundle(metaData); + + writeIntentsList(intents, dest, flags); + } + + /** + *

+ * Implementation note: The serialized form for the intent list also contains the name + * of the concrete class that's stored in the list, and assumes that every element of the + * list is of the same type. This is very similar to the original parcelable mechanism. + * We cannot use that directly because IntentInfo extends IntentFilter, which is parcelable + * and is public API. It also declares Parcelable related methods as final which means + * we can't extend them. The approach of using composition instead of inheritance leads to + * a large set of cascading changes in the PackageManagerService, which seem undesirable. + * + *

+ * WARNING: The list of objects returned by this function might need to be fixed up + * to make sure their owner fields are consistent. See {@code fixupOwner}. + */ + private static void writeIntentsList(ArrayList list, Parcel out, + int flags) { + if (list == null) { + out.writeInt(-1); + return; + } + + final int N = list.size(); + out.writeInt(N); + + // Don't bother writing the component name if the list is empty. + if (N > 0) { + IntentInfo info = list.get(0); + out.writeString(info.getClass().getName()); + + for (int i = 0; i < N;i++) { + list.get(i).writeIntentInfoToParcel(out, flags); + } + } + } + + private static ArrayList createIntentsList(Parcel in) { + int N = in.readInt(); + if (N == -1) { + return null; + } + + if (N == 0) { + return new ArrayList<>(0); + } + + String componentName = in.readString(); + final ArrayList intentsList; + try { + final Class cls = (Class) Class.forName(componentName); + final Constructor cons = cls.getConstructor(Parcel.class); + + intentsList = new ArrayList<>(N); + for (int i = 0; i < N; ++i) { + intentsList.add(cons.newInstance(in)); + } + } catch (ReflectiveOperationException ree) { + throw new AssertionError("Unable to construct intent list for: " + componentName); + } + + return intentsList; + } + + public void appendComponentShortName(StringBuilder sb) { + ComponentName.appendShortString(sb, owner.applicationInfo.packageName, className); + } + + public void printComponentShortName(PrintWriter pw) { + ComponentName.printShortString(pw, owner.applicationInfo.packageName, className); + } + + public void setPackageName(String packageName) { + componentName = null; + componentShortName = null; + } + } + + public final static class Permission extends Component implements Parcelable { + @UnsupportedAppUsage + public final PermissionInfo info; + @UnsupportedAppUsage + public boolean tree; + @UnsupportedAppUsage + public PermissionGroup group; + + /** + * @hide + */ + public Permission(Package owner, @Nullable String backgroundPermission) { + super(owner); + info = new PermissionInfo(backgroundPermission); + } + + @UnsupportedAppUsage + public Permission(Package _owner, PermissionInfo _info) { + super(_owner); + info = _info; + } + + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + + public String toString() { + return "Permission{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + info.name + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(info, flags); + dest.writeInt(tree ? 1 : 0); + dest.writeParcelable(group, flags); + } + + /** @hide */ + public boolean isAppOp() { + return info.isAppOp(); + } + + private Permission(Parcel in) { + super(in); + final ClassLoader boot = Object.class.getClassLoader(); + info = in.readParcelable(boot, android.content.pm.PermissionInfo.class); + if (info.group != null) { + info.group = info.group.intern(); + } + + tree = (in.readInt() == 1); + group = in.readParcelable(boot, android.content.pm.PackageParser.PermissionGroup.class); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Permission createFromParcel(Parcel in) { + return new Permission(in); + } + + public Permission[] newArray(int size) { + return new Permission[size]; + } + }; + } + + public final static class PermissionGroup extends Component implements Parcelable { + @UnsupportedAppUsage + public final PermissionGroupInfo info; + + public PermissionGroup(Package owner, @StringRes int requestDetailResourceId, + @StringRes int backgroundRequestResourceId, + @StringRes int backgroundRequestDetailResourceId) { + super(owner); + info = new PermissionGroupInfo(requestDetailResourceId, backgroundRequestResourceId, + backgroundRequestDetailResourceId); + } + + public PermissionGroup(Package _owner, PermissionGroupInfo _info) { + super(_owner); + info = _info; + } + + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + + public String toString() { + return "PermissionGroup{" + + Integer.toHexString(System.identityHashCode(this)) + + " " + info.name + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(info, flags); + } + + private PermissionGroup(Parcel in) { + super(in); + info = in.readParcelable(Object.class.getClassLoader(), android.content.pm.PermissionGroupInfo.class); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public PermissionGroup createFromParcel(Parcel in) { + return new PermissionGroup(in); + } + + public PermissionGroup[] newArray(int size) { + return new PermissionGroup[size]; + } + }; + } + + private static boolean copyNeeded(int flags, Package p, + FrameworkPackageUserState state, Bundle metaData, int userId) { + if (userId != UserHandle.USER_SYSTEM) { + // We always need to copy for other users, since we need + // to fix up the uid. + return true; + } + if (state.getEnabledState() != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { + boolean enabled = + state.getEnabledState() == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; + if (p.applicationInfo.enabled != enabled) { + return true; + } + } + boolean suspended = (p.applicationInfo.flags & FLAG_SUSPENDED) != 0; + if (state.isSuspended() != suspended) { + return true; + } + if (!state.isInstalled() || state.isHidden()) { + return true; + } + if (state.isStopped()) { + return true; + } + if (state.isInstantApp() != p.applicationInfo.isInstantApp()) { + return true; + } + if ((flags & PackageManager.GET_META_DATA) != 0 + && (metaData != null || p.mAppMetaData != null)) { + return true; + } + if ((flags & PackageManager.GET_SHARED_LIBRARY_FILES) != 0 + && p.usesLibraryFiles != null) { + return true; + } + if ((flags & PackageManager.GET_SHARED_LIBRARY_FILES) != 0 + && p.usesLibraryInfos != null) { + return true; + } + if (p.staticSharedLibName != null) { + return true; + } + return false; + } + + @UnsupportedAppUsage + public static ApplicationInfo generateApplicationInfo(Package p, int flags, + FrameworkPackageUserState state) { + return generateApplicationInfo(p, flags, state, UserHandle.getCallingUserId()); + } + + private static void updateApplicationInfo(ApplicationInfo ai, int flags, + FrameworkPackageUserState state) { + // CompatibilityMode is global state. + if (!sCompatibilityModeEnabled) { + ai.disableCompatibilityMode(); + } + if (state.isInstalled()) { + ai.flags |= ApplicationInfo.FLAG_INSTALLED; + } else { + ai.flags &= ~ApplicationInfo.FLAG_INSTALLED; + } + if (state.isSuspended()) { + ai.flags |= ApplicationInfo.FLAG_SUSPENDED; + } else { + ai.flags &= ~ApplicationInfo.FLAG_SUSPENDED; + } + if (state.isInstantApp()) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_INSTANT; + } else { + ai.privateFlags &= ~ApplicationInfo.PRIVATE_FLAG_INSTANT; + } + if (state.isVirtualPreload()) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_VIRTUAL_PRELOAD; + } else { + ai.privateFlags &= ~ApplicationInfo.PRIVATE_FLAG_VIRTUAL_PRELOAD; + } + if (state.isHidden()) { + ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_HIDDEN; + } else { + ai.privateFlags &= ~ApplicationInfo.PRIVATE_FLAG_HIDDEN; + } + if (state.getEnabledState() == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + ai.enabled = true; + } else if (state.getEnabledState() + == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) { + ai.enabled = (flags & PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS) != 0; + } else if (state.getEnabledState() == PackageManager.COMPONENT_ENABLED_STATE_DISABLED + || state.getEnabledState() + == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER) { + ai.enabled = false; + } + ai.enabledSetting = state.getEnabledState(); + if (ai.category == ApplicationInfo.CATEGORY_UNDEFINED) { + ai.category = FallbackCategoryProvider.getFallbackCategory(ai.packageName); + } + ai.seInfoUser = getSeinfoUser(state); + final OverlayPaths overlayPaths = state.getAllOverlayPaths(); + if (overlayPaths != null) { + ai.resourceDirs = overlayPaths.getResourceDirs().toArray(new String[0]); + ai.overlayPaths = overlayPaths.getOverlayPaths().toArray(new String[0]); + } + ai.icon = (sUseRoundIcon && ai.roundIconRes != 0) ? ai.roundIconRes : ai.iconRes; + } + + @UnsupportedAppUsage + public static ApplicationInfo generateApplicationInfo(Package p, int flags, + FrameworkPackageUserState state, int userId) { + if (p == null) return null; + if (!checkUseInstalledOrHidden(flags, state, p.applicationInfo) || !p.isMatch(flags)) { + return null; + } + if (!copyNeeded(flags, p, state, null, userId) + && ((flags&PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS) == 0 + || state.getEnabledState() + != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED)) { + // In this case it is safe to directly modify the internal ApplicationInfo state: + // - CompatibilityMode is global state, so will be the same for every call. + // - We only come in to here if the app should reported as installed; this is the + // default state, and we will do a copy otherwise. + // - The enable state will always be reported the same for the application across + // calls; the only exception is for the UNTIL_USED mode, and in that case we will + // be doing a copy. + updateApplicationInfo(p.applicationInfo, flags, state); + return p.applicationInfo; + } + + // Make shallow copy so we can store the metadata/libraries safely + ApplicationInfo ai = new ApplicationInfo(p.applicationInfo); + ai.initForUser(userId); + if ((flags & PackageManager.GET_META_DATA) != 0) { + ai.metaData = p.mAppMetaData; + } + if ((flags & PackageManager.GET_SHARED_LIBRARY_FILES) != 0) { + ai.sharedLibraryFiles = p.usesLibraryFiles; + ai.sharedLibraryInfos = p.usesLibraryInfos; + } + if (state.isStopped()) { + ai.flags |= ApplicationInfo.FLAG_STOPPED; + } else { + ai.flags &= ~ApplicationInfo.FLAG_STOPPED; + } + updateApplicationInfo(ai, flags, state); + return ai; + } + + public static ApplicationInfo generateApplicationInfo(ApplicationInfo ai, int flags, + FrameworkPackageUserState state, int userId) { + if (ai == null) return null; + if (!checkUseInstalledOrHidden(flags, state, ai)) { + return null; + } + // This is only used to return the ResolverActivity; we will just always + // make a copy. + ai = new ApplicationInfo(ai); + ai.initForUser(userId); + if (state.isStopped()) { + ai.flags |= ApplicationInfo.FLAG_STOPPED; + } else { + ai.flags &= ~ApplicationInfo.FLAG_STOPPED; + } + updateApplicationInfo(ai, flags, state); + return ai; + } + + @UnsupportedAppUsage + public static final PermissionInfo generatePermissionInfo( + Permission p, int flags) { + if (p == null) return null; + if ((flags&PackageManager.GET_META_DATA) == 0) { + return p.info; + } + PermissionInfo pi = new PermissionInfo(p.info); + pi.metaData = p.metaData; + return pi; + } + + @UnsupportedAppUsage + public static final PermissionGroupInfo generatePermissionGroupInfo( + PermissionGroup pg, int flags) { + if (pg == null) return null; + if ((flags&PackageManager.GET_META_DATA) == 0) { + return pg.info; + } + PermissionGroupInfo pgi = new PermissionGroupInfo(pg.info); + pgi.metaData = pg.metaData; + return pgi; + } + + public final static class Activity extends Component implements Parcelable { + @UnsupportedAppUsage + public final ActivityInfo info; + private boolean mHasMaxAspectRatio; + private boolean mHasMinAspectRatio; + + private boolean hasMaxAspectRatio() { + return mHasMaxAspectRatio; + } + + private boolean hasMinAspectRatio() { + return mHasMinAspectRatio; + } + + // To construct custom activity which does not exist in manifest + Activity(final Package owner, final String className, final ActivityInfo info) { + super(owner, new ArrayList<>(0), className); + this.info = info; + this.info.applicationInfo = owner.applicationInfo; + } + + public Activity(final ParseComponentArgs args, final ActivityInfo _info) { + super(args, _info); + info = _info; + info.applicationInfo = args.owner.applicationInfo; + } + + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + + + private void setMaxAspectRatio(float maxAspectRatio) { + if (info.resizeMode == RESIZE_MODE_RESIZEABLE + || info.resizeMode == RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION) { + // Resizeable activities can be put in any aspect ratio. + return; + } + + if (maxAspectRatio < 1.0f && maxAspectRatio != 0) { + // Ignore any value lesser than 1.0. + return; + } + + info.setMaxAspectRatio(maxAspectRatio); + mHasMaxAspectRatio = true; + } + + private void setMinAspectRatio(float minAspectRatio) { + if (info.resizeMode == RESIZE_MODE_RESIZEABLE + || info.resizeMode == RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION) { + // Resizeable activities can be put in any aspect ratio. + return; + } + + if (minAspectRatio < 1.0f && minAspectRatio != 0) { + // Ignore any value lesser than 1.0. + return; + } + + info.setMinAspectRatio(minAspectRatio); + mHasMinAspectRatio = true; + } + + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("Activity{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(' '); + appendComponentShortName(sb); + sb.append('}'); + return sb.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(info, flags | Parcelable.PARCELABLE_ELIDE_DUPLICATES); + dest.writeBoolean(mHasMaxAspectRatio); + dest.writeBoolean(mHasMinAspectRatio); + } + + private Activity(Parcel in) { + super(in); + info = in.readParcelable(Object.class.getClassLoader(), android.content.pm.ActivityInfo.class); + mHasMaxAspectRatio = in.readBoolean(); + mHasMinAspectRatio = in.readBoolean(); + + for (ActivityIntentInfo aii : intents) { + aii.activity = this; + order = Math.max(aii.getOrder(), order); + } + + if (info.permission != null) { + info.permission = info.permission.intern(); + } + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Activity createFromParcel(Parcel in) { + return new Activity(in); + } + + public Activity[] newArray(int size) { + return new Activity[size]; + } + }; + } + + @UnsupportedAppUsage + public static final ActivityInfo generateActivityInfo(Activity a, int flags, + FrameworkPackageUserState state, int userId) { + return generateActivityInfo(a, flags, state, userId, null); + } + + private static ActivityInfo generateActivityInfo(Activity a, int flags, + FrameworkPackageUserState state, int userId, ApplicationInfo applicationInfo) { + if (a == null) return null; + if (!checkUseInstalledOrHidden(flags, state, a.owner.applicationInfo)) { + return null; + } + if (!copyNeeded(flags, a.owner, state, a.metaData, userId)) { + updateApplicationInfo(a.info.applicationInfo, flags, state); + return a.info; + } + // Make shallow copies so we can store the metadata safely + ActivityInfo ai = new ActivityInfo(a.info); + ai.metaData = a.metaData; + + if (applicationInfo == null) { + applicationInfo = generateApplicationInfo(a.owner, flags, state, userId); + } + ai.applicationInfo = applicationInfo; + + return ai; + } + + public static final ActivityInfo generateActivityInfo(ActivityInfo ai, int flags, + FrameworkPackageUserState state, int userId) { + if (ai == null) return null; + if (!checkUseInstalledOrHidden(flags, state, ai.applicationInfo)) { + return null; + } + // This is only used to return the ResolverActivity; we will just always + // make a copy. + ai = new ActivityInfo(ai); + ai.applicationInfo = generateApplicationInfo(ai.applicationInfo, flags, state, userId); + return ai; + } + + public final static class Service extends Component implements Parcelable { + @UnsupportedAppUsage + public final ServiceInfo info; + + public Service(final ParseComponentArgs args, final ServiceInfo _info) { + super(args, _info); + info = _info; + info.applicationInfo = args.owner.applicationInfo; + } + + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("Service{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(' '); + appendComponentShortName(sb); + sb.append('}'); + return sb.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(info, flags | Parcelable.PARCELABLE_ELIDE_DUPLICATES); + } + + private Service(Parcel in) { + super(in); + info = in.readParcelable(Object.class.getClassLoader(), android.content.pm.ServiceInfo.class); + + for (ServiceIntentInfo aii : intents) { + aii.service = this; + order = Math.max(aii.getOrder(), order); + } + + if (info.permission != null) { + info.permission = info.permission.intern(); + } + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Service createFromParcel(Parcel in) { + return new Service(in); + } + + public Service[] newArray(int size) { + return new Service[size]; + } + }; + } + + @UnsupportedAppUsage + public static final ServiceInfo generateServiceInfo(Service s, int flags, + FrameworkPackageUserState state, int userId) { + return generateServiceInfo(s, flags, state, userId, null); + } + + private static ServiceInfo generateServiceInfo(Service s, int flags, + FrameworkPackageUserState state, int userId, ApplicationInfo applicationInfo) { + if (s == null) return null; + if (!checkUseInstalledOrHidden(flags, state, s.owner.applicationInfo)) { + return null; + } + if (!copyNeeded(flags, s.owner, state, s.metaData, userId)) { + updateApplicationInfo(s.info.applicationInfo, flags, state); + return s.info; + } + // Make shallow copies so we can store the metadata safely + ServiceInfo si = new ServiceInfo(s.info); + si.metaData = s.metaData; + + if (applicationInfo == null) { + applicationInfo = generateApplicationInfo(s.owner, flags, state, userId); + } + si.applicationInfo = applicationInfo; + + return si; + } + + public final static class Provider extends Component implements Parcelable { + @UnsupportedAppUsage + public final ProviderInfo info; + @UnsupportedAppUsage + public boolean syncable; + + public Provider(final ParseComponentArgs args, final ProviderInfo _info) { + super(args, _info); + info = _info; + info.applicationInfo = args.owner.applicationInfo; + syncable = false; + } + + @UnsupportedAppUsage + public Provider(Provider existingProvider) { + super(existingProvider); + this.info = existingProvider.info; + this.syncable = existingProvider.syncable; + } + + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("Provider{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(' '); + appendComponentShortName(sb); + sb.append('}'); + return sb.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(info, flags | Parcelable.PARCELABLE_ELIDE_DUPLICATES); + dest.writeInt((syncable) ? 1 : 0); + } + + private Provider(Parcel in) { + super(in); + info = in.readParcelable(Object.class.getClassLoader(), android.content.pm.ProviderInfo.class); + syncable = (in.readInt() == 1); + + for (ProviderIntentInfo aii : intents) { + aii.provider = this; + } + + if (info.readPermission != null) { + info.readPermission = info.readPermission.intern(); + } + + if (info.writePermission != null) { + info.writePermission = info.writePermission.intern(); + } + + if (info.authority != null) { + info.authority = info.authority.intern(); + } + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Provider createFromParcel(Parcel in) { + return new Provider(in); + } + + public Provider[] newArray(int size) { + return new Provider[size]; + } + }; + } + + @UnsupportedAppUsage + public static final ProviderInfo generateProviderInfo(Provider p, int flags, + FrameworkPackageUserState state, int userId) { + return generateProviderInfo(p, flags, state, userId, null); + } + + private static ProviderInfo generateProviderInfo(Provider p, int flags, + FrameworkPackageUserState state, int userId, ApplicationInfo applicationInfo) { + if (p == null) return null; + if (!checkUseInstalledOrHidden(flags, state, p.owner.applicationInfo)) { + return null; + } + if (!copyNeeded(flags, p.owner, state, p.metaData, userId) + && ((flags & PackageManager.GET_URI_PERMISSION_PATTERNS) != 0 + || p.info.uriPermissionPatterns == null)) { + updateApplicationInfo(p.info.applicationInfo, flags, state); + return p.info; + } + // Make shallow copies so we can store the metadata safely + ProviderInfo pi = new ProviderInfo(p.info); + pi.metaData = p.metaData; + if ((flags & PackageManager.GET_URI_PERMISSION_PATTERNS) == 0) { + pi.uriPermissionPatterns = null; + } + + if (applicationInfo == null) { + applicationInfo = generateApplicationInfo(p.owner, flags, state, userId); + } + pi.applicationInfo = applicationInfo; + + return pi; + } + + public final static class Instrumentation extends Component implements + Parcelable { + @UnsupportedAppUsage + public final InstrumentationInfo info; + + public Instrumentation(final ParsePackageItemArgs args, final InstrumentationInfo _info) { + super(args, _info); + info = _info; + } + + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("Instrumentation{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(' '); + appendComponentShortName(sb); + sb.append('}'); + return sb.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeParcelable(info, flags); + } + + private Instrumentation(Parcel in) { + super(in); + info = in.readParcelable(Object.class.getClassLoader(), android.content.pm.InstrumentationInfo.class); + + if (info.targetPackage != null) { + info.targetPackage = info.targetPackage.intern(); + } + + if (info.targetProcesses != null) { + info.targetProcesses = info.targetProcesses.intern(); + } + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Instrumentation createFromParcel(Parcel in) { + return new Instrumentation(in); + } + + public Instrumentation[] newArray(int size) { + return new Instrumentation[size]; + } + }; + } + + @UnsupportedAppUsage + public static final InstrumentationInfo generateInstrumentationInfo( + Instrumentation i, int flags) { + if (i == null) return null; + if ((flags&PackageManager.GET_META_DATA) == 0) { + return i.info; + } + InstrumentationInfo ii = new InstrumentationInfo(i.info); + ii.metaData = i.metaData; + return ii; + } + + public static abstract class IntentInfo extends IntentFilter { + @UnsupportedAppUsage + public boolean hasDefault; + @UnsupportedAppUsage + public int labelRes; + @UnsupportedAppUsage + public CharSequence nonLocalizedLabel; + @UnsupportedAppUsage + public int icon; + @UnsupportedAppUsage + public int logo; + @UnsupportedAppUsage + public int banner; + public int preferred; + + @UnsupportedAppUsage + protected IntentInfo() { + } + + protected IntentInfo(Parcel dest) { + super(dest); + hasDefault = (dest.readInt() == 1); + labelRes = dest.readInt(); + nonLocalizedLabel = dest.readCharSequence(); + icon = dest.readInt(); + logo = dest.readInt(); + banner = dest.readInt(); + preferred = dest.readInt(); + } + + + public void writeIntentInfoToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(hasDefault ? 1 : 0); + dest.writeInt(labelRes); + dest.writeCharSequence(nonLocalizedLabel); + dest.writeInt(icon); + dest.writeInt(logo); + dest.writeInt(banner); + dest.writeInt(preferred); + } + } + + public final static class ActivityIntentInfo extends IntentInfo { + @UnsupportedAppUsage + public Activity activity; + + public ActivityIntentInfo(Activity _activity) { + activity = _activity; + } + + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("ActivityIntentInfo{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(' '); + activity.appendComponentShortName(sb); + sb.append('}'); + return sb.toString(); + } + + public ActivityIntentInfo(Parcel in) { + super(in); + } + } + + public final static class ServiceIntentInfo extends IntentInfo { + @UnsupportedAppUsage + public Service service; + + public ServiceIntentInfo(Service _service) { + service = _service; + } + + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("ServiceIntentInfo{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(' '); + service.appendComponentShortName(sb); + sb.append('}'); + return sb.toString(); + } + + public ServiceIntentInfo(Parcel in) { + super(in); + } + } + + public static final class ProviderIntentInfo extends IntentInfo { + @UnsupportedAppUsage + public Provider provider; + + public ProviderIntentInfo(Provider provider) { + this.provider = provider; + } + + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("ProviderIntentInfo{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(' '); + provider.appendComponentShortName(sb); + sb.append('}'); + return sb.toString(); + } + + public ProviderIntentInfo(Parcel in) { + super(in); + } + } + + /** + * @hide + */ + @UnsupportedAppUsage + public static void setCompatibilityModeEnabled(boolean compatibilityModeEnabled) { + sCompatibilityModeEnabled = compatibilityModeEnabled; + } + + /** + * @hide + */ + public static void readConfigUseRoundIcon(Resources r) { + if (r != null) { + sUseRoundIcon = r.getBoolean(com.android.internal.R.bool.config_useRoundIcon); + return; + } + + ApplicationInfo androidAppInfo; + try { + androidAppInfo = ActivityThread.getPackageManager().getApplicationInfo( + "android", 0 /* flags */, + UserHandle.myUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + Resources systemResources = Resources.getSystem(); + + // Create in-flight as this overlayable resource is only used when config changes + Resources overlayableRes = ResourcesManager.getInstance().getResources(null, + null, + null, + androidAppInfo.resourceDirs, + androidAppInfo.overlayPaths, + androidAppInfo.sharedLibraryFiles, + null, + null, + systemResources.getCompatibilityInfo(), + systemResources.getClassLoader(), + null); + + sUseRoundIcon = overlayableRes.getBoolean(com.android.internal.R.bool.config_useRoundIcon); + } + + public static class PackageParserException extends Exception { + public final int error; + + public PackageParserException(int error, String detailMessage) { + super(detailMessage); + this.error = error; + } + + public PackageParserException(int error, String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + this.error = error; + } + } + + // Duplicate the SplitAsset related classes with PackageParser.Package/ApkLite here, and + // change the original one using new Package/ApkLite. The propose is that we don't want to + // have two branches of methods in SplitAsset related classes so we can keep real classes + // clean and move all the legacy code to one place. + + /** + * Simple interface for loading base Assets and Splits. Used by PackageParser when parsing + * split APKs. + * + * @hide + * @deprecated Do not use. New changes should use + * {@link android.content.pm.split.SplitAssetLoader} instead. + */ + @Deprecated + private interface SplitAssetLoader extends AutoCloseable { + AssetManager getBaseAssetManager() throws PackageParserException; + AssetManager getSplitAssetManager(int splitIdx) throws PackageParserException; + + ApkAssets getBaseApkAssets(); + } + + /** + * A helper class that implements the dependency tree traversal for splits. Callbacks + * are implemented by subclasses to notify whether a split has already been constructed + * and is cached, and to actually create the split requested. + * + * This helper is meant to be subclassed so as to reduce the number of allocations + * needed to make use of it. + * + * All inputs and outputs are assumed to be indices into an array of splits. + * + * @hide + * @deprecated Do not use. New changes should use + * {@link android.content.pm.split.SplitDependencyLoader} instead. + */ + @Deprecated + private abstract static class SplitDependencyLoader { + private final @NonNull SparseArray mDependencies; + + /** + * Construct a new SplitDependencyLoader. Meant to be called from the + * subclass constructor. + * @param dependencies The dependency tree of splits. + */ + protected SplitDependencyLoader(@NonNull SparseArray dependencies) { + mDependencies = dependencies; + } + + /** + * Traverses the dependency tree and constructs any splits that are not already + * cached. This routine short-circuits and skips the creation of splits closer to the + * root if they are cached, as reported by the subclass implementation of + * {@link #isSplitCached(int)}. The construction of splits is delegated to the subclass + * implementation of {@link #constructSplit(int, int[], int)}. + * @param splitIdx The index of the split to load. 0 represents the base Application. + */ + protected void loadDependenciesForSplit(@IntRange(from = 0) int splitIdx) throws E { + // Quick check before any allocations are done. + if (isSplitCached(splitIdx)) { + return; + } + + // Special case the base, since it has no dependencies. + if (splitIdx == 0) { + final int[] configSplitIndices = collectConfigSplitIndices(0); + constructSplit(0, configSplitIndices, -1); + return; + } + + // Build up the dependency hierarchy. + final IntArray linearDependencies = new IntArray(); + linearDependencies.add(splitIdx); + + // Collect all the dependencies that need to be constructed. + // They will be listed from leaf to root. + while (true) { + // Only follow the first index into the array. The others are config splits and + // get loaded with the split. + final int[] deps = mDependencies.get(splitIdx); + if (deps != null && deps.length > 0) { + splitIdx = deps[0]; + } else { + splitIdx = -1; + } + + if (splitIdx < 0 || isSplitCached(splitIdx)) { + break; + } + + linearDependencies.add(splitIdx); + } + + // Visit each index, from right to left (root to leaf). + int parentIdx = splitIdx; + for (int i = linearDependencies.size() - 1; i >= 0; i--) { + final int idx = linearDependencies.get(i); + final int[] configSplitIndices = collectConfigSplitIndices(idx); + constructSplit(idx, configSplitIndices, parentIdx); + parentIdx = idx; + } + } + + private @NonNull int[] collectConfigSplitIndices(int splitIdx) { + // The config splits appear after the first element. + final int[] deps = mDependencies.get(splitIdx); + if (deps == null || deps.length <= 1) { + return EmptyArray.INT; + } + return Arrays.copyOfRange(deps, 1, deps.length); + } + + /** + * Subclass to report whether the split at `splitIdx` is cached and need not be constructed. + * It is assumed that if `splitIdx` is cached, any parent of `splitIdx` is also cached. + * @param splitIdx The index of the split to check for in the cache. + * @return true if the split is cached and does not need to be constructed. + */ + protected abstract boolean isSplitCached(@IntRange(from = 0) int splitIdx); + + /** + * Subclass to construct a split at index `splitIdx` with parent split `parentSplitIdx`. + * The result is expected to be cached by the subclass in its own structures. + * @param splitIdx The index of the split to construct. 0 represents the base Application. + * @param configSplitIndices The array of configuration splits to load along with this + * split. May be empty (length == 0) but never null. + * @param parentSplitIdx The index of the parent split. -1 if there is no parent. + * @throws E Subclass defined exception representing failure to construct a split. + */ + protected abstract void constructSplit(@IntRange(from = 0) int splitIdx, + @NonNull @IntRange(from = 1) int[] configSplitIndices, + @IntRange(from = -1) int parentSplitIdx) throws E; + + public static class IllegalDependencyException extends Exception { + private IllegalDependencyException(String message) { + super(message); + } + } + + private static int[] append(int[] src, int elem) { + if (src == null) { + return new int[] { elem }; + } + int[] dst = Arrays.copyOf(src, src.length + 1); + dst[src.length] = elem; + return dst; + } + + public static @NonNull SparseArray createDependenciesFromPackage( + PackageLite pkg) + throws SplitDependencyLoader.IllegalDependencyException { + // The data structure that holds the dependencies. In PackageParser, splits are stored + // in their own array, separate from the base. We treat all paths as equals, so + // we need to insert the base as index 0, and shift all other splits. + final SparseArray splitDependencies = new SparseArray<>(); + + // The base depends on nothing. + splitDependencies.put(0, new int[] {-1}); + + // First write out the dependencies. These must appear first in the + // array of ints, as is convention in this class. + for (int splitIdx = 0; splitIdx < pkg.splitNames.length; splitIdx++) { + if (!pkg.isFeatureSplits[splitIdx]) { + // Non-feature splits don't have dependencies. + continue; + } + + // Implicit dependency on the base. + final int targetIdx; + final String splitDependency = pkg.usesSplitNames[splitIdx]; + if (splitDependency != null) { + final int depIdx = Arrays.binarySearch(pkg.splitNames, splitDependency); + if (depIdx < 0) { + throw new SplitDependencyLoader.IllegalDependencyException( + "Split '" + pkg.splitNames[splitIdx] + "' requires split '" + + splitDependency + "', which is missing."); + } + targetIdx = depIdx + 1; + } else { + // Implicitly depend on the base. + targetIdx = 0; + } + splitDependencies.put(splitIdx + 1, new int[] {targetIdx}); + } + + // Write out the configForSplit reverse-dependencies. These appear after the + // dependencies and are considered leaves. + // + // At this point, all splits in splitDependencies have the first element in their + // array set. + for (int splitIdx = 0, size = pkg.splitNames.length; splitIdx < size; splitIdx++) { + if (pkg.isFeatureSplits[splitIdx]) { + // Feature splits are not configForSplits. + continue; + } + + // Implicit feature for the base. + final int targetSplitIdx; + final String configForSplit = pkg.configForSplit[splitIdx]; + if (configForSplit != null) { + final int depIdx = Arrays.binarySearch(pkg.splitNames, configForSplit); + if (depIdx < 0) { + throw new SplitDependencyLoader.IllegalDependencyException( + "Split '" + pkg.splitNames[splitIdx] + "' targets split '" + + configForSplit + "', which is missing."); + } + + if (!pkg.isFeatureSplits[depIdx]) { + throw new SplitDependencyLoader.IllegalDependencyException( + "Split '" + pkg.splitNames[splitIdx] + "' declares itself as " + + "configuration split for a non-feature split '" + + pkg.splitNames[depIdx] + "'"); + } + targetSplitIdx = depIdx + 1; + } else { + targetSplitIdx = 0; + } + splitDependencies.put(targetSplitIdx, + append(splitDependencies.get(targetSplitIdx), splitIdx + 1)); + } + + // Verify that there are no cycles. + final BitSet bitset = new BitSet(); + for (int i = 0, size = splitDependencies.size(); i < size; i++) { + int splitIdx = splitDependencies.keyAt(i); + + bitset.clear(); + while (splitIdx != -1) { + // Check if this split has been visited yet. + if (bitset.get(splitIdx)) { + throw new SplitDependencyLoader.IllegalDependencyException( + "Cycle detected in split dependencies."); + } + + // Mark the split so that if we visit it again, we no there is a cycle. + bitset.set(splitIdx); + + // Follow the first dependency only, the others are leaves by definition. + final int[] deps = splitDependencies.get(splitIdx); + splitIdx = deps != null ? deps[0] : -1; + } + } + return splitDependencies; + } + } + + /** + * Loads the base and split APKs into a single AssetManager. + * @hide + * @deprecated Do not use. New changes should use + * {@link android.content.pm.split.DefaultSplitAssetLoader} instead. + */ + @Deprecated + private static class DefaultSplitAssetLoader implements SplitAssetLoader { + private final String mBaseCodePath; + private final String[] mSplitCodePaths; + private final @ParseFlags int mFlags; + private AssetManager mCachedAssetManager; + + private ApkAssets mBaseApkAssets; + + DefaultSplitAssetLoader(PackageLite pkg, @ParseFlags int flags) { + mBaseCodePath = pkg.baseCodePath; + mSplitCodePaths = pkg.splitCodePaths; + mFlags = flags; + } + + private static ApkAssets loadApkAssets(String path, @ParseFlags int flags) + throws PackageParserException { + if ((flags & PackageParser.PARSE_MUST_BE_APK) != 0 && !PackageParser.isApkPath(path)) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK, + "Invalid package file: " + path); + } + + try { + return ApkAssets.loadFromPath(path); + } catch (IOException e) { + throw new PackageParserException(INSTALL_FAILED_INVALID_APK, + "Failed to load APK at path " + path, e); + } + } + + @Override + public AssetManager getBaseAssetManager() throws PackageParserException { + if (mCachedAssetManager != null) { + return mCachedAssetManager; + } + + ApkAssets[] apkAssets = new ApkAssets[(mSplitCodePaths != null + ? mSplitCodePaths.length : 0) + 1]; + + mBaseApkAssets = loadApkAssets(mBaseCodePath, mFlags); + + // Load the base. + int splitIdx = 0; + apkAssets[splitIdx++] = mBaseApkAssets; + + // Load any splits. + if (!ArrayUtils.isEmpty(mSplitCodePaths)) { + for (String apkPath : mSplitCodePaths) { + apkAssets[splitIdx++] = loadApkAssets(apkPath, mFlags); + } + } + + AssetManager assets = new AssetManager(); + assets.setConfiguration(0, 0, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); + assets.setApkAssets(apkAssets, false /*invalidateCaches*/); + + mCachedAssetManager = assets; + return mCachedAssetManager; + } + + @Override + public AssetManager getSplitAssetManager(int splitIdx) throws PackageParserException { + return getBaseAssetManager(); + } + + @Override + public void close() throws Exception { + IoUtils.closeQuietly(mCachedAssetManager); + } + + @Override + public ApkAssets getBaseApkAssets() { + return mBaseApkAssets; + } + } + + /** + * Loads AssetManagers for splits and their dependencies. This SplitAssetLoader implementation + * is to be used when an application opts-in to isolated split loading. + * @hide + * @deprecated Do not use. New changes should use + * {@link android.content.pm.split.SplitAssetDependencyLoader} instead. + */ + @Deprecated + private static class SplitAssetDependencyLoader extends + SplitDependencyLoader implements SplitAssetLoader { + private final String[] mSplitPaths; + private final @ParseFlags int mFlags; + private final ApkAssets[][] mCachedSplitApks; + private final AssetManager[] mCachedAssetManagers; + + SplitAssetDependencyLoader(PackageLite pkg, + SparseArray dependencies, @ParseFlags int flags) { + super(dependencies); + + // The base is inserted into index 0, so we need to shift all the splits by 1. + mSplitPaths = new String[pkg.splitCodePaths.length + 1]; + mSplitPaths[0] = pkg.baseCodePath; + System.arraycopy(pkg.splitCodePaths, 0, mSplitPaths, 1, pkg.splitCodePaths.length); + + mFlags = flags; + mCachedSplitApks = new ApkAssets[mSplitPaths.length][]; + mCachedAssetManagers = new AssetManager[mSplitPaths.length]; + } + + @Override + protected boolean isSplitCached(int splitIdx) { + return mCachedAssetManagers[splitIdx] != null; + } + + private static ApkAssets loadApkAssets(String path, @ParseFlags int flags) + throws PackageParserException { + if ((flags & PackageParser.PARSE_MUST_BE_APK) != 0 && !PackageParser.isApkPath(path)) { + throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK, + "Invalid package file: " + path); + } + + try { + return ApkAssets.loadFromPath(path); + } catch (IOException e) { + throw new PackageParserException(PackageManager.INSTALL_FAILED_INVALID_APK, + "Failed to load APK at path " + path, e); + } + } + + private static AssetManager createAssetManagerWithAssets(ApkAssets[] apkAssets) { + final AssetManager assets = new AssetManager(); + assets.setConfiguration(0, 0, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); + assets.setApkAssets(apkAssets, false /*invalidateCaches*/); + return assets; + } + + @Override + protected void constructSplit(int splitIdx, @NonNull int[] configSplitIndices, + int parentSplitIdx) throws PackageParserException { + final ArrayList assets = new ArrayList<>(); + + // Include parent ApkAssets. + if (parentSplitIdx >= 0) { + Collections.addAll(assets, mCachedSplitApks[parentSplitIdx]); + } + + // Include this ApkAssets. + assets.add(loadApkAssets(mSplitPaths[splitIdx], mFlags)); + + // Load and include all config splits for this feature. + for (int configSplitIdx : configSplitIndices) { + assets.add(loadApkAssets(mSplitPaths[configSplitIdx], mFlags)); + } + + // Cache the results. + mCachedSplitApks[splitIdx] = assets.toArray(new ApkAssets[assets.size()]); + mCachedAssetManagers[splitIdx] = createAssetManagerWithAssets( + mCachedSplitApks[splitIdx]); + } + + @Override + public AssetManager getBaseAssetManager() throws PackageParserException { + loadDependenciesForSplit(0); + return mCachedAssetManagers[0]; + } + + @Override + public AssetManager getSplitAssetManager(int idx) throws PackageParserException { + // Since we insert the base at position 0, and PackageParser keeps splits separate from + // the base, we need to adjust the index. + loadDependenciesForSplit(idx + 1); + return mCachedAssetManagers[idx + 1]; + } + + @Override + public void close() throws Exception { + for (AssetManager assets : mCachedAssetManagers) { + IoUtils.closeQuietly(assets); + } + } + + @Override + public ApkAssets getBaseApkAssets() { + return mCachedSplitApks[0][0]; + } + } + + + + public static boolean isMatch(@NonNull FrameworkPackageUserState state, + ComponentInfo componentInfo, long flags) { + return isMatch(state, componentInfo.applicationInfo.isSystemApp(), + componentInfo.applicationInfo.enabled, componentInfo.enabled, + componentInfo.directBootAware, componentInfo.name, flags); + } + + public static boolean isMatch(@NonNull FrameworkPackageUserState state, boolean isSystem, + boolean isPackageEnabled, ComponentInfo component, long flags) { + return isMatch(state, isSystem, isPackageEnabled, component.isEnabled(), + component.directBootAware, component.name, flags); + } + + /** + * Test if the given component is considered installed, enabled and a match for the given + * flags. + * + *

+ * Expects at least one of {@link PackageManager#MATCH_DIRECT_BOOT_AWARE} and {@link + * PackageManager#MATCH_DIRECT_BOOT_UNAWARE} are specified in {@code flags}. + *

+ */ + public static boolean isMatch(@NonNull FrameworkPackageUserState state, boolean isSystem, + boolean isPackageEnabled, boolean isComponentEnabled, + boolean isComponentDirectBootAware, String componentName, long flags) { + final boolean matchUninstalled = (flags & PackageManager.MATCH_KNOWN_PACKAGES) != 0; + if (!isAvailable(state, flags) && !(isSystem && matchUninstalled)) { + return reportIfDebug(false, flags); + } + + if (!isEnabled(state, isPackageEnabled, isComponentEnabled, componentName, flags)) { + return reportIfDebug(false, flags); + } + + if ((flags & PackageManager.MATCH_SYSTEM_ONLY) != 0) { + if (!isSystem) { + return reportIfDebug(false, flags); + } + } + + final boolean matchesUnaware = ((flags & PackageManager.MATCH_DIRECT_BOOT_UNAWARE) != 0) + && !isComponentDirectBootAware; + final boolean matchesAware = ((flags & PackageManager.MATCH_DIRECT_BOOT_AWARE) != 0) + && isComponentDirectBootAware; + return reportIfDebug(matchesUnaware || matchesAware, flags); + } + + public static boolean isAvailable(@NonNull FrameworkPackageUserState state, long flags) { + // True if it is installed for this user and it is not hidden. If it is hidden, + // still return true if the caller requested MATCH_UNINSTALLED_PACKAGES + final boolean matchAnyUser = (flags & PackageManager.MATCH_ANY_USER) != 0; + final boolean matchUninstalled = (flags & PackageManager.MATCH_UNINSTALLED_PACKAGES) != 0; + return matchAnyUser + || (state.isInstalled() + && (!state.isHidden() || matchUninstalled)); + } + + public static boolean reportIfDebug(boolean result, long flags) { + if (DEBUG_PARSER && !result) { + Slog.i(TAG, "No match!; flags: " + + DebugUtils.flagsToString(PackageManager.class, "MATCH_", flags) + " " + + Debug.getCaller()); + } + return result; + } + + public static boolean isEnabled(@NonNull FrameworkPackageUserState state, ComponentInfo componentInfo, + long flags) { + return isEnabled(state, componentInfo.applicationInfo.enabled, componentInfo.enabled, + componentInfo.name, flags); + } + + public static boolean isEnabled(@NonNull FrameworkPackageUserState state, boolean isPackageEnabled, + ComponentInfo parsedComponent, long flags) { + return isEnabled(state, isPackageEnabled, parsedComponent.isEnabled(), + parsedComponent.name, flags); + } + + /** + * Test if the given component is considered enabled. + */ + public static boolean isEnabled(@NonNull FrameworkPackageUserState state, + boolean isPackageEnabled, boolean isComponentEnabled, String componentName, + long flags) { + if ((flags & MATCH_DISABLED_COMPONENTS) != 0) { + return true; + } + + // First check if the overall package is disabled; if the package is + // enabled then fall through to check specific component + switch (state.getEnabledState()) { + case PackageManager.COMPONENT_ENABLED_STATE_DISABLED: + case PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER: + return false; + case PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED: + if ((flags & MATCH_DISABLED_UNTIL_USED_COMPONENTS) == 0) { + return false; + } + // fallthrough + case PackageManager.COMPONENT_ENABLED_STATE_DEFAULT: + if (!isPackageEnabled) { + return false; + } + // fallthrough + case PackageManager.COMPONENT_ENABLED_STATE_ENABLED: + break; + } + + // Check if component has explicit state before falling through to + // the manifest default + if (state.isComponentEnabled(componentName)) { + return true; + } else if (state.isComponentDisabled(componentName)) { + return false; + } + + return isComponentEnabled; + } + + /** + * Writes the keyset mapping to the provided package. {@code null} mappings are permitted. + */ + public static void writeKeySetMapping(@NonNull Parcel dest, + @NonNull Map> keySetMapping) { + if (keySetMapping == null) { + dest.writeInt(-1); + return; + } + + final int N = keySetMapping.size(); + dest.writeInt(N); + + for (String key : keySetMapping.keySet()) { + dest.writeString(key); + ArraySet keys = keySetMapping.get(key); + if (keys == null) { + dest.writeInt(-1); + continue; + } + + final int M = keys.size(); + dest.writeInt(M); + for (int j = 0; j < M; j++) { + dest.writeSerializable(keys.valueAt(j)); + } + } + } + + /** + * Reads a keyset mapping from the given parcel at the given data position. May return + * {@code null} if the serialized mapping was {@code null}. + */ + @NonNull + public static ArrayMap> readKeySetMapping(@NonNull Parcel in) { + final int N = in.readInt(); + if (N == -1) { + return null; + } + + ArrayMap> keySetMapping = new ArrayMap<>(); + for (int i = 0; i < N; ++i) { + String key = in.readString(); + final int M = in.readInt(); + if (M == -1) { + keySetMapping.put(key, null); + continue; + } + + ArraySet keys = new ArraySet<>(M); + for (int j = 0; j < M; ++j) { + PublicKey pk = + in.readSerializable(PublicKey.class.getClassLoader(), PublicKey.class); + keys.add(pk); + } + + keySetMapping.put(key, keys); + } + + return keySetMapping; + } + + public static String getSeinfoUser(FrameworkPackageUserState userState) { + if (userState.isInstantApp()) { + return ":ephemeralapp:complete"; + } + return ":complete"; + } +} diff --git a/aosp/frameworks/base/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java b/aosp/frameworks/base/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..b60fc919b8def4511d2a49f0898998c32ecd6699 --- /dev/null +++ b/aosp/frameworks/base/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2022 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.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES; + +import android.annotation.CheckResult; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.content.pm.SigningDetails; +import android.content.pm.parsing.result.ParseInput; +import android.content.pm.parsing.result.ParseResult; +import android.os.Build; +import android.os.FileUtils; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Base64; +import android.util.Slog; +import android.util.apk.ApkSignatureVerifier; + +import com.android.internal.util.ArrayUtils; +import com.android.modules.utils.build.UnboundedSdkLevel; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; + +/** @hide */ +public class FrameworkParsingPackageUtils { + + private static final String TAG = "FrameworkParsingPackageUtils"; + + /** + * For those names would be used as a part of the file name. Limits size to 223 and reserves 32 + * for the OS. + */ + private static final int MAX_FILE_NAME_SIZE = 223; + + public static final int PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY = 1 << 7; + public static final int PARSE_APK_IN_APEX = 1 << 9; + + /** + * Check if the given name is valid. + * + * @param name The name to check. + * @param requireSeparator {@code true} if the name requires containing a separator at least. + * @param requireFilename {@code true} to apply file name validation to the given name. It also + * limits length of the name to the {@link #MAX_FILE_NAME_SIZE}. + * @return Success if it's valid. + */ + public static String validateName(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 "bad character '" + c + "'"; + } + if (requireFilename) { + if (!FileUtils.isValidExtFilename(name)) { + return "Invalid filename"; + } else if (N > MAX_FILE_NAME_SIZE) { + return "the length of the name is greater than " + MAX_FILE_NAME_SIZE; + } + } + return hasSep || !requireSeparator ? null : "must have at least one '.' separator"; + } + + /** + * @see #validateName(String, boolean, boolean) + */ + public static ParseResult validateName(ParseInput input, String name, boolean requireSeparator, + boolean requireFilename) { + final String errorMessage = validateName(name, requireSeparator, requireFilename); + if (errorMessage != null) { + return input.error(errorMessage); + } + return input.success(null); + } + + /** + * @return {@link PublicKey} of a given encoded public key. + */ + public static PublicKey parsePublicKey(final String encodedPublicKey) { + if (encodedPublicKey == null) { + Slog.w(TAG, "Could not parse null public key"); + return null; + } + + try { + return parsePublicKey(Base64.decode(encodedPublicKey, Base64.DEFAULT)); + } catch (IllegalArgumentException e) { + Slog.w(TAG, "Could not parse verifier public key; invalid Base64"); + return null; + } + } + + /** + * @return {@link PublicKey} of the given byte array of a public key. + */ + public static PublicKey parsePublicKey(final byte[] publicKey) { + if (publicKey == null) { + Slog.w(TAG, "Could not parse null public key"); + return null; + } + + final EncodedKeySpec keySpec; + try { + keySpec = new X509EncodedKeySpec(publicKey); + } catch (IllegalArgumentException e) { + Slog.w(TAG, "Could not parse verifier public key; invalid Base64"); + return null; + } + + /* First try the key as an RSA key. */ + try { + final KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + Slog.wtf(TAG, "Could not parse public key: RSA KeyFactory not included in build"); + } catch (InvalidKeySpecException e) { + // Not a RSA public key. + } + + /* Now try it as a ECDSA key. */ + try { + final KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + Slog.wtf(TAG, "Could not parse public key: EC KeyFactory not included in build"); + } catch (InvalidKeySpecException e) { + // Not a ECDSA public key. + } + + /* Now try it as a DSA key. */ + try { + final KeyFactory keyFactory = KeyFactory.getInstance("DSA"); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + Slog.wtf(TAG, "Could not parse public key: DSA KeyFactory not included in build"); + } catch (InvalidKeySpecException e) { + // Not a DSA public key. + } + + /* Not a supported key type */ + return null; + } + + /** + * Returns {@code true} if both the property name and value are empty or if the given system + * property is set to the specified value. Properties can be one or more, and if properties are + * more than one, they must be separated by comma, and count of names and values must be equal, + * and also every given system property must be set to the corresponding value. + * In all other cases, returns {@code false} + */ + public static boolean checkRequiredSystemProperties(@Nullable String rawPropNames, + @Nullable String rawPropValues) { + if (TextUtils.isEmpty(rawPropNames) || TextUtils.isEmpty(rawPropValues)) { + if (!TextUtils.isEmpty(rawPropNames) || !TextUtils.isEmpty(rawPropValues)) { + // malformed condition - incomplete + Slog.w(TAG, "Disabling overlay - incomplete property :'" + rawPropNames + + "=" + rawPropValues + "' - require both requiredSystemPropertyName" + + " AND requiredSystemPropertyValue to be specified."); + return false; + } + // no valid condition set - so no exclusion criteria, overlay will be included. + return true; + } + + final String[] propNames = rawPropNames.split(","); + final String[] propValues = rawPropValues.split(","); + + if (propNames.length != propValues.length) { + Slog.w(TAG, "Disabling overlay - property :'" + rawPropNames + + "=" + rawPropValues + "' - require both requiredSystemPropertyName" + + " AND requiredSystemPropertyValue lists to have the same size."); + return false; + } + for (int i = 0; i < propNames.length; i++) { + // Check property value: make sure it is both set and equal to expected value + final String currValue = SystemProperties.get(propNames[i]); + if (!TextUtils.equals(currValue, propValues[i])) { + return false; + } + } + return true; + } + + @CheckResult + public static ParseResult getSigningDetails(ParseInput input, + String baseCodePath, boolean skipVerify, boolean isStaticSharedLibrary, + @NonNull SigningDetails existingSigningDetails, int targetSdk) { + int minSignatureScheme = ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk( + targetSdk); + if (isStaticSharedLibrary) { + // must use v2 signing scheme + minSignatureScheme = SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2; + } + final ParseResult verified; + if (skipVerify) { + // systemDir APKs are already trusted, save time by not verifying; since the + // signature is not verified and some system apps can have their V2+ signatures + // stripped allow pulling the certs from the jar signature. + verified = ApkSignatureVerifier.unsafeGetCertsWithoutVerification(input, baseCodePath, + SigningDetails.SignatureSchemeVersion.JAR, false); + } else { + verified = ApkSignatureVerifier.verify(input, baseCodePath, minSignatureScheme); + } + + if (verified.isError()) { + return input.error(verified); + } + + // 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, + verified.getResult())) { + return input.error(INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, + baseCodePath + " has mismatched certificates"); + } + + return input.success(existingSigningDetails); + } + } + + /** + * Computes the minSdkVersion to use at runtime. If the package is not compatible with this + * platform, populates {@code outError[0]} with an error message. + *

+ * If {@code minCode} is not specified, e.g. the value is {@code null}, then behavior varies + * based on the {@code platformSdkVersion}: + *

    + *
  • If the platform SDK version is greater than or equal to the + * {@code minVers}, returns the {@code mniVers} unmodified. + *
  • Otherwise, returns -1 to indicate that the package is not + * compatible with this platform. + *
+ *

+ * Otherwise, the behavior varies based on whether the current platform + * is a pre-release version, e.g. the {@code platformSdkCodenames} array + * has length > 0: + *

    + *
  • If this is a pre-release platform and the value specified by + * {@code targetCode} is contained within the array of allowed pre-release + * codenames, this method will return {@link Build.VERSION_CODES#CUR_DEVELOPMENT}. + *
  • If this is a released platform, this method will return -1 to + * indicate that the package is not compatible with this platform. + *
+ * + * @param minVers minSdkVersion number, if specified in the application manifest, + * or 1 otherwise + * @param minCode minSdkVersion code, if specified in the application manifest, or + * {@code null} otherwise + * @param platformSdkVersion platform SDK version number, typically Build.VERSION.SDK_INT + * @param platformSdkCodenames array of allowed prerelease SDK codenames for this platform + * @return the minSdkVersion to use at runtime if successful + */ + public static ParseResult 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."); + } + } + + /** + * Computes the targetSdkVersion to use at runtime. If the package is not compatible with this + * platform, populates {@code outError[0]} with an error message. + *

+ * If {@code targetCode} is not specified, e.g. the value is {@code null}, then the {@code + * targetVers} will be returned unmodified. + *

+ * When {@code allowUnknownCodenames} is false, the behavior varies based on whether the + * current platform is a pre-release version, e.g. the {@code platformSdkCodenames} array has + * length > 0: + *

    + *
  • If this is a pre-release platform and the value specified by + * {@code targetCode} is contained within the array of allowed pre-release + * codenames, this method will return {@link Build.VERSION_CODES#CUR_DEVELOPMENT}. + *
  • If this is a released platform, this method will return -1 to + * indicate that the package is not compatible with this platform. + *
+ *

+ * When {@code allowUnknownCodenames} is true, any codename that is not known (presumed to be + * a codename announced after the build of the current device) is allowed and this method will + * return {@link Build.VERSION_CODES#CUR_DEVELOPMENT}. + * + * @param targetVers targetSdkVersion number, if specified in the application + * manifest, or 0 otherwise + * @param targetCode targetSdkVersion code, if specified in the application manifest, + * or {@code null} otherwise + * @param platformSdkCodenames array of allowed pre-release SDK codenames for this platform + * @param allowUnknownCodenames allow unknown codenames, if true this method will accept unknown + * (presumed to be future) codenames + * @return the targetSdkVersion to use at runtime if successful + */ + public static ParseResult computeTargetSdkVersion(@IntRange(from = 0) int targetVers, + @Nullable String targetCode, @NonNull String[] platformSdkCodenames, + @NonNull ParseInput input, boolean allowUnknownCodenames) { + // If it's a release SDK, return the version number unmodified. + if (targetCode == null) { + return input.success(targetVers); + } + + try { + if (allowUnknownCodenames && UnboundedSdkLevel.isAtMost(targetCode)) { + return input.success(Build.VERSION_CODES.CUR_DEVELOPMENT); + } + } catch (IllegalArgumentException e) { + // isAtMost() throws it when encountering an older SDK codename + return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK, e.getMessage()); + } + + // 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."); + } + } + + /** + * Computes the maxSdkVersion. If the package is not compatible with this platform, populates + * {@code outError[0]} with an error message. + *

+ * {@code maxVers} is compared against {@code platformSdkVersion}. If {@code maxVers} is less + * than the {@code platformSdkVersion} then populates {@code outError[0]} with an error message. + * Otherwise, it returns {@code maxVers} unmodified. + * + * @param maxVers maxSdkVersion number, if specified in the application manifest, or {@code + * Integer.MAX_VALUE} otherwise + * @param platformSdkVersion platform SDK version number, typically Build.VERSION.SDK_INT + * @return the maxSdkVersion that was recognised or an error if the condition is not satisfied + */ + public static ParseResult computeMaxSdkVersion(@IntRange(from = 0) int maxVers, + @IntRange(from = 1) int platformSdkVersion, @NonNull ParseInput input) { + if (platformSdkVersion > maxVers) { + return input.error(PackageManager.INSTALL_FAILED_NEWER_SDK, + "Requires max SDK version " + maxVers + " but is " + + platformSdkVersion); + } else { + return input.success(maxVers); + } + } + + /** + * 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); + } +} diff --git a/aosp/frameworks/base/core/java/android/util/apk/ApkSignatureVerifier.java b/aosp/frameworks/base/core/java/android/util/apk/ApkSignatureVerifier.java new file mode 100644 index 0000000000000000000000000000000000000000..92762d63f853c0c9c3bf70fa7c1f0ead19023d9e --- /dev/null +++ b/aosp/frameworks/base/core/java/android/util/apk/ApkSignatureVerifier.java @@ -0,0 +1,595 @@ +/* + * Copyright (C) 2017 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.util.apk; + +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION; +import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; +import static android.util.apk.ApkSignatureSchemeV4Verifier.APK_SIGNATURE_SCHEME_DEFAULT; + +import android.content.pm.Signature; +import android.content.pm.SigningDetails; +import android.content.pm.SigningDetails.SignatureSchemeVersion; +import android.content.pm.parsing.ApkLiteParseUtils; +import android.content.pm.parsing.result.ParseInput; +import android.content.pm.parsing.result.ParseResult; +import android.os.Build; +import android.os.Trace; +import android.os.incremental.V4Signature; +import android.util.Pair; +import android.util.jar.StrictJarFile; + +import com.android.internal.util.ArrayUtils; + +import libcore.io.IoUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.security.DigestException; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.zip.ZipEntry; + +/** + * Facade class that takes care of the details of APK verification on + * behalf of ParsingPackageUtils. + * + * @hide for internal use only. + */ +public class ApkSignatureVerifier { + + private static final AtomicReference sBuffer = new AtomicReference<>(); + + /** + * Verifies the provided APK and returns the certificates associated with each signer. + */ + public static ParseResult verify(ParseInput input, String apkPath, + @SignatureSchemeVersion int minSignatureSchemeVersion) { + return verifySignatures(input, apkPath, minSignatureSchemeVersion, true /* verifyFull */, false); + } + + /** + * Returns the certificates associated with each signer for the given APK without verification. + * This method is dangerous and should not be used, unless the caller is absolutely certain the + * APK is trusted. + */ + public static ParseResult unsafeGetCertsWithoutVerification( + ParseInput input, String apkPath, int minSignatureSchemeVersion, boolean skipFindEntry) { + return verifySignatures(input, apkPath, minSignatureSchemeVersion, false /* verifyFull */, skipFindEntry); + } + + /** + * Verifies the provided APK using all allowed signing schemas. + * @return the certificates associated with each signer. + * @param verifyFull whether to verify all contents of this APK or just collect certificates. + */ + private static ParseResult verifySignatures(ParseInput input, String apkPath, + @SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull, boolean skipFindEntry) { + final ParseResult result = + verifySignaturesInternal(input, apkPath, minSignatureSchemeVersion, verifyFull, skipFindEntry); + if (result.isError()) { + return input.error(result); + } + return input.success(result.getResult().signingDetails); + } + + /** + * Verifies the provided APK using all allowed signing schemas. + * @return the certificates associated with each signer and content digests. + * @param verifyFull whether to verify all contents of this APK or just collect certificates. + * @hide + */ + public static ParseResult verifySignaturesInternal(ParseInput input, + String apkPath, @SignatureSchemeVersion int minSignatureSchemeVersion, + boolean verifyFull, boolean skipFindEntry) { + + if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V4) { + // V4 and before are older than the requested minimum signing version + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No signature found in package of version " + minSignatureSchemeVersion + + " or newer for package " + apkPath); + } + + // first try v4 + try { + return verifyV4Signature(input, apkPath, minSignatureSchemeVersion, verifyFull); + } catch (SignatureNotFoundException e) { + // not signed with v4, try older if allowed + if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V4) { + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No APK Signature Scheme v4 signature in package " + apkPath, e); + } + } + + if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V3) { + // V3 and before are older than the requested minimum signing version + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No signature found in package of version " + minSignatureSchemeVersion + + " or newer for package " + apkPath); + } + + return verifyV3AndBelowSignatures(input, apkPath, minSignatureSchemeVersion, verifyFull, skipFindEntry); + } + + private static ParseResult verifyV3AndBelowSignatures( + ParseInput input, String apkPath, @SignatureSchemeVersion int minSignatureSchemeVersion, + boolean verifyFull, boolean skipFindEntry) { + // try v3 + try { + return verifyV3Signature(input, apkPath, verifyFull); + } catch (SignatureNotFoundException e) { + // not signed with v3, try older if allowed + if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V3) { + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No APK Signature Scheme v3 signature in package " + apkPath, e); + } + } + + // redundant, protective version check + if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V2) { + // V2 and before are older than the requested minimum signing version + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No signature found in package of version " + minSignatureSchemeVersion + + " or newer for package " + apkPath); + } + + // try v2 + try { + return verifyV2Signature(input, apkPath, verifyFull); + } catch (SignatureNotFoundException e) { + // not signed with v2, try older if allowed + if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V2) { + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No APK Signature Scheme v2 signature in package " + apkPath, e); + } + } + + // redundant, protective version check + if (minSignatureSchemeVersion > SignatureSchemeVersion.JAR) { + // V1 and is older than the requested minimum signing version + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "No signature found in package of version " + minSignatureSchemeVersion + + " or newer for package " + apkPath); + } + + // v2 didn't work, try jarsigner + return verifyV1Signature(input, apkPath, verifyFull, skipFindEntry); + } + + /** + * Verifies the provided APK using V4 schema. + * + * @param verifyFull whether to verify (V4 vs V3) or just collect certificates. + * @return the certificates associated with each signer. + * @throws SignatureNotFoundException if there are no V4 signatures in the APK + */ + private static ParseResult verifyV4Signature(ParseInput input, + String apkPath, @SignatureSchemeVersion int minSignatureSchemeVersion, + boolean verifyFull) throws SignatureNotFoundException { + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV4" : "certsOnlyV4"); + try { + final Pair v4Pair = + ApkSignatureSchemeV4Verifier.extractSignature(apkPath); + final V4Signature.HashingInfo hashingInfo = v4Pair.first; + final V4Signature.SigningInfos signingInfos = v4Pair.second; + + Signature[] pastSignerSigs = null; + Map nonstreamingDigests = null; + Certificate[][] nonstreamingCerts = null; + + int v3BlockId = APK_SIGNATURE_SCHEME_DEFAULT; + // If V4 contains additional signing blocks then we need to always run v2/v3 verifier + // to figure out which block they use. + if (verifyFull || signingInfos.signingInfoBlocks.length > 0) { + try { + // v4 is an add-on and requires v2 or v3 signature to validate against its + // certificate and digest + ApkSignatureSchemeV3Verifier.VerifiedSigner v3Signer = + ApkSignatureSchemeV3Verifier.unsafeGetCertsWithoutVerification(apkPath); + nonstreamingDigests = v3Signer.contentDigests; + nonstreamingCerts = new Certificate[][]{v3Signer.certs}; + if (v3Signer.por != null) { + // populate proof-of-rotation information + pastSignerSigs = new Signature[v3Signer.por.certs.size()]; + for (int i = 0; i < pastSignerSigs.length; i++) { + pastSignerSigs[i] = new Signature( + v3Signer.por.certs.get(i).getEncoded()); + pastSignerSigs[i].setFlags(v3Signer.por.flagsList.get(i)); + } + } + v3BlockId = v3Signer.blockId; + } catch (SignatureNotFoundException e) { + try { + ApkSignatureSchemeV2Verifier.VerifiedSigner v2Signer = + ApkSignatureSchemeV2Verifier.verify(apkPath, false); + nonstreamingDigests = v2Signer.contentDigests; + nonstreamingCerts = v2Signer.certs; + } catch (SignatureNotFoundException ee) { + throw new SecurityException( + "V4 verification failed to collect V2/V3 certificates from : " + + apkPath, ee); + } + } + } + + ApkSignatureSchemeV4Verifier.VerifiedSigner vSigner = + ApkSignatureSchemeV4Verifier.verify(apkPath, hashingInfo, signingInfos, + v3BlockId); + Certificate[][] signerCerts = new Certificate[][]{vSigner.certs}; + Signature[] signerSigs = convertToSignatures(signerCerts); + + if (verifyFull) { + Signature[] nonstreamingSigs = convertToSignatures(nonstreamingCerts); + if (nonstreamingSigs.length != signerSigs.length) { + throw new SecurityException( + "Invalid number of certificates: " + nonstreamingSigs.length); + } + + for (int i = 0, size = signerSigs.length; i < size; ++i) { + if (!nonstreamingSigs[i].equals(signerSigs[i])) { + throw new SecurityException( + "V4 signature certificate does not match V2/V3"); + } + } + + boolean found = false; + for (byte[] nonstreamingDigest : nonstreamingDigests.values()) { + if (ArrayUtils.equals(vSigner.apkDigest, nonstreamingDigest, + vSigner.apkDigest.length)) { + found = true; + break; + } + } + if (!found) { + throw new SecurityException("APK digest in V4 signature does not match V2/V3"); + } + } + + return input.success(new SigningDetailsWithDigests(new SigningDetails(signerSigs, + SignatureSchemeVersion.SIGNING_BLOCK_V4, pastSignerSigs), + vSigner.contentDigests)); + } catch (SignatureNotFoundException e) { + throw e; + } catch (Exception e) { + // APK Signature Scheme v4 signature found but did not verify. + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to collect certificates from " + apkPath + + " using APK Signature Scheme v4", e); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + } + + /** + * Verifies the provided APK using V3 schema. + * + * @param verifyFull whether to verify all contents of this APK or just collect certificates. + * @return the certificates associated with each signer. + * @throws SignatureNotFoundException if there are no V3 signatures in the APK + */ + private static ParseResult verifyV3Signature(ParseInput input, + String apkPath, boolean verifyFull) throws SignatureNotFoundException { + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV3" : "certsOnlyV3"); + try { + ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner = + verifyFull ? ApkSignatureSchemeV3Verifier.verify(apkPath) + : ApkSignatureSchemeV3Verifier.unsafeGetCertsWithoutVerification( + apkPath); + Certificate[][] signerCerts = new Certificate[][]{vSigner.certs}; + Signature[] signerSigs = convertToSignatures(signerCerts); + Signature[] pastSignerSigs = null; + if (vSigner.por != null) { + // populate proof-of-rotation information + pastSignerSigs = new Signature[vSigner.por.certs.size()]; + for (int i = 0; i < pastSignerSigs.length; i++) { + pastSignerSigs[i] = new Signature(vSigner.por.certs.get(i).getEncoded()); + pastSignerSigs[i].setFlags(vSigner.por.flagsList.get(i)); + } + } + return input.success(new SigningDetailsWithDigests(new SigningDetails(signerSigs, + SignatureSchemeVersion.SIGNING_BLOCK_V3, pastSignerSigs), + vSigner.contentDigests)); + } catch (SignatureNotFoundException e) { + throw e; + } catch (Exception e) { + // APK Signature Scheme v3 signature found but did not verify + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to collect certificates from " + apkPath + + " using APK Signature Scheme v3", e); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + } + + /** + * Verifies the provided APK using V2 schema. + * + * @param verifyFull whether to verify all contents of this APK or just collect certificates. + * @return the certificates associated with each signer. + * @throws SignatureNotFoundException if there are no V2 signatures in the APK + */ + private static ParseResult verifyV2Signature(ParseInput input, + String apkPath, boolean verifyFull) throws SignatureNotFoundException { + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV2" : "certsOnlyV2"); + try { + ApkSignatureSchemeV2Verifier.VerifiedSigner vSigner = + ApkSignatureSchemeV2Verifier.verify(apkPath, verifyFull); + Certificate[][] signerCerts = vSigner.certs; + Signature[] signerSigs = convertToSignatures(signerCerts); + return input.success(new SigningDetailsWithDigests(new SigningDetails(signerSigs, + SignatureSchemeVersion.SIGNING_BLOCK_V2), vSigner.contentDigests)); + } catch (SignatureNotFoundException e) { + throw e; + } catch (Exception e) { + // APK Signature Scheme v2 signature found but did not verify + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to collect certificates from " + apkPath + + " using APK Signature Scheme v2", e); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + } + + /** + * Verifies the provided APK using JAR schema. + * @return the certificates associated with each signer. + * @param verifyFull whether to verify all contents of this APK or just collect certificates. + */ + private static ParseResult verifyV1Signature(ParseInput input, + String apkPath, boolean verifyFull, boolean skipFindEntry) { + StrictJarFile jarFile = null; + + try { + final Certificate[][] lastCerts; + final Signature[] lastSigs; + + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor"); + + // we still pass verify = true to ctor to collect certs, even though we're not checking + // the whole jar. + jarFile = new StrictJarFile( + apkPath, + true, // collect certs + verifyFull, // whether to reject APK with stripped v2 signatures (b/27887819) + skipFindEntry); // shareApp should skipFindEntry + final List toVerify = new ArrayList<>(); + + // Gather certs from AndroidManifest.xml, which every APK must have, as an optimization + // to not need to verify the whole APK when verifyFUll == false. + final ZipEntry manifestEntry = jarFile.findEntry( + ApkLiteParseUtils.ANDROID_MANIFEST_FILENAME); + if (manifestEntry == null) { + return input.error(INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Package " + apkPath + " has no manifest"); + } + final ParseResult result = + loadCertificates(input, jarFile, manifestEntry); + if (result.isError()) { + return input.error(result); + } + lastCerts = result.getResult(); + if (ArrayUtils.isEmpty(lastCerts)) { + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, "Package " + + apkPath + " has no certificates at entry " + + ApkLiteParseUtils.ANDROID_MANIFEST_FILENAME); + } + lastSigs = convertToSignatures(lastCerts); + + // fully verify all contents, except for AndroidManifest.xml and the META-INF/ files. + if (verifyFull) { + final Iterator i = jarFile.iterator(); + while (i.hasNext()) { + final ZipEntry entry = i.next(); + if (entry.isDirectory()) continue; + + final String entryName = entry.getName(); + if (entryName.startsWith("META-INF/")) continue; + if (entryName.equals(ApkLiteParseUtils.ANDROID_MANIFEST_FILENAME)) continue; + + toVerify.add(entry); + } + + for (ZipEntry entry : toVerify) { + final Certificate[][] entryCerts; + final ParseResult ret = + loadCertificates(input, jarFile, entry); + if (ret.isError()) { + return input.error(ret); + } + entryCerts = ret.getResult(); + if (ArrayUtils.isEmpty(entryCerts)) { + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Package " + apkPath + " has no certificates at entry " + + entry.getName()); + } + + // make sure all entries use the same signing certs + final Signature[] entrySigs = convertToSignatures(entryCerts); + if (!Arrays.equals(lastSigs, entrySigs)) { + return input.error( + INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, + "Package " + apkPath + " has mismatched certificates at entry " + + entry.getName()); + } + } + } + return input.success(new SigningDetailsWithDigests( + new SigningDetails(lastSigs, SignatureSchemeVersion.JAR), null)); + } catch (GeneralSecurityException e) { + return input.error(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING, + "Failed to collect certificates from " + apkPath, e); + } catch (IOException | RuntimeException e) { + return input.error(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to collect certificates from " + apkPath, e); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + closeQuietly(jarFile); + } + } + + private static ParseResult loadCertificates(ParseInput input, + StrictJarFile jarFile, ZipEntry entry) { + InputStream is = null; + try { + // We must read the stream for the JarEntry to retrieve + // its certificates. + is = jarFile.getInputStream(entry); + readFullyIgnoringContents(is); + return input.success(jarFile.getCertificateChains(entry)); + } catch (IOException | RuntimeException e) { + return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "Failed reading " + entry.getName() + " in " + jarFile, e); + } finally { + IoUtils.closeQuietly(is); + } + } + + private static void readFullyIgnoringContents(InputStream in) throws IOException { + byte[] buffer = sBuffer.getAndSet(null); + if (buffer == null) { + buffer = new byte[4096]; + } + + int n = 0; + int count = 0; + while ((n = in.read(buffer, 0, buffer.length)) != -1) { + count += n; + } + + sBuffer.set(buffer); + return; + } + + /** + * Converts an array of certificate chains into the {@code Signature} equivalent used by the + * PackageManager. + * + * @throws CertificateEncodingException if it is unable to create a Signature object. + */ + private static Signature[] convertToSignatures(Certificate[][] certs) + throws CertificateEncodingException { + final Signature[] res = new Signature[certs.length]; + for (int i = 0; i < certs.length; i++) { + res[i] = new Signature(certs[i]); + } + return res; + } + + private static void closeQuietly(StrictJarFile jarFile) { + if (jarFile != null) { + try { + jarFile.close(); + } catch (Exception ignored) { + } + } + } + + /** + * Returns the minimum signature scheme version required for an app targeting the specified + * {@code targetSdk}. + */ + public static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdk) { + if (targetSdk >= Build.VERSION_CODES.R) { + return SignatureSchemeVersion.SIGNING_BLOCK_V2; + } + return SignatureSchemeVersion.JAR; + } + + /** + * Result of a successful APK verification operation. + */ + public static class Result { + public final Certificate[][] certs; + public final Signature[] sigs; + public final int signatureSchemeVersion; + + public Result(Certificate[][] certs, Signature[] sigs, int signingVersion) { + this.certs = certs; + this.sigs = sigs; + this.signatureSchemeVersion = signingVersion; + } + } + + /** + * @return the verity root hash in the Signing Block. + */ + public static byte[] getVerityRootHash(String apkPath) throws IOException, SecurityException { + // first try v3 + try { + return ApkSignatureSchemeV3Verifier.getVerityRootHash(apkPath); + } catch (SignatureNotFoundException e) { + // try older version + } + try { + return ApkSignatureSchemeV2Verifier.getVerityRootHash(apkPath); + } catch (SignatureNotFoundException e) { + return null; + } + } + + /** + * Generates the Merkle tree and verity metadata to the buffer allocated by the {@code + * ByteBufferFactory}. + * + * @return the verity root hash of the generated Merkle tree. + */ + public static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory) + throws IOException, SignatureNotFoundException, SecurityException, DigestException, + NoSuchAlgorithmException { + // first try v3 + try { + return ApkSignatureSchemeV3Verifier.generateApkVerity(apkPath, bufferFactory); + } catch (SignatureNotFoundException e) { + // try older version + } + return ApkSignatureSchemeV2Verifier.generateApkVerity(apkPath, bufferFactory); + } + + /** + * Extended signing details. + * @hide for internal use only. + */ + public static class SigningDetailsWithDigests { + public final SigningDetails signingDetails; + + /** + * APK Signature Schemes v2/v3/v4 might contain multiple content digests. + * SignatureVerifier usually chooses one of them to verify. + * For certain signature schemes, e.g. v4, this digest is verified continuously. + * For others, e.g. v2, the caller has to specify if they want to verify. + * Please refer to documentation for more details. + */ + public final Map contentDigests; + + SigningDetailsWithDigests(SigningDetails signingDetails, + Map contentDigests) { + this.signingDetails = signingDetails; + this.contentDigests = contentDigests; + } + } +} diff --git a/aosp/frameworks/base/core/java/android/util/jar/StrictJarFile.java b/aosp/frameworks/base/core/java/android/util/jar/StrictJarFile.java new file mode 100644 index 0000000000000000000000000000000000000000..8ed8c5b52f1b570a1a24a07ce0abfa7e214fbbb7 --- /dev/null +++ b/aosp/frameworks/base/core/java/android/util/jar/StrictJarFile.java @@ -0,0 +1,514 @@ +/* + * Copyright (C) 2013 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.util.jar; + +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; + +import dalvik.system.CloseGuard; +import java.io.FileDescriptor; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.cert.Certificate; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.util.zip.ZipEntry; +import libcore.io.IoBridge; +import libcore.io.IoUtils; +import libcore.io.Streams; + +/** + * A subset of the JarFile API implemented as a thin wrapper over + * system/core/libziparchive. + * + * @hide for internal use only. Not API compatible (or as forgiving) as + * {@link java.util.jar.JarFile} + */ +public final class StrictJarFile { + + private final long nativeHandle; + + // NOTE: It's possible to share a file descriptor with the native + // code, at the cost of some additional complexity. + private final FileDescriptor fd; + + private final StrictJarManifest manifest; + private final StrictJarVerifier verifier; + + private final boolean isSigned; + + private final CloseGuard guard = CloseGuard.get(); + private boolean closed; + + public StrictJarFile(String fileName) + throws IOException, SecurityException { + this(fileName, true, true); + } + + public StrictJarFile(FileDescriptor fd) + throws IOException, SecurityException { + this(fd, true, true); + } + + public StrictJarFile(FileDescriptor fd, + boolean verify, + boolean signatureSchemeRollbackProtectionsEnforced) + throws IOException, SecurityException { + this("[fd:" + fd.getInt$() + "]", fd, verify, + signatureSchemeRollbackProtectionsEnforced, false); + } + + public StrictJarFile(String fileName, + boolean verify, + boolean signatureSchemeRollbackProtectionsEnforced) + throws IOException, SecurityException { + this(fileName, IoBridge.open(fileName, OsConstants.O_RDONLY), + verify, signatureSchemeRollbackProtectionsEnforced, false); + } + + public StrictJarFile(String fileName, + boolean verify, + boolean signatureSchemeRollbackProtectionsEnforced, + boolean skipFindEntry) + throws IOException, SecurityException { + this(fileName, IoBridge.open(fileName, OsConstants.O_RDONLY), + verify, signatureSchemeRollbackProtectionsEnforced, skipFindEntry); + } + + /** + * @param name of the archive (not necessarily a path). + * @param fd seekable file descriptor for the JAR file. + * @param verify whether to verify the file's JAR signatures and collect the corresponding + * signer certificates. + * @param signatureSchemeRollbackProtectionsEnforced {@code true} to enforce protections against + * stripping newer signature schemes (e.g., APK Signature Scheme v2) from the file, or + * {@code false} to ignore any such protections. This parameter is ignored when + * {@code verify} is {@code false}. + */ + private StrictJarFile(String name, + FileDescriptor fd, + boolean verify, + boolean signatureSchemeRollbackProtectionsEnforced, + boolean skipFindEntry) + throws IOException, SecurityException { + this.nativeHandle = nativeOpenJarFile(name, fd.getInt$()); + this.fd = fd; + + try { + // Read the MANIFEST and signature files up front and try to + // parse them. We never want to accept a JAR File with broken signatures + // or manifests, so it's best to throw as early as possible. + if (verify) { + HashMap metaEntries = getMetaEntries(); + this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true); + this.verifier = + new StrictJarVerifier( + name, + manifest, + metaEntries, + signatureSchemeRollbackProtectionsEnforced); + + if (!skipFindEntry) { + Set files = manifest.getEntries().keySet(); + for (String file : files) { + if (findEntry(file) == null) { + throw new SecurityException("File " + file + " in manifest does not exist"); + } + } + } + + isSigned = verifier.readCertificates() && verifier.isSignedJar(); + } else { + isSigned = false; + this.manifest = null; + this.verifier = null; + } + } catch (IOException | SecurityException e) { + nativeClose(this.nativeHandle); + IoUtils.closeQuietly(fd); + closed = true; + throw e; + } + + guard.open("close"); + } + + public StrictJarManifest getManifest() { + return manifest; + } + + public Iterator iterator() throws IOException { + return new EntryIterator(nativeHandle, ""); + } + + public ZipEntry findEntry(String name) { + return nativeFindEntry(nativeHandle, name); + } + + /** + * Return all certificate chains for a given {@link ZipEntry} belonging to this jar. + * This method MUST be called only after fully exhausting the InputStream belonging + * to this entry. + * + * Returns {@code null} if this jar file isn't signed or if this method is + * called before the stream is processed. + */ + public Certificate[][] getCertificateChains(ZipEntry ze) { + if (isSigned) { + return verifier.getCertificateChains(ze.getName()); + } + + return null; + } + + /** + * Return all certificates for a given {@link ZipEntry} belonging to this jar. + * This method MUST be called only after fully exhausting the InputStream belonging + * to this entry. + * + * Returns {@code null} if this jar file isn't signed or if this method is + * called before the stream is processed. + * + * @deprecated Switch callers to use getCertificateChains instead + */ + @Deprecated + public Certificate[] getCertificates(ZipEntry ze) { + if (isSigned) { + Certificate[][] certChains = verifier.getCertificateChains(ze.getName()); + + // Measure number of certs. + int count = 0; + for (Certificate[] chain : certChains) { + count += chain.length; + } + + // Create new array and copy all the certs into it. + Certificate[] certs = new Certificate[count]; + int i = 0; + for (Certificate[] chain : certChains) { + System.arraycopy(chain, 0, certs, i, chain.length); + i += chain.length; + } + + return certs; + } + + return null; + } + + public InputStream getInputStream(ZipEntry ze) { + final InputStream is = getZipInputStream(ze); + + if (isSigned) { + StrictJarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName()); + if (entry == null) { + return is; + } + + return new JarFileInputStream(is, ze.getSize(), entry); + } + + return is; + } + + public void close() throws IOException { + if (!closed) { + if (guard != null) { + guard.close(); + } + + nativeClose(nativeHandle); + IoUtils.closeQuietly(fd); + closed = true; + } + } + + @Override + protected void finalize() throws Throwable { + try { + if (guard != null) { + guard.warnIfOpen(); + } + close(); + } finally { + super.finalize(); + } + } + + private InputStream getZipInputStream(ZipEntry ze) { + if (ze.getMethod() == ZipEntry.STORED) { + return new FDStream(fd, ze.getDataOffset(), + ze.getDataOffset() + ze.getSize()); + } else { + final FDStream wrapped = new FDStream( + fd, ze.getDataOffset(), ze.getDataOffset() + ze.getCompressedSize()); + + int bufSize = Math.max(1024, (int) Math.min(ze.getSize(), 65535L)); + return new ZipInflaterInputStream(wrapped, new Inflater(true), bufSize, ze); + } + } + + static final class EntryIterator implements Iterator { + private final long iterationHandle; + private ZipEntry nextEntry; + + EntryIterator(long nativeHandle, String prefix) throws IOException { + iterationHandle = nativeStartIteration(nativeHandle, prefix); + } + + public ZipEntry next() { + if (nextEntry != null) { + final ZipEntry ze = nextEntry; + nextEntry = null; + return ze; + } + + return nativeNextEntry(iterationHandle); + } + + public boolean hasNext() { + if (nextEntry != null) { + return true; + } + + final ZipEntry ze = nativeNextEntry(iterationHandle); + if (ze == null) { + return false; + } + + nextEntry = ze; + return true; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + private HashMap getMetaEntries() throws IOException { + HashMap metaEntries = new HashMap(); + + Iterator entryIterator = new EntryIterator(nativeHandle, "META-INF/"); + while (entryIterator.hasNext()) { + final ZipEntry entry = entryIterator.next(); + metaEntries.put(entry.getName(), Streams.readFully(getInputStream(entry))); + } + + return metaEntries; + } + + static final class JarFileInputStream extends FilterInputStream { + private final StrictJarVerifier.VerifierEntry entry; + + private long count; + private boolean done = false; + + JarFileInputStream(InputStream is, long size, StrictJarVerifier.VerifierEntry e) { + super(is); + entry = e; + + count = size; + } + + @Override + public int read() throws IOException { + if (done) { + return -1; + } + if (count > 0) { + int r = super.read(); + if (r != -1) { + entry.write(r); + count--; + } else { + count = 0; + } + if (count == 0) { + done = true; + entry.verify(); + } + return r; + } else { + done = true; + entry.verify(); + return -1; + } + } + + @Override + public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + if (done) { + return -1; + } + if (count > 0) { + int r = super.read(buffer, byteOffset, byteCount); + if (r != -1) { + int size = r; + if (count < size) { + size = (int) count; + } + entry.write(buffer, byteOffset, size); + count -= size; + } else { + count = 0; + } + if (count == 0) { + done = true; + entry.verify(); + } + return r; + } else { + done = true; + entry.verify(); + return -1; + } + } + + @Override + public int available() throws IOException { + if (done) { + return 0; + } + return super.available(); + } + + @Override + public long skip(long byteCount) throws IOException { + return Streams.skipByReading(this, byteCount); + } + } + + /** @hide */ + public static class ZipInflaterInputStream extends InflaterInputStream { + private final ZipEntry entry; + private long bytesRead = 0; + private boolean closed; + + public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) { + super(is, inf, bsize); + this.entry = entry; + } + + @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + final int i; + try { + i = super.read(buffer, byteOffset, byteCount); + } catch (IOException e) { + throw new IOException("Error reading data for " + entry.getName() + " near offset " + + bytesRead, e); + } + if (i == -1) { + if (entry.getSize() != bytesRead) { + throw new IOException("Size mismatch on inflated file: " + bytesRead + " vs " + + entry.getSize()); + } + } else { + bytesRead += i; + } + return i; + } + + @Override public int available() throws IOException { + if (closed) { + // Our superclass will throw an exception, but there's a jtreg test that + // explicitly checks that the InputStream returned from ZipFile.getInputStream + // returns 0 even when closed. + return 0; + } + return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead); + } + + @Override + public void close() throws IOException { + super.close(); + closed = true; + } + } + + /** + * Wrap a stream around a FileDescriptor. The file descriptor is shared + * among all streams returned by getInputStream(), so we have to synchronize + * access to it. (We can optimize this by adding buffering here to reduce + * collisions.) + * + *

We could support mark/reset, but we don't currently need them. + * + * @hide + */ + public static class FDStream extends InputStream { + private final FileDescriptor fd; + private long endOffset; + private long offset; + + public FDStream(FileDescriptor fd, long initialOffset, long endOffset) { + this.fd = fd; + offset = initialOffset; + this.endOffset = endOffset; + } + + @Override public int available() throws IOException { + return (offset < endOffset ? 1 : 0); + } + + @Override public int read() throws IOException { + return Streams.readSingleByte(this); + } + + @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { + synchronized (this.fd) { + final long length = endOffset - offset; + if (byteCount > length) { + byteCount = (int) length; + } + try { + Os.lseek(fd, offset, OsConstants.SEEK_SET); + } catch (ErrnoException e) { + throw new IOException(e); + } + int count = IoBridge.read(fd, buffer, byteOffset, byteCount); + if (count > 0) { + offset += count; + return count; + } else { + return -1; + } + } + } + + @Override public long skip(long byteCount) throws IOException { + if (byteCount > endOffset - offset) { + byteCount = endOffset - offset; + } + offset += byteCount; + return byteCount; + } + } + + private static native long nativeOpenJarFile(String name, int fd) + throws IOException; + private static native long nativeStartIteration(long nativeHandle, String prefix); + private static native ZipEntry nativeNextEntry(long iterationHandle); + private static native ZipEntry nativeFindEntry(long nativeHandle, String entryName); + private static native void nativeClose(long nativeHandle); +} diff --git a/aosp/frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/aosp/frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..84da79d0df5269d1831b1f445ac35905d27db3de --- /dev/null +++ b/aosp/frameworks/base/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -0,0 +1,3450 @@ +/* + * 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 com.android.internal.pm.pkg.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.Flags.disallowSdkLibsToBeApps; +import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; +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_MANIFEST_MALFORMED; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NOT_APK; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_RESOURCES_ARSC_COMPRESSED; +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 static com.android.internal.pm.pkg.parsing.ParsingUtils.parseKnownActivityEmbeddingCerts; + +import android.annotation.AnyRes; +import android.annotation.CheckResult; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StyleableRes; +import android.app.ActivityThread; +import android.app.ResourcesManager; +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.Flags; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.Property; +import android.content.pm.Signature; +import android.content.pm.SigningDetails; +import android.content.pm.parsing.ApkLiteParseUtils; +import android.content.pm.parsing.FrameworkParsingPackageUtils; +import android.content.pm.parsing.PackageLite; +import android.content.pm.parsing.result.ParseInput; +import android.content.pm.parsing.result.ParseInput.DeferredError; +import android.content.pm.parsing.result.ParseResult; +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.Environment; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.os.Trace; +import android.os.UserHandle; +import android.os.ext.SdkExtensions; +import android.permission.PermissionManager; +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.SparseIntArray; +import android.util.TypedValue; +import android.util.apk.ApkSignatureVerifier; + +import com.android.internal.R; +import com.android.internal.os.ClassLoaderFactory; +import com.android.internal.pm.parsing.pkg.ParsedPackage; +import com.android.internal.pm.permission.CompatibilityPermissionInfo; +import com.android.internal.pm.pkg.component.ComponentMutateUtils; +import com.android.internal.pm.pkg.component.ComponentParseUtils; +import com.android.internal.pm.pkg.component.InstallConstraintsTagParser; +import com.android.internal.pm.pkg.component.ParsedActivity; +import com.android.internal.pm.pkg.component.ParsedActivityImpl; +import com.android.internal.pm.pkg.component.ParsedActivityUtils; +import com.android.internal.pm.pkg.component.ParsedApexSystemService; +import com.android.internal.pm.pkg.component.ParsedApexSystemServiceUtils; +import com.android.internal.pm.pkg.component.ParsedAttribution; +import com.android.internal.pm.pkg.component.ParsedAttributionUtils; +import com.android.internal.pm.pkg.component.ParsedComponent; +import com.android.internal.pm.pkg.component.ParsedInstrumentation; +import com.android.internal.pm.pkg.component.ParsedInstrumentationUtils; +import com.android.internal.pm.pkg.component.ParsedIntentInfo; +import com.android.internal.pm.pkg.component.ParsedIntentInfoImpl; +import com.android.internal.pm.pkg.component.ParsedIntentInfoUtils; +import com.android.internal.pm.pkg.component.ParsedMainComponent; +import com.android.internal.pm.pkg.component.ParsedPermission; +import com.android.internal.pm.pkg.component.ParsedPermissionGroup; +import com.android.internal.pm.pkg.component.ParsedPermissionUtils; +import com.android.internal.pm.pkg.component.ParsedProcess; +import com.android.internal.pm.pkg.component.ParsedProcessUtils; +import com.android.internal.pm.pkg.component.ParsedProvider; +import com.android.internal.pm.pkg.component.ParsedProviderUtils; +import com.android.internal.pm.pkg.component.ParsedService; +import com.android.internal.pm.pkg.component.ParsedServiceUtils; +import com.android.internal.pm.pkg.component.ParsedUsesPermission; +import com.android.internal.pm.pkg.component.ParsedUsesPermissionImpl; +import com.android.internal.pm.split.DefaultSplitAssetLoader; +import com.android.internal.pm.split.SplitAssetDependencyLoader; +import com.android.internal.pm.split.SplitAssetLoader; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.XmlUtils; + +import libcore.io.IoUtils; +import libcore.util.EmptyArray; +import libcore.util.HexEncoding; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +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 { + + private static final String TAG = ParsingUtils.TAG; + + public static final boolean DEBUG_JAR = false; + public static final boolean DEBUG_BACKUP = false; + public static final float DEFAULT_PRE_O_MAX_ASPECT_RATIO = 1.86f; + public static final float ASPECT_RATIO_NOT_SET = -1f; + + /** + * File name in an APK for the Android manifest. + */ + public static final String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml"; + + public static final String APP_METADATA_FILE_NAME = "app.metadata"; + + /** + * Path prefix for apps on expanded storage + */ + public static final String MNT_EXPAND = "/mnt/expand/"; + + public static final String TAG_ADOPT_PERMISSIONS = "adopt-permissions"; + public static final String TAG_APPLICATION = "application"; + public static final String TAG_ATTRIBUTION = "attribution"; + public static final String TAG_COMPATIBLE_SCREENS = "compatible-screens"; + public static final String TAG_EAT_COMMENT = "eat-comment"; + public static final String TAG_FEATURE_GROUP = "feature-group"; + public static final String TAG_INSTALL_CONSTRAINTS = "install-constraints"; + public static final String TAG_INSTRUMENTATION = "instrumentation"; + public static final String TAG_KEY_SETS = "key-sets"; + public static final String TAG_MANIFEST = "manifest"; + public static final String TAG_ORIGINAL_PACKAGE = "original-package"; + public static final String TAG_OVERLAY = "overlay"; + public static final String TAG_PACKAGE = "package"; + public static final String TAG_PACKAGE_VERIFIER = "package-verifier"; + public static final String TAG_PERMISSION = "permission"; + public static final String TAG_PERMISSION_GROUP = "permission-group"; + public static final String TAG_PERMISSION_TREE = "permission-tree"; + public static final String TAG_PROFILEABLE = "profileable"; + public static final String TAG_PROTECTED_BROADCAST = "protected-broadcast"; + public static final String TAG_QUERIES = "queries"; + public static final String TAG_RECEIVER = "receiver"; + public static final String TAG_RESTRICT_UPDATE = "restrict-update"; + public static final String TAG_SUPPORTS_INPUT = "supports-input"; + public static final String TAG_SUPPORT_SCREENS = "supports-screens"; + public static final String TAG_USES_CONFIGURATION = "uses-configuration"; + public static final String TAG_USES_FEATURE = "uses-feature"; + public static final String TAG_USES_GL_TEXTURE = "uses-gl-texture"; + public static final String TAG_USES_PERMISSION = "uses-permission"; + public static final String TAG_USES_PERMISSION_SDK_23 = "uses-permission-sdk-23"; + public static final String TAG_USES_PERMISSION_SDK_M = "uses-permission-sdk-m"; + public static final String TAG_USES_SDK = "uses-sdk"; + public static final String TAG_USES_SPLIT = "uses-split"; + + public static final String METADATA_MAX_ASPECT_RATIO = "android.max_aspect"; + public static final String METADATA_SUPPORTS_SIZE_CHANGES = "android.supports_size_changes"; + public static final String METADATA_CAN_DISPLAY_ON_REMOTE_DEVICES = + "android.can_display_on_remote_devices"; + public static final String METADATA_ACTIVITY_WINDOW_LAYOUT_AFFINITY = + "android.activity_window_layout_affinity"; + public static final String METADATA_ACTIVITY_LAUNCH_MODE = "android.activity.launch_mode"; + + public static final int SDK_VERSION = Build.VERSION.SDK_INT; + public static final String[] SDK_CODENAMES = Build.VERSION.ACTIVE_CODENAMES; + + public static boolean sCompatibilityModeEnabled = true; + public static boolean sUseRoundIcon = false; + + public static final int PARSE_DEFAULT_INSTALL_LOCATION = + PackageInfo.INSTALL_LOCATION_UNSPECIFIED; + public static final int PARSE_DEFAULT_TARGET_SANDBOX = 1; + + /** + * If set to true, we will only allow package files that exactly match the DTD. Otherwise, we + * try to get as much from the package as we can without failing. This should normally be set to + * false, to support extensions to the DTD in future versions. + */ + public static final boolean RIGID_PARSER = false; + + public static final int PARSE_MUST_BE_APK = 1 << 0; + public static final int PARSE_IGNORE_PROCESSES = 1 << 1; + public static final int PARSE_EXTERNAL_STORAGE = 1 << 3; + public static final int PARSE_IS_SYSTEM_DIR = 1 << 4; + public static final int PARSE_COLLECT_CERTIFICATES = 1 << 5; + public static final int PARSE_ENFORCE_CODE = 1 << 6; + /** + * This flag is applied in the ApkLiteParser. Used by OverlayConfigParser to ignore the checks + * of required system property within the overlay tag. + */ + public static final int PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY = 1 << 7; + public static final int PARSE_APK_IN_APEX = 1 << 9; + + public static final int PARSE_CHATTY = 1 << 31; + + @IntDef(flag = true, prefix = { "PARSE_" }, value = { + PARSE_CHATTY, + PARSE_COLLECT_CERTIFICATES, + PARSE_ENFORCE_CODE, + PARSE_EXTERNAL_STORAGE, + PARSE_IGNORE_PROCESSES, + PARSE_IS_SYSTEM_DIR, + PARSE_MUST_BE_APK, + PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ParseFlags {} + + /** + * For cases outside of PackageManagerService when an APK needs to be parsed as a one-off + * request, without caching the input object and without querying the internal system state for + * feature support. + */ + @NonNull + public static ParseResult parseDefault(ParseInput input, File file, + @ParseFlags int parseFlags, + @NonNull List splitPermissions, + boolean collectCertificates, Callback callback) { + + ParsingPackageUtils parser = new ParsingPackageUtils(null /*separateProcesses*/, + null /*displayMetrics*/, splitPermissions, callback); + var parseResult = parser.parsePackage(input, file, parseFlags); + if (parseResult.isError()) { + return input.error(parseResult); + } + + var pkg = parseResult.getResult().hideAsParsed(); + + if (collectCertificates) { + final ParseResult ret = + ParsingPackageUtils.getSigningDetails(input, pkg, false /*skipVerify*/); + if (ret.isError()) { + return input.error(ret); + } + pkg.setSigningDetails(ret.getResult()); + } + + return input.success(pkg); + } + + private final String[] mSeparateProcesses; + private final DisplayMetrics mDisplayMetrics; + @NonNull + private final List mSplitPermissionInfos; + private final Callback mCallback; + + public ParsingPackageUtils(String[] separateProcesses, DisplayMetrics displayMetrics, + @NonNull List splitPermissions, + @NonNull Callback callback) { + mSeparateProcesses = separateProcesses; + mDisplayMetrics = displayMetrics; + mSplitPermissionInfos = splitPermissions; + 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). + *

+ * This performs validity checking on cluster style packages, such as requiring identical + * package name and version codes, a single base APK, and unique split names. + *

+ * Note that this does not perform signature verification; that must be done separately + * in {@link #getSigningDetails(ParseInput, ParsedPackage, 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. + */ + public ParseResult parsePackage(ParseInput input, File packageFile, int flags) { + 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 validity checking, such as requiring + * identical package name and version codes, a single base APK, and unique + * split names. + *

+ * Note that this does not perform signature verification; that must be done separately + * in {@link #getSigningDetails(ParseInput, ParsedPackage, boolean)}. + */ + private ParseResult parseClusterPackage(ParseInput input, File packageDir, + int flags) { + int liteParseFlags = 0; + if ((flags & PARSE_APK_IN_APEX) != 0) { + liteParseFlags |= PARSE_APK_IN_APEX; + } + final ParseResult liteResult = + ApkLiteParseUtils.parseClusterPackageLite(input, packageDir, liteParseFlags); + if (liteResult.isError()) { + return input.error(liteResult); + } + + final PackageLite lite = liteResult.getResult(); + // Build the split dependency tree. + SparseArray splitDependencies = null; + final SplitAssetLoader assetLoader; + if (lite.isIsolatedSplits() && !ArrayUtils.isEmpty(lite.getSplitNames())) { + 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 File baseApk = new File(lite.getBaseApkPath()); + boolean shouldSkipComponents = lite.isIsSdkLibrary() && disallowSdkLibsToBeApps(); + final ParseResult result = parseBaseApk(input, baseApk, + lite.getPath(), assetLoader, flags, shouldSkipComponents); + if (result.isError()) { + return input.error(result); + } + + ParsingPackage pkg = result.getResult(); + if (!ArrayUtils.isEmpty(lite.getSplitNames())) { + pkg.asSplit( + lite.getSplitNames(), + lite.getSplitApkPaths(), + lite.getSplitRevisionCodes(), + splitDependencies + ); + final int num = lite.getSplitNames().length; + + for (int i = 0; i < num; i++) { + final AssetManager splitAssets = assetLoader.getSplitAssetManager(i); + final ParseResult split = + parseSplitApk(input, pkg, i, splitAssets, flags); + if (split.isError()) { + return input.error(split); + } + } + } + + pkg.set32BitAbiPreferred(lite.isUse32bitAbi()); + return input.success(pkg); + } catch (IllegalArgumentException e) { + return input.error(e.getCause() instanceof IOException ? INSTALL_FAILED_INVALID_APK + : INSTALL_PARSE_FAILED_NOT_APK, e.getMessage(), e); + } finally { + IoUtils.closeQuietly(assetLoader); + } + } + + /** + * Parse the given APK file, treating it as as a single monolithic package. + *

+ * Note that this does not perform signature verification; that must be done separately + * in {@link #getSigningDetails(ParseInput, ParsedPackage, boolean)}. + */ + private ParseResult parseMonolithicPackage(ParseInput input, File apkFile, + int flags) { + final ParseResult liteResult = + ApkLiteParseUtils.parseMonolithicPackageLite(input, apkFile, flags); + if (liteResult.isError()) { + return input.error(liteResult); + } + + final PackageLite lite = liteResult.getResult(); + final SplitAssetLoader assetLoader = new DefaultSplitAssetLoader(lite, flags); + try { + boolean shouldSkipComponents = lite.isIsSdkLibrary() && disallowSdkLibsToBeApps(); + final ParseResult result = parseBaseApk(input, + apkFile, + apkFile.getCanonicalPath(), + assetLoader, flags, shouldSkipComponents); + if (result.isError()) { + return input.error(result); + } + + return input.success(result.getResult() + .set32BitAbiPreferred(lite.isUse32bitAbi())); + } catch (IOException e) { + return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "Failed to get path: " + apkFile, e); + } finally { + IoUtils.closeQuietly(assetLoader); + } + } + + /** + * Creates ParsingPackage using only PackageLite. + * Missing fields will contain reasonable defaults. + * Used for packageless (aka archived) package installation. + */ + public ParseResult parsePackageFromPackageLite(ParseInput input, + PackageLite lite, int flags) { + final String volumeUuid = getVolumeUuid(lite.getPath()); + final String pkgName = lite.getPackageName(); + + final TypedArray manifestArray = null; + final ParsingPackage pkg = mCallback.startParsingPackage(pkgName, + lite.getBaseApkPath(), lite.getPath(), manifestArray, lite.isCoreApp()); + + final int targetSdk = lite.getTargetSdk(); + final String versionName = null; + final int compileSdkVersion = 0; + final String compileSdkVersionCodeName = null; + final boolean isolatedSplitLoading = false; + + // Normally set from manifestArray. + pkg.setVersionCode(lite.getVersionCode()); + pkg.setVersionCodeMajor(lite.getVersionCodeMajor()); + pkg.setBaseRevisionCode(lite.getBaseRevisionCode()); + pkg.setVersionName(versionName); + pkg.setCompileSdkVersion(compileSdkVersion); + pkg.setCompileSdkVersionCodeName(compileSdkVersionCodeName); + pkg.setIsolatedSplitLoading(isolatedSplitLoading); + pkg.setTargetSdkVersion(targetSdk); + + // parseBaseApkTags + pkg.setInstallLocation(lite.getInstallLocation()) + .setTargetSandboxVersion(PARSE_DEFAULT_TARGET_SANDBOX) + /* Set the global "on SD card" flag */ + .setExternalStorage((flags & PARSE_EXTERNAL_STORAGE) != 0); + + var archivedPackage = lite.getArchivedPackage(); + if (archivedPackage == null) { + return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "archivePackage is missing"); + } + + // parseBaseAppBasicFlags + pkg + // Default true + .setBackupAllowed(true) + .setClearUserDataAllowed(true) + .setClearUserDataOnFailedRestoreAllowed(true) + .setAllowNativeHeapPointerTagging(true) + .setEnabled(true) + .setExtractNativeLibrariesRequested(true) + // targetSdkVersion gated + .setAllowAudioPlaybackCapture(targetSdk >= Build.VERSION_CODES.Q) + .setHardwareAccelerated(targetSdk >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + .setRequestLegacyExternalStorage( + XmlUtils.convertValueToBoolean(archivedPackage.requestLegacyExternalStorage, + targetSdk < Build.VERSION_CODES.Q)) + .setCleartextTrafficAllowed(targetSdk < Build.VERSION_CODES.P) + // Default false + .setDefaultToDeviceProtectedStorage(XmlUtils.convertValueToBoolean( + archivedPackage.defaultToDeviceProtectedStorage, false)) + .setUserDataFragile( + XmlUtils.convertValueToBoolean(archivedPackage.userDataFragile, false)) + // Ints + .setCategory(ApplicationInfo.CATEGORY_UNDEFINED) + // Floats Default 0f + .setMaxAspectRatio(0f) + .setMinAspectRatio(0f); + + // No APK - no code. + pkg.setDeclaredHavingCode(false); + + final String taskAffinity = null; + ParseResult taskAffinityResult = ComponentParseUtils.buildTaskAffinityName( + pkgName, pkgName, taskAffinity, input); + if (taskAffinityResult.isError()) { + return input.error(taskAffinityResult); + } + pkg.setTaskAffinity(taskAffinityResult.getResult()); + + final CharSequence pname = null; + ParseResult processNameResult = ComponentParseUtils.buildProcessName( + pkgName, null /*defProc*/, pname, flags, mSeparateProcesses, input); + if (processNameResult.isError()) { + return input.error(processNameResult); + } + pkg.setProcessName(processNameResult.getResult()); + + pkg.setGwpAsanMode(-1); + pkg.setMemtagMode(-1); + + afterParseBaseApplication(pkg); + + final ParseResult result = validateBaseApkTags(input, pkg); + if (result.isError()) { + return result; + } + + pkg.setVolumeUuid(volumeUuid); + + if ((flags & PARSE_COLLECT_CERTIFICATES) != 0) { + pkg.setSigningDetails(lite.getSigningDetails()); + } else { + pkg.setSigningDetails(SigningDetails.UNKNOWN); + } + + return input.success(pkg + .set32BitAbiPreferred(lite.isUse32bitAbi())); + } + + private static String getVolumeUuid(final String apkPath) { + String volumeUuid = null; + if (apkPath.startsWith(MNT_EXPAND)) { + final int end = apkPath.indexOf('/', MNT_EXPAND.length()); + volumeUuid = apkPath.substring(MNT_EXPAND.length(), end); + } + return volumeUuid; + } + + private ParseResult parseBaseApk(ParseInput input, File apkFile, + String codePath, SplitAssetLoader assetLoader, int flags, + boolean shouldSkipComponents) { + final String apkPath = apkFile.getAbsolutePath(); + + final String volumeUuid = getVolumeUuid(apkPath); + + if (DEBUG_JAR) Slog.d(TAG, "Scanning base APK: " + apkPath); + + final AssetManager assets; + try { + assets = assetLoader.getBaseAssetManager(); + } catch (IllegalArgumentException e) { + return input.error(e.getCause() instanceof IOException ? INSTALL_FAILED_INVALID_APK + : INSTALL_PARSE_FAILED_NOT_APK, e.getMessage(), e); + } + 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, + ANDROID_MANIFEST_FILENAME)) { + final Resources res = new Resources(assets, mDisplayMetrics, null); + + ParseResult result = parseBaseApk(input, apkPath, codePath, res, + parser, flags, shouldSkipComponents); + if (result.isError()) { + return input.error(result.getErrorCode(), + apkPath + " (at " + parser.getPositionDescription() + "): " + + result.getErrorMessage()); + } + + final ParsingPackage pkg = result.getResult(); + if (assets.containsAllocatedTable()) { + final ParseResult deferResult = input.deferError( + "Targeting R+ (version " + Build.VERSION_CODES.R + " and above) requires" + + " the resources.arsc of installed APKs to be stored uncompressed" + + " and aligned on a 4-byte boundary", + DeferredError.RESOURCES_ARSC_COMPRESSED); + if (deferResult.isError()) { + return input.error(INSTALL_PARSE_FAILED_RESOURCES_ARSC_COMPRESSED, + deferResult.getErrorMessage()); + } + } + + ApkAssets apkAssets = assetLoader.getBaseApkAssets(); + boolean definesOverlayable = false; + try { + definesOverlayable = apkAssets.definesOverlayable(); + } catch (IOException ignored) { + // Will fail if there's no packages in the ApkAssets, which can be treated as false + } + + if (definesOverlayable) { + SparseArray packageNames = assets.getAssignedPackageIdentifiers(); + int size = packageNames.size(); + for (int index = 0; index < size; index++) { + String packageName = packageNames.valueAt(index); + Map overlayableToActor = assets.getOverlayableMap(packageName); + if (overlayableToActor != null && !overlayableToActor.isEmpty()) { + for (String overlayable : overlayableToActor.keySet()) { + pkg.addOverlayable(overlayable, overlayableToActor.get(overlayable)); + } + } + } + } + + pkg.setVolumeUuid(volumeUuid); + + if ((flags & PARSE_COLLECT_CERTIFICATES) != 0) { + final ParseResult ret = + getSigningDetails(input, pkg, false /*skipVerify*/); + if (ret.isError()) { + return input.error(ret); + } + pkg.setSigningDetails(ret.getResult()); + } else { + pkg.setSigningDetails(SigningDetails.UNKNOWN); + } + + if (Flags.aslInApkAppMetadataSource()) { + try (InputStream in = assets.open(APP_METADATA_FILE_NAME)) { + pkg.setAppMetadataFileInApk(true); + } catch (Exception e) { } + } + + return input.success(pkg); + } catch (Exception e) { + return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, + "Failed to read manifest from " + apkPath, e); + } + } + + private ParseResult parseSplitApk(ParseInput input, + ParsingPackage pkg, int splitIndex, AssetManager assets, int flags) { + final String apkPath = pkg.getSplitCodePaths()[splitIndex]; + + if (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, + ANDROID_MANIFEST_FILENAME)) { + Resources res = new Resources(assets, mDisplayMetrics, null); + ParseResult 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 base APK. 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 + * @param shouldSkipComponents If the package is a sdk-library + * @return Parsed package or null on error. + */ + private ParseResult parseBaseApk(ParseInput input, String apkPath, + String codePath, Resources res, XmlResourceParser parser, int flags, + boolean shouldSkipComponents) + throws XmlPullParserException, IOException { + final String splitName; + final String pkgName; + + ParseResult> packageSplitResult = + ApkLiteParseUtils.parsePackageSplitNames(input, parser); + if (packageSplitResult.isError()) { + return input.error(packageSplitResult); + } + + Pair packageSplit = packageSplitResult.getResult(); + 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 + ); + } + + final TypedArray manifestArray = res.obtainAttributes(parser, R.styleable.AndroidManifest); + try { + final boolean isCoreApp = parser.getAttributeBooleanValue(null /*namespace*/, + "coreApp", false); + final ParsingPackage pkg = mCallback.startParsingPackage( + pkgName, apkPath, codePath, manifestArray, isCoreApp); + final ParseResult result = + parseBaseApkTags(input, pkg, manifestArray, res, parser, flags, + shouldSkipComponents); + if (result.isError()) { + return result; + } + + return input.success(pkg); + } finally { + manifestArray.recycle(); + } + } + + /** + * Parse the manifest of a split APK. + *

+ * 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 parseSplitApk(ParseInput input, ParsingPackage pkg, + Resources res, XmlResourceParser parser, int flags, int splitIndex) + throws XmlPullParserException, IOException { + // We parsed manifest tag earlier; just skip past it + final ParseResult> packageSplitResult = + ApkLiteParseUtils.parsePackageSplitNames(input, parser); + if (packageSplitResult.isError()) { + return input.error(packageSplitResult); + } + + 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 (TAG_APPLICATION.equals(tagName)) { + if (foundApp) { + if (RIGID_PARSER) { + result = input.error(" has more than one "); + } else { + Slog.w(TAG, " has more than one "); + result = input.success(null); + } + } else { + foundApp = true; + result = parseSplitApplication(input, pkg, res, parser, flags, splitIndex); + } + } else { + result = ParsingUtils.unknownTag("", pkg, parser, input); + } + + if (result.isError()) { + return input.error(result); + } + } + + if (!foundApp) { + ParseResult deferResult = input.deferError( + " does not contain an ", DeferredError.MISSING_APP_TAG); + if (deferResult.isError()) { + return input.error(deferResult); + } + } + + return input.success(pkg); + } + + /** + * Parse the {@code application} XML tree at the current parse location in a + * split APK manifest. + *

+ * 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 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(); + } + + // 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. + String defaultSplitName = pkg.getSplitNames()[splitIndex]; + + 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 activityResult = + ParsedActivityUtils.parseActivityOrReceiver(mSeparateProcesses, pkg, + res, parser, flags, sUseRoundIcon, defaultSplitName, 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 serviceResult = ParsedServiceUtils.parseService( + mSeparateProcesses, pkg, res, parser, flags, sUseRoundIcon, + defaultSplitName, input); + if (serviceResult.isSuccess()) { + ParsedService service = serviceResult.getResult(); + pkg.addService(service); + mainComponent = service; + } + result = serviceResult; + break; + case "provider": + ParseResult providerResult = + ParsedProviderUtils.parseProvider(mSeparateProcesses, pkg, res, parser, + flags, sUseRoundIcon, defaultSplitName, 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, + sUseRoundIcon, defaultSplitName, 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); + } + } + + 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 metaDataResult = parseMetaData(pkg, null /*component*/, + res, parser, "", input); + if (metaDataResult.isSuccess() && metaDataResult.getResult() != null) { + pkg.setMetaData(metaDataResult.getResult().toBundle(pkg.getMetaData())); + } + return metaDataResult; + case "property": + ParseResult propertyResult = parseMetaData(pkg, null /*component*/, + res, parser, "", input); + if (propertyResult.isSuccess()) { + pkg.addProperty(propertyResult.getResult()); + } + return propertyResult; + case "uses-sdk-library": + return parseUsesSdkLibrary(input, pkg, res, parser); + case "uses-static-library": + return parseUsesStaticLibrary(input, pkg, res, parser); + case "uses-library": + return parseUsesLibrary(input, pkg, res, parser); + case "uses-native-library": + return parseUsesNativeLibrary(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("", pkg, parser, input); + } + } + private ParseResult parseBaseApkTags(ParseInput input, ParsingPackage pkg, + TypedArray sa, Resources res, XmlResourceParser parser, int flags, + boolean shouldSkipComponents) throws XmlPullParserException, IOException { + ParseResult sharedUserResult = parseSharedUser(input, pkg, sa); + if (sharedUserResult.isError()) { + return sharedUserResult; + } + + final boolean updatableSystem = parser.getAttributeBooleanValue(null /*namespace*/, + "updatableSystem", true); + final String emergencyInstaller = parser.getAttributeValue(null /*namespace*/, + "emergencyInstaller"); + + pkg.setInstallLocation(anInteger(PARSE_DEFAULT_INSTALL_LOCATION, + R.styleable.AndroidManifest_installLocation, sa)) + .setTargetSandboxVersion(anInteger(PARSE_DEFAULT_TARGET_SANDBOX, + R.styleable.AndroidManifest_targetSandboxVersion, sa)) + /* Set the global "on SD card" flag */ + .setExternalStorage((flags & PARSE_EXTERNAL_STORAGE) != 0) + .setUpdatableSystem(updatableSystem) + .setEmergencyInstaller(emergencyInstaller); + + 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; + + // has special logic, so it's handled outside the general method + if (TAG_APPLICATION.equals(tagName)) { + if (foundApp) { + if (RIGID_PARSER) { + result = input.error(" has more than one "); + } else { + Slog.w(TAG, " has more than one "); + result = input.success(null); + } + } else { + foundApp = true; + result = parseBaseApplication(input, pkg, res, parser, flags, + shouldSkipComponents); + } + } else { + result = parseBaseApkTag(tagName, input, pkg, res, parser, flags); + } + + if (result.isError()) { + return input.error(result); + } + } + + if (!foundApp && ArrayUtils.size(pkg.getInstrumentations()) == 0) { + ParseResult deferResult = input.deferError( + " does not contain an or ", + DeferredError.MISSING_APP_TAG); + if (deferResult.isError()) { + return input.error(deferResult); + } + } + + return validateBaseApkTags(input, pkg); + } + + private ParseResult validateBaseApkTags(ParseInput input, ParsingPackage pkg) { + if (!ParsedAttributionUtils.isCombinationValid(pkg.getAttributions())) { + return input.error( + INSTALL_PARSE_FAILED_BAD_MANIFEST, + "Combination tags are not valid" + ); + } + + if (ParsedPermissionUtils.declareDuplicatePermission(pkg)) { + return input.error( + INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + "Found duplicate permission with a different attribute value." + ); + } + + convertCompatPermissions(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.isSmallScreensSupported() + && !pkg.isNormalScreensSupported() + && !pkg.isLargeScreensSupported() + && !pkg.isExtraLargeScreensSupported() + && !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 TAG_OVERLAY: + return parseOverlay(input, pkg, res, parser); + case TAG_KEY_SETS: + return parseKeySets(input, pkg, res, parser); + case "feature": // TODO moltmann: Remove + case TAG_ATTRIBUTION: + return parseAttribution(input, pkg, res, parser); + case TAG_PERMISSION_GROUP: + return parsePermissionGroup(input, pkg, res, parser); + case TAG_PERMISSION: + return parsePermission(input, pkg, res, parser); + case TAG_PERMISSION_TREE: + return parsePermissionTree(input, pkg, res, parser); + case TAG_USES_PERMISSION: + case TAG_USES_PERMISSION_SDK_M: + case TAG_USES_PERMISSION_SDK_23: + return parseUsesPermission(input, pkg, res, parser); + case TAG_USES_CONFIGURATION: + return parseUsesConfiguration(input, pkg, res, parser); + case TAG_USES_FEATURE: + return parseUsesFeature(input, pkg, res, parser); + case TAG_FEATURE_GROUP: + return parseFeatureGroup(input, pkg, res, parser); + case TAG_USES_SDK: + return parseUsesSdk(input, pkg, res, parser, flags); + case TAG_SUPPORT_SCREENS: + return parseSupportScreens(input, pkg, res, parser); + case TAG_PROTECTED_BROADCAST: + return parseProtectedBroadcast(input, pkg, res, parser); + case TAG_INSTRUMENTATION: + return parseInstrumentation(input, pkg, res, parser); + case TAG_ORIGINAL_PACKAGE: + return parseOriginalPackage(input, pkg, res, parser); + case TAG_ADOPT_PERMISSIONS: + return parseAdoptPermissions(input, pkg, res, parser); + case TAG_USES_GL_TEXTURE: + case TAG_COMPATIBLE_SCREENS: + case TAG_SUPPORTS_INPUT: + case TAG_EAT_COMMENT: + // Just skip this tag + XmlUtils.skipCurrentTag(parser); + return input.success(pkg); + case TAG_RESTRICT_UPDATE: + return parseRestrictUpdateHash(flags, input, pkg, res, parser); + case TAG_INSTALL_CONSTRAINTS: + return parseInstallConstraints(input, pkg, res, parser, + mCallback.getInstallConstraintsAllowlist()); + case TAG_QUERIES: + return parseQueries(input, pkg, res, parser); + default: + return ParsingUtils.unknownTag("", pkg, parser, input); + } + } + + private static ParseResult parseSharedUser(ParseInput input, + ParsingPackage pkg, TypedArray sa) { + String str = nonConfigString(0, R.styleable.AndroidManifest_sharedUserId, sa); + if (TextUtils.isEmpty(str)) { + return input.success(pkg); + } + + if (!"android".equals(pkg.getPackageName())) { + ParseResult nameResult = FrameworkParsingPackageUtils.validateName(input, str, + true, true); + if (nameResult.isError()) { + return input.error(PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID, + " specifies bad sharedUserId name \"" + str + "\": " + + nameResult.getErrorMessage()); + } + } + + boolean leaving = false; + if (PackageManager.ENABLE_SHARED_UID_MIGRATION) { + int max = anInteger(0, R.styleable.AndroidManifest_sharedUserMaxSdkVersion, sa); + leaving = (max != 0) && (max < Build.VERSION.RESOURCES_SDK_INT); + } + + return input.success(pkg + .setLeavingSharedUser(leaving) + .setSharedUserId(str.intern()) + .setSharedUserLabelResourceId( + resId(R.styleable.AndroidManifest_sharedUserLabel, sa))); + } + + private static ParseResult 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 publicKeys = new ArrayMap<>(); + ArraySet upgradeKeySets = new ArraySet<>(); + ArrayMap> definedKeySets = new ArrayMap<>(); + ArraySet 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 = + FrameworkParsingPackageUtils.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("", pkg, parser, + input); + if (result.isError()) { + return input.error(result); + } + break; + } + } + String packageName = pkg.getPackageName(); + Set 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> 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 parseAttribution(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser) + throws IOException, XmlPullParserException { + ParseResult result = ParsedAttributionUtils.parseAttribution(res, + parser, input); + if (result.isError()) { + return input.error(result); + } + return input.success(pkg.addAttribution(result.getResult())); + } + + private static ParseResult parsePermissionGroup(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser) + throws XmlPullParserException, IOException { + ParseResult result = ParsedPermissionUtils.parsePermissionGroup( + pkg, res, parser, sUseRoundIcon, input); + if (result.isError()) { + return input.error(result); + } + return input.success(pkg.addPermissionGroup(result.getResult())); + } + + private static ParseResult parsePermission(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser) + throws XmlPullParserException, IOException { + ParseResult result = ParsedPermissionUtils.parsePermission( + pkg, res, parser, sUseRoundIcon, input); + if (result.isError()) { + return input.error(result); + } + ParsedPermission permission = result.getResult(); + if (permission != null) { + pkg.addPermission(permission); + } + return input.success(pkg); + } + + private static ParseResult parsePermissionTree(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser) + throws XmlPullParserException, IOException { + ParseResult result = ParsedPermissionUtils.parsePermissionTree( + pkg, res, parser, sUseRoundIcon, input); + if (result.isError()) { + return input.error(result); + } + return input.success(pkg.addPermission(result.getResult())); + } + + private int parseMinOrMaxSdkVersion(TypedArray sa, int attr, int defaultValue) { + int val = defaultValue; + TypedValue peekVal = sa.peekValue(attr); + if (peekVal != null) { + if (peekVal.type >= TypedValue.TYPE_FIRST_INT + && peekVal.type <= TypedValue.TYPE_LAST_INT) { + val = peekVal.data; + } + } + return val; + } + + private ParseResult 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 minSdkVersion = parseMinOrMaxSdkVersion(sa, + R.styleable.AndroidManifestUsesPermission_minSdkVersion, + Integer.MIN_VALUE); + + int maxSdkVersion = parseMinOrMaxSdkVersion(sa, + R.styleable.AndroidManifestUsesPermission_maxSdkVersion, + Integer.MAX_VALUE); + + final ArraySet requiredFeatures = new ArraySet<>(); + String feature = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestUsesPermission_requiredFeature, + 0); + if (feature != null) { + requiredFeatures.add(feature); + } + + final ArraySet requiredNotFeatures = new ArraySet<>(); + feature = sa.getNonConfigurationString( + com.android.internal.R.styleable + .AndroidManifestUsesPermission_requiredNotFeature, + 0); + if (feature != null) { + requiredNotFeatures.add(feature); + } + + final int usesPermissionFlags = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesPermission_usesPermissionFlags, + 0); + + final int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + final ParseResult result; + switch (parser.getName()) { + case "required-feature": + result = parseRequiredFeature(input, res, parser); + if (result.isSuccess()) { + requiredFeatures.add((String) result.getResult()); + } + break; + + case "required-not-feature": + result = parseRequiredNotFeature(input, res, parser); + if (result.isSuccess()) { + requiredNotFeatures.add((String) result.getResult()); + } + break; + + default: + result = ParsingUtils.unknownTag("", pkg, parser, input); + break; + } + + if (result.isError()) { + return input.error(result); + } + } + + // Can only succeed from here on out + ParseResult success = input.success(pkg); + + if (name == null) { + return success; + } + + if (Build.VERSION.RESOURCES_SDK_INT < minSdkVersion + || Build.VERSION.RESOURCES_SDK_INT > maxSdkVersion) { + return success; + } + + if (mCallback != null) { + // Only allow requesting this permission if the platform supports all of the + // "required-feature"s. + for (int i = requiredFeatures.size() - 1; i >= 0; i--) { + if (!mCallback.hasFeature(requiredFeatures.valueAt(i))) { + return success; + } + } + + // Only allow requesting this permission if the platform does not supports any of + // the "required-not-feature"s. + for (int i = requiredNotFeatures.size() - 1; i >= 0; i--) { + if (mCallback.hasFeature(requiredNotFeatures.valueAt(i))) { + return success; + } + } + } + + // Quietly ignore duplicate permission requests, but fail loudly if + // the two requests have conflicting flags + boolean found = false; + final List usesPermissions = pkg.getUsesPermissions(); + final int size = usesPermissions.size(); + for (int i = 0; i < size; i++) { + final ParsedUsesPermission usesPermission = usesPermissions.get(i); + if (Objects.equals(usesPermission.getName(), name)) { + if (usesPermission.getUsesPermissionFlags() != usesPermissionFlags) { + return input.error("Conflicting uses-permissions flags: " + + name + " in package: " + pkg.getPackageName() + " at: " + + parser.getPositionDescription()); + } else { + Slog.w(TAG, "Ignoring duplicate uses-permissions/uses-permissions-sdk-m: " + + name + " in package: " + pkg.getPackageName() + " at: " + + parser.getPositionDescription()); + } + found = true; + break; + } + } + + if (!found) { + pkg.addUsesPermission(new ParsedUsesPermissionImpl(name, usesPermissionFlags)); + } + return success; + } finally { + sa.recycle(); + } + } + + private ParseResult parseRequiredFeature(ParseInput input, Resources res, + AttributeSet attrs) { + final TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestRequiredFeature); + try { + final String featureName = sa.getString( + R.styleable.AndroidManifestRequiredFeature_name); + return TextUtils.isEmpty(featureName) + ? input.error("Feature name is missing from tag.") + : input.success(featureName); + } finally { + sa.recycle(); + } + } + + private ParseResult parseRequiredNotFeature(ParseInput input, Resources res, + AttributeSet attrs) { + final TypedArray sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestRequiredNotFeature); + try { + final String featureName = sa.getString( + R.styleable.AndroidManifestRequiredNotFeature_name); + return TextUtils.isEmpty(featureName) + ? input.error("Feature name is missing from tag.") + : input.success(featureName); + } finally { + sa.recycle(); + } + } + + private static ParseResult 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 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 parseFeatureGroup(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser) + throws IOException, XmlPullParserException { + FeatureGroupInfo group = new FeatureGroupInfo(); + ArrayList 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 declared are mandatory. + featureInfo.flags |= FeatureInfo.FLAG_REQUIRED; + features = ArrayUtils.add(features, featureInfo); + } else { + Slog.w(TAG, + "Unknown element under : " + innerTagName + + " at " + pkg.getBaseApkPath() + " " + + 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 parseUsesSdk(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser, int flags) + throws IOException, XmlPullParserException { + if (SDK_VERSION > 0) { + final boolean isApkInApex = (flags & PARSE_APK_IN_APEX) != 0; + TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestUsesSdk); + try { + int minVers = ParsingUtils.DEFAULT_MIN_SDK_VERSION; + String minCode = null; + boolean minAssigned = false; + int targetVers = ParsingUtils.DEFAULT_TARGET_SDK_VERSION; + String targetCode = null; + int maxVers = Integer.MAX_VALUE; + + TypedValue val = sa.peekValue(R.styleable.AndroidManifestUsesSdk_minSdkVersion); + if (val != null) { + if (val.type == TypedValue.TYPE_STRING && val.string != null) { + minCode = val.string.toString(); + minAssigned = !TextUtils.isEmpty(minCode); + } else { + // If it's not a string, it's an integer. + minVers = val.data; + minAssigned = true; + } + } + + val = sa.peekValue(R.styleable.AndroidManifestUsesSdk_targetSdkVersion); + if (val != null) { + if (val.type == TypedValue.TYPE_STRING && val.string != null) { + targetCode = val.string.toString(); + if (!minAssigned) { + minCode = targetCode; + } + } else { + // If it's not a string, it's an integer. + targetVers = val.data; + } + } else { + targetVers = minVers; + targetCode = minCode; + } + + if (isApkInApex) { + val = sa.peekValue(R.styleable.AndroidManifestUsesSdk_maxSdkVersion); + if (val != null) { + // maxSdkVersion only supports integer + maxVers = val.data; + } + } + + ParseResult targetSdkVersionResult = FrameworkParsingPackageUtils + .computeTargetSdkVersion(targetVers, targetCode, SDK_CODENAMES, input, + isApkInApex); + if (targetSdkVersionResult.isError()) { + return input.error(targetSdkVersionResult); + } + + int targetSdkVersion = targetSdkVersionResult.getResult(); + + ParseResult deferResult = + input.enableDeferredError(pkg.getPackageName(), targetSdkVersion); + if (deferResult.isError()) { + return input.error(deferResult); + } + + ParseResult minSdkVersionResult = FrameworkParsingPackageUtils + .computeMinSdkVersion(minVers, minCode, SDK_VERSION, SDK_CODENAMES, input); + if (minSdkVersionResult.isError()) { + return input.error(minSdkVersionResult); + } + + int minSdkVersion = minSdkVersionResult.getResult(); + + pkg.setMinSdkVersion(minSdkVersion) + .setTargetSdkVersion(targetSdkVersion); + if (isApkInApex) { + ParseResult maxSdkVersionResult = FrameworkParsingPackageUtils + .computeMaxSdkVersion(maxVers, SDK_VERSION, input); + if (maxSdkVersionResult.isError()) { + return input.error(maxSdkVersionResult); + } + int maxSdkVersion = maxSdkVersionResult.getResult(); + pkg.setMaxSdkVersion(maxSdkVersion); + } + + int type; + final int innerDepth = parser.getDepth(); + SparseIntArray minExtensionVersions = null; + 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")) { + if (minExtensionVersions == null) { + minExtensionVersions = new SparseIntArray(); + } + result = parseExtensionSdk(input, res, parser, minExtensionVersions); + XmlUtils.skipCurrentTag(parser); + } else { + result = ParsingUtils.unknownTag("", pkg, parser, input); + } + + if (result.isError()) { + return input.error(result); + } + } + pkg.setMinExtensionVersions(exactSizedCopyOfSparseArray(minExtensionVersions)); + } finally { + sa.recycle(); + } + } + return input.success(pkg); + } + + @Nullable + private static SparseIntArray exactSizedCopyOfSparseArray(@Nullable SparseIntArray input) { + if (input == null) { + return null; + } + SparseIntArray output = new SparseIntArray(input.size()); + for (int i = 0; i < input.size(); i++) { + output.put(input.keyAt(i), input.valueAt(i)); + } + return output; + } + + private static ParseResult parseExtensionSdk( + ParseInput input, Resources res, XmlResourceParser parser, + SparseIntArray minExtensionVersions) { + 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, + " must specify an sdkVersion >= 0"); + } + if (minVersion < 0) { + return input.error( + PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + " 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"); + } + minExtensionVersions.put(sdkVersion, minVersion); + return input.success(minExtensionVersions); + } + + private static ParseResult parseRestrictUpdateHash(int flags, ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser) { + if ((flags & 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 parseInstallConstraints(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser, Set allowlist) + throws IOException, XmlPullParserException { + return InstallConstraintsTagParser.parseInstallConstraints( + input, pkg, res, parser, allowlist); + } + + private static ParseResult 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 result = ParsedIntentInfoUtils.parseIntentInfo( + null /*className*/, pkg, res, parser, true /*allowGlobs*/, + true /*allowAutoVerify*/, input); + if (result.isError()) { + return input.error(result); + } + + IntentFilter intentInfo = result.getResult().getIntentFilter(); + + Uri data = null; + String dataType = null; + String host = null; + 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) + .path(IntentFilter.WILDCARD_PATH) + .build(); + } + if (numTypes == 1) { + dataType = intentInfo.getDataType(0); + // The dataType may have had the '/' removed for the dynamic mimeType feature. + // If we detect that case, we add the * back. + if (!dataType.contains("/")) { + dataType = dataType + "/*"; + } + if (data == null) { + data = new Uri.Builder() + .scheme("content") + .authority(IntentFilter.WILDCARD) + .path(IntentFilter.WILDCARD_PATH) + .build(); + } + } + 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.getNonConfigurationString( + R.styleable.AndroidManifestQueriesPackage_name, 0); + 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.getNonConfigurationString( + R.styleable.AndroidManifestQueriesProvider_authorities, 0); + 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 + * base APK manifest. + *

+ * 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 parseBaseApplication(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser, int flags, + boolean shouldSkipComponents) 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(" 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(" invalid android:name"); + } else if (outInfoName == null) { + return input.error("Empty class name in package " + packageName); + } + + pkg.setApplicationClassName(outInfoName); + } + + TypedValue labelValue = sa.peekValue(R.styleable.AndroidManifestApplication_label); + if (labelValue != null) { + pkg.setLabelResourceId(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.isBackupAllowed()) { + // 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 (DEBUG_BACKUP) { + Slog.v(TAG, "android:backupAgent = " + backupAgentName + + " from " + pkgName + "+" + backupAgent); + } + + pkg.setBackupAgentName(backupAgentName) + .setKillAfterRestoreAllowed(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 (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.setFullBackupContentResourceId(fullBackupContent); + } + if (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)); + } + + 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 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 processNameResult = ComponentParseUtils.buildProcessName( + pkgName, null /*defProc*/, pname, flags, mSeparateProcesses, input); + if (processNameResult.isError()) { + return input.error(processNameResult); + } + + String processName = processNameResult.getResult(); + pkg.setProcessName(processName); + + if (pkg.isSaveStateDisallowed()) { + // 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); + } + + pkg.setGwpAsanMode(sa.getInt(R.styleable.AndroidManifestApplication_gwpAsanMode, -1)); + pkg.setMemtagMode(sa.getInt(R.styleable.AndroidManifestApplication_memtagMode, -1)); + if (sa.hasValue(R.styleable.AndroidManifestApplication_nativeHeapZeroInitialized)) { + final boolean v = sa.getBoolean( + R.styleable.AndroidManifestApplication_nativeHeapZeroInitialized, false); + pkg.setNativeHeapZeroInitialized( + v ? ApplicationInfo.ZEROINIT_ENABLED : ApplicationInfo.ZEROINIT_DISABLED); + } + if (sa.hasValue( + R.styleable.AndroidManifestApplication_requestRawExternalStorageAccess)) { + pkg.setRequestRawExternalStorageAccess(sa.getBoolean(R.styleable + .AndroidManifestApplication_requestRawExternalStorageAccess, + false)); + } + if (sa.hasValue( + R.styleable.AndroidManifestApplication_requestForegroundServiceExemption)) { + pkg.setRequestForegroundServiceExemption(sa.getBoolean(R.styleable + .AndroidManifestApplication_requestForegroundServiceExemption, + false)); + } + final ParseResult> knownActivityEmbeddingCertsResult = + parseKnownActivityEmbeddingCerts(sa, res, + R.styleable.AndroidManifestApplication_knownActivityEmbeddingCerts, + input); + if (knownActivityEmbeddingCertsResult.isError()) { + return input.error(knownActivityEmbeddingCertsResult); + } else { + final Set knownActivityEmbeddingCerts = knownActivityEmbeddingCertsResult + .getResult(); + if (knownActivityEmbeddingCerts != null) { + pkg.setKnownActivityEmbeddingCerts(knownActivityEmbeddingCerts); + } + } + } 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": + if (shouldSkipComponents) { + continue; + } + ParseResult activityResult = + ParsedActivityUtils.parseActivityOrReceiver(mSeparateProcesses, pkg, + res, parser, flags, sUseRoundIcon, null /*defaultSplitName*/, + 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": + if (shouldSkipComponents) { + continue; + } + ParseResult serviceResult = + ParsedServiceUtils.parseService(mSeparateProcesses, pkg, res, parser, + flags, sUseRoundIcon, null /*defaultSplitName*/, + input); + if (serviceResult.isSuccess()) { + ParsedService service = serviceResult.getResult(); + hasServiceOrder |= (service.getOrder() != 0); + pkg.addService(service); + } + + result = serviceResult; + break; + case "provider": + if (shouldSkipComponents) { + continue; + } + ParseResult providerResult = + ParsedProviderUtils.parseProvider(mSeparateProcesses, pkg, res, parser, + flags, sUseRoundIcon, null /*defaultSplitName*/, + input); + if (providerResult.isSuccess()) { + pkg.addProvider(providerResult.getResult()); + } + + result = providerResult; + break; + case "activity-alias": + if (shouldSkipComponents) { + continue; + } + activityResult = ParsedActivityUtils.parseActivityAlias(pkg, res, + parser, sUseRoundIcon, null /*defaultSplitName*/, + input); + if (activityResult.isSuccess()) { + ParsedActivity activity = activityResult.getResult(); + hasActivityOrder |= (activity.getOrder() != 0); + pkg.addActivity(activity); + } + + result = activityResult; + break; + case "apex-system-service": + ParseResult systemServiceResult = + ParsedApexSystemServiceUtils.parseApexSystemService(res, + parser, input); + if (systemServiceResult.isSuccess()) { + ParsedApexSystemService systemService = + systemServiceResult.getResult(); + pkg.addApexSystemService(systemService); + } + + result = systemServiceResult; + break; + default: + result = parseBaseAppChildTag(input, tagName, pkg, res, parser, flags); + break; + } + + if (result.isError()) { + return input.error(result); + } + } + + if (TextUtils.isEmpty(pkg.getStaticSharedLibraryName()) && TextUtils.isEmpty( + pkg.getSdkLibraryName())) { + // Add a hidden app detail activity to normal apps which forwards user to App Details + // page. + ParseResult a = generateAppDetailsHiddenActivity(input, pkg); + if (a.isError()) { + // Error should be impossible here, as the only failure case as of SDK R is a + // string validation error on a constant ":app_details" string passed in by the + // parsing code itself. For this reason, this is just a hard failure instead of + // deferred. + return input.error(a); + } + + pkg.addActivity(a.getResult()); + } + + if (hasActivityOrder) { + pkg.sortActivities(); + } + if (hasReceiverOrder) { + pkg.sortReceivers(); + } + if (hasServiceOrder) { + pkg.sortServices(); + } + + afterParseBaseApplication(pkg); + + return input.success(pkg); + } + + // 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. + private void afterParseBaseApplication(ParsingPackage pkg) { + setMaxAspectRatio(pkg); + setMinAspectRatio(pkg); + setSupportsSizeChanges(pkg); + + pkg.setHasDomainUrls(hasDomainURLs(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 + .setBackupAllowed(bool(true, R.styleable.AndroidManifestApplication_allowBackup, sa)) + .setClearUserDataAllowed(bool(true, R.styleable.AndroidManifestApplication_allowClearUserData, sa)) + .setClearUserDataOnFailedRestoreAllowed(bool(true, R.styleable.AndroidManifestApplication_allowClearUserDataOnFailedRestore, sa)) + .setAllowNativeHeapPointerTagging(bool(true, R.styleable.AndroidManifestApplication_allowNativeHeapPointerTagging, sa)) + .setEnabled(bool(true, R.styleable.AndroidManifestApplication_enabled, sa)) + .setExtractNativeLibrariesRequested(bool(true, R.styleable.AndroidManifestApplication_extractNativeLibs, sa)) + .setDeclaredHavingCode(bool(true, R.styleable.AndroidManifestApplication_hasCode, sa)) + // Default false + .setTaskReparentingAllowed(bool(false, R.styleable.AndroidManifestApplication_allowTaskReparenting, sa)) + .setSaveStateDisallowed(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)) + .setUserDataFragile(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)) + .setRtlSupported(bool(false, R.styleable.AndroidManifestApplication_supportsRtl, sa)) + .setTestOnly(bool(false, R.styleable.AndroidManifestApplication_testOnly, sa)) + .setUseEmbeddedDex(bool(false, R.styleable.AndroidManifestApplication_useEmbeddedDex, sa)) + .setNonSdkApiRequested(bool(false, R.styleable.AndroidManifestApplication_usesNonSdkApi, sa)) + .setVmSafeMode(bool(false, R.styleable.AndroidManifestApplication_vmSafeMode, sa)) + .setAutoRevokePermissions(anInt(R.styleable.AndroidManifestApplication_autoRevokePermissions, sa)) + .setAttributionsAreUserVisible(bool(false, R.styleable.AndroidManifestApplication_attributionsAreUserVisible, sa)) + .setResetEnabledSettingsOnAppDataCleared(bool(false, + R.styleable.AndroidManifestApplication_resetEnabledSettingsOnAppDataCleared, + sa)) + .setOnBackInvokedCallbackEnabled(bool(false, R.styleable.AndroidManifestApplication_enableOnBackInvokedCallback, sa)) + // targetSdkVersion gated + .setAllowAudioPlaybackCapture(bool(targetSdk >= Build.VERSION_CODES.Q, R.styleable.AndroidManifestApplication_allowAudioPlaybackCapture, sa)) + .setHardwareAccelerated(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)) + .setCleartextTrafficAllowed(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 + .setBannerResourceId(resId(R.styleable.AndroidManifestApplication_banner, sa)) + .setDescriptionResourceId(resId(R.styleable.AndroidManifestApplication_description, sa)) + .setIconResourceId(resId(R.styleable.AndroidManifestApplication_icon, sa)) + .setLogoResourceId(resId(R.styleable.AndroidManifestApplication_logo, sa)) + .setNetworkSecurityConfigResourceId(resId(R.styleable.AndroidManifestApplication_networkSecurityConfig, sa)) + .setRoundIconResourceId(resId(R.styleable.AndroidManifestApplication_roundIcon, sa)) + .setThemeResourceId(resId(R.styleable.AndroidManifestApplication_theme, sa)) + .setDataExtractionRulesResourceId( + resId(R.styleable.AndroidManifestApplication_dataExtractionRules, sa)) + .setLocaleConfigResourceId(resId(R.styleable.AndroidManifestApplication_localeConfig, 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)) + .setAllowCrossUidActivitySwitchFromBelow(bool(true, R.styleable.AndroidManifestApplication_allowCrossUidActivitySwitchFromBelow, 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, boolean)}. + */ + 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) + final ParseResult metaDataResult = parseMetaData(pkg, null /*component*/, + res, parser, "", input); + if (metaDataResult.isSuccess() && metaDataResult.getResult() != null) { + pkg.setMetaData(metaDataResult.getResult().toBundle(pkg.getMetaData())); + } + return metaDataResult; + case "property": + final ParseResult propertyResult = parseMetaData(pkg, null /*component*/, + res, parser, "", input); + if (propertyResult.isSuccess()) { + pkg.addProperty(propertyResult.getResult()); + } + return propertyResult; + case "sdk-library": + return parseSdkLibrary(pkg, res, parser, input); + case "static-library": + return parseStaticLibrary(pkg, res, parser, input); + case "library": + return parseLibrary(pkg, res, parser, input); + case "uses-sdk-library": + return parseUsesSdkLibrary(input, pkg, res, parser); + case "uses-static-library": + return parseUsesStaticLibrary(input, pkg, res, parser); + case "uses-library": + return parseUsesLibrary(input, pkg, res, parser); + case "uses-native-library": + return parseUsesNativeLibrary(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("", pkg, parser, input); + } + } + + @NonNull + private static ParseResult parseSdkLibrary( + ParsingPackage pkg, Resources res, + XmlResourceParser parser, ParseInput input) { + TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestSdkLibrary); + try { + // Note: don't allow this value to be a reference to a resource that may change. + String lname = sa.getNonResourceString( + R.styleable.AndroidManifestSdkLibrary_name); + final int versionMajor = sa.getInt( + R.styleable.AndroidManifestSdkLibrary_versionMajor, + -1); + + // Fail if malformed. + if (lname == null || versionMajor < 0) { + return input.error("Bad sdk-library declaration name: " + lname + + " version: " + versionMajor); + } else if (pkg.getSharedUserId() != null) { + return input.error( + PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID, + "sharedUserId not allowed in SDK library" + ); + } else if (pkg.getSdkLibraryName() != null) { + return input.error("Multiple SDKs for package " + + pkg.getPackageName()); + } + + return input.success(pkg.setSdkLibraryName(lname.intern()) + .setSdkLibVersionMajor(versionMajor) + .setSdkLibrary(true)); + } finally { + sa.recycle(); + } + } + + @NonNull + private static ParseResult 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.getStaticSharedLibraryName() != null) { + return input.error("Multiple static-shared libs for package " + + pkg.getPackageName()); + } + + return input.success(pkg.setStaticSharedLibraryName(lname.intern()) + .setStaticSharedLibraryVersion( + PackageInfo.composeLongVersionCode(versionMajor, version)) + .setStaticSharedLibrary(true)); + } finally { + sa.recycle(); + } + } + + @NonNull + private static ParseResult 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 parseUsesSdkLibrary(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser) + throws XmlPullParserException, IOException { + TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestUsesSdkLibrary); + try { + // Note: don't allow this value to be a reference to a resource that may change. + String lname = sa.getNonResourceString( + R.styleable.AndroidManifestUsesSdkLibrary_name); + final int versionMajor = sa.getInt( + R.styleable.AndroidManifestUsesSdkLibrary_versionMajor, -1); + String certSha256Digest = sa.getNonResourceString(R.styleable + .AndroidManifestUsesSdkLibrary_certDigest); + boolean optional = + sa.getBoolean(R.styleable.AndroidManifestUsesSdkLibrary_optional, false); + + // Since an APK providing a static shared lib can only provide the lib - fail if + // malformed + if (lname == null || versionMajor < 0 || certSha256Digest == null) { + return input.error("Bad uses-sdk-library declaration name: " + lname + + " version: " + versionMajor + " certDigest" + certSha256Digest); + } + + // Can depend only on one version of the same library + List usesSdkLibraries = pkg.getUsesSdkLibraries(); + if (usesSdkLibraries.contains(lname)) { + return input.error( + "Depending on multiple versions of SDK 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(); + + if ("".equals(certSha256Digest)) { + // Test-only uses-sdk-library empty certificate digest override. + certSha256Digest = SystemProperties.get( + "debug.pm.uses_sdk_library_default_cert_digest", ""); + // Validate the overridden digest. + try { + HexEncoding.decode(certSha256Digest, false); + } catch (IllegalArgumentException e) { + certSha256Digest = ""; + } + } + + ParseResult certResult = parseAdditionalCertificates(input, res, parser); + if (certResult.isError()) { + return input.error(certResult); + } + String[] 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.addUsesSdkLibrary(lname, versionMajor, certSha256Digests, optional)); + } finally { + sa.recycle(); + } + } + + @NonNull + private static ParseResult 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 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 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, version, certSha256Digests)); + } finally { + sa.recycle(); + } + } + + @NonNull + private static ParseResult 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 parseUsesNativeLibrary(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser) { + TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestUsesNativeLibrary); + try { + // Note: don't allow this value to be a reference to a resource + // that may change. + String lname = sa.getNonResourceString( + R.styleable.AndroidManifestUsesNativeLibrary_name); + boolean req = sa.getBoolean(R.styleable.AndroidManifestUsesNativeLibrary_required, + true); + + if (lname != null) { + if (req) { + // Upgrade to treat as stronger constraint + pkg.addUsesNativeLibrary(lname) + .removeUsesOptionalNativeLibrary(lname); + } else { + // Ignore if someone already defined as required + if (!ArrayUtils.contains(pkg.getUsesNativeLibraries(), lname)) { + pkg.addUsesOptionalNativeLibrary(lname); + } + } + } + + return input.success(pkg); + } finally { + sa.recycle(); + } + } + + @NonNull + private static ParseResult parseProcesses(ParseInput input, ParsingPackage pkg, + Resources res, XmlResourceParser parser, String[] separateProcesses, int flags) + throws IOException, XmlPullParserException { + ParseResult> 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 parseProfileable(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser) { + TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestProfileable); + try { + ParsingPackage newPkg = pkg.setProfileableByShell(pkg.isProfileableByShell() + || bool(false, R.styleable.AndroidManifestProfileable_shell, sa)); + return input.success(newPkg.setProfileable(newPkg.isProfileable() + && bool(true, R.styleable.AndroidManifestProfileable_enabled, sa))); + } finally { + sa.recycle(); + } + } + + private static ParseResult 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 generateAppDetailsHiddenActivity(ParseInput input, + ParsingPackage pkg) { + String packageName = pkg.getPackageName(); + ParseResult result = ComponentParseUtils.buildTaskAffinityName( + packageName, packageName, ":app_details", input); + if (result.isError()) { + return input.error(result); + } + + String taskAffinity = result.getResult(); + + // Build custom App Details activity info instead of parsing it from xml + return input.success(ParsedActivityImpl.makeAppDetailsActivity(packageName, + pkg.getProcessName(), pkg.getUiOptions(), taskAffinity, + pkg.isHardwareAccelerated())); + } + + /** + * Check if one of the IntentFilter as both actions DEFAULT / VIEW and a HTTP/HTTPS data URI + * + * This is distinct from any of the functionality of app links domain verification, and cannot + * be converted to remain backwards compatible. It's possible the presence of this flag does + * not indicate a valid package for domain verification. + */ + private static boolean hasDomainURLs(ParsingPackage pkg) { + final List activities = pkg.getActivities(); + final int activitiesSize = activities.size(); + for (int index = 0; index < activitiesSize; index++) { + ParsedActivity activity = activities.get(index); + List filters = activity.getIntents(); + final int filtersSize = filters.size(); + for (int filtersIndex = 0; filtersIndex < filtersSize; filtersIndex++) { + IntentFilter aii = filters.get(filtersIndex).getIntentFilter(); + 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 ? 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(METADATA_MAX_ASPECT_RATIO)) { + maxAspectRatio = appMetaData.getFloat(METADATA_MAX_ASPECT_RATIO, maxAspectRatio); + } + } + + List 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() != ASPECT_RATIO_NOT_SET) { + 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() + .getFloat(METADATA_MAX_ASPECT_RATIO, maxAspectRatio); + + ComponentMutateUtils.setMaxAspectRatio(activity, 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) { + // Use the application max aspect ration as default if set. + final float minAspectRatio = pkg.getMinAspectRatio(); + + List activities = pkg.getActivities(); + int activitiesSize = activities.size(); + for (int index = 0; index < activitiesSize; index++) { + ParsedActivity activity = activities.get(index); + if (activity.getMinAspectRatio() == ASPECT_RATIO_NOT_SET) { + ComponentMutateUtils.setMinAspectRatio(activity, activity.getResizeMode(), + minAspectRatio); + } + } + } + + private void setSupportsSizeChanges(ParsingPackage pkg) { + final Bundle appMetaData = pkg.getMetaData(); + final boolean supportsSizeChanges = appMetaData != null + && appMetaData.getBoolean(METADATA_SUPPORTS_SIZE_CHANGES, false); + + List activities = pkg.getActivities(); + int activitiesSize = activities.size(); + for (int index = 0; index < activitiesSize; index++) { + ParsedActivity activity = activities.get(index); + if (supportsSizeChanges || activity.getMetaData() + .getBoolean(METADATA_SUPPORTS_SIZE_CHANGES, false)) { + ComponentMutateUtils.setSupportsSizeChanges(activity, true); + } + } + } + + private static ParseResult 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(" does not specify a target package"); + } else if (priority < 0 || priority > 9999) { + return input.error(" 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 (!FrameworkParsingPackageUtils.checkRequiredSystemProperties(propName, propValue)) { + String message = "Skipping target and overlay pair " + target + " and " + + pkg.getBaseApkPath() + + ": overlay ignored due to required system property: " + + propName + " with value: " + propValue; + Slog.i(TAG, message); + return input.skip(message); + } + + return input.success(pkg.setResourceOverlay(true) + .setOverlayTarget(target) + .setOverlayPriority(priority) + .setOverlayTargetOverlayableName( + 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 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 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 + .setSmallScreensSupported( + anInt(1, R.styleable.AndroidManifestSupportsScreens_smallScreens, sa)) + .setNormalScreensSupported( + anInt(1, R.styleable.AndroidManifestSupportsScreens_normalScreens, sa)) + .setLargeScreensSupported( + anInt(1, R.styleable.AndroidManifestSupportsScreens_largeScreens, sa)) + .setExtraLargeScreensSupported( + 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 parseInstrumentation(ParseInput input, + ParsingPackage pkg, Resources res, XmlResourceParser parser) + throws XmlPullParserException, IOException { + ParseResult result = ParsedInstrumentationUtils.parseInstrumentation( + pkg, res, parser, sUseRoundIcon, input); + if (result.isError()) { + return input.error(result); + } + return input.success(pkg.addInstrumentation(result.getResult())); + } + + private static ParseResult 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)) { + pkg.addOriginalPackage(orig); + } + return input.success(pkg); + } finally { + sa.recycle(); + } + } + + private static ParseResult 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 convertCompatPermissions(ParsingPackage pkg) { + for (int i = 0, size = CompatibilityPermissionInfo.COMPAT_PERMS.length; i < size; i++) { + final CompatibilityPermissionInfo info = CompatibilityPermissionInfo.COMPAT_PERMS[i]; + if (pkg.getTargetSdkVersion() >= info.getSdkVersion()) { + break; + } + if (!pkg.getRequestedPermissions().contains(info.getName())) { + pkg.addImplicitPermission(info.getName()); + } + } + } + + private void convertSplitPermissions(ParsingPackage pkg) { + final int listSize = mSplitPermissionInfos.size(); + for (int is = 0; is < listSize; is++) { + final PermissionManager.SplitPermissionInfo spi = mSplitPermissionInfos.get(is); + Set requestedPermissions = pkg.getRequestedPermissions(); + if (pkg.getTargetSdkVersion() >= spi.getTargetSdk() + || !requestedPermissions.contains(spi.getSplitPermission())) { + continue; + } + final List newPerms = spi.getNewPermissions(); + for (int in = 0; in < newPerms.size(); in++) { + final String perm = newPerms.get(in); + if (!requestedPermissions.contains(perm)) { + pkg.addImplicitPermission(perm); + } + } + } + } + + /** + * 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 activities = pkg.getActivities(); + int activitiesSize = activities.size(); + for (int index = 0; index < activitiesSize; index++) { + ParsedActivity activity = activities.get(index); + ComponentMutateUtils.setResizeMode(activity, RESIZE_MODE_UNRESIZEABLE); + ComponentMutateUtils.setExactFlags(activity, + activity.getFlags() & ~FLAG_SUPPORTS_PICTURE_IN_PICTURE); + } + } + + /** + * Parse a meta data defined on the enclosing tag. + *

Meta data can be defined by either <meta-data> or <property> elements. + */ + public static ParseResult parseMetaData(ParsingPackage pkg, ParsedComponent component, + Resources res, XmlResourceParser parser, String tagName, ParseInput input) { + TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestMetaData); + try { + final Property property; + final String name = TextUtils.safeIntern( + nonConfigString(0, R.styleable.AndroidManifestMetaData_name, sa)); + if (name == null) { + return input.error(tagName + " requires an android:name attribute"); + } + + final String packageName = pkg.getPackageName(); + final String className = component != null ? component.getName() : null; + TypedValue v = sa.peekValue(R.styleable.AndroidManifestMetaData_resource); + if (v != null && v.resourceId != 0) { + property = new Property(name, v.resourceId, true, packageName, className); + } else { + v = sa.peekValue(R.styleable.AndroidManifestMetaData_value); + if (v != null) { + if (v.type == TypedValue.TYPE_STRING) { + final CharSequence cs = v.coerceToString(); + final String stringValue = cs != null ? cs.toString() : null; + property = new Property(name, stringValue, packageName, className); + } else if (v.type == TypedValue.TYPE_INT_BOOLEAN) { + property = new Property(name, v.data != 0, packageName, className); + } else if (v.type >= TypedValue.TYPE_FIRST_INT + && v.type <= TypedValue.TYPE_LAST_INT) { + property = new Property(name, v.data, false, packageName, className); + } else if (v.type == TypedValue.TYPE_FLOAT) { + property = new Property(name, v.getFloat(), packageName, className); + } else { + if (!RIGID_PARSER) { + Slog.w(TAG, + tagName + " only supports string, integer, float, color, " + + "boolean, and resource reference types: " + + parser.getName() + " at " + + pkg.getBaseApkPath() + " " + + parser.getPositionDescription()); + property = null; + } else { + return input.error(tagName + " only supports string, integer, float, " + + "color, boolean, and resource reference types"); + } + } + } else { + return input.error(tagName + " requires an android:value " + + "or android:resource attribute"); + } + } + return input.success(property); + } 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. + * + * TODO(b/155513789): Remove this in favor of collecting certificates during the original parse + * call if requested. Leaving this as an optional method for the caller means we have to + * construct a placeholder ParseInput. + */ + @CheckResult + public static ParseResult getSigningDetails(ParseInput input, + ParsedPackage pkg, boolean skipVerify) { + return getSigningDetails(input, pkg.getBaseApkPath(), pkg.isStaticSharedLibrary(), + pkg.getTargetSdkVersion(), pkg.getSplitCodePaths(), skipVerify); + } + + @CheckResult + private static ParseResult getSigningDetails(ParseInput input, + ParsingPackage pkg, boolean skipVerify) { + return getSigningDetails(input, pkg.getBaseApkPath(), pkg.isStaticSharedLibrary(), + pkg.getTargetSdkVersion(), pkg.getSplitCodePaths(), skipVerify); + } + + /** + * Collect certificates from all the APKs described in the given package. Also asserts that + * all APK contents are signed correctly and consistently. + * + * TODO(b/155513789): Remove this in favor of collecting certificates during the original parse + * call if requested. Leaving this as an optional method for the caller means we have to + * construct a dummy ParseInput. + */ + @CheckResult + public static ParseResult getSigningDetails(ParseInput input, + String baseApkPath, boolean isStaticSharedLibrary, int targetSdkVersion, + String[] splitCodePaths, boolean skipVerify) { + SigningDetails signingDetails = SigningDetails.UNKNOWN; + + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "collectCertificates"); + try { + ParseResult result = getSigningDetails( + input, + baseApkPath, + skipVerify, + isStaticSharedLibrary, + signingDetails, + targetSdkVersion + ); + if (result.isError()) { + return input.error(result); + } + + signingDetails = result.getResult(); + final File frameworkRes = new File(Environment.getRootDirectory(), + "framework/framework-res.apk"); + boolean isFrameworkResSplit = frameworkRes.getAbsolutePath() + .equals(baseApkPath); + if (!ArrayUtils.isEmpty(splitCodePaths) && !isFrameworkResSplit) { + for (int i = 0; i < splitCodePaths.length; i++) { + result = getSigningDetails( + input, + splitCodePaths[i], + skipVerify, + isStaticSharedLibrary, + signingDetails, + targetSdkVersion + ); + if (result.isError()) { + return input.error(result); + } + } + } + return result; + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + } + + @CheckResult + public static ParseResult getSigningDetails(ParseInput input, + String baseCodePath, boolean skipVerify, boolean isStaticSharedLibrary, + @NonNull SigningDetails existingSigningDetails, int targetSdk) { + int minSignatureScheme = ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk( + targetSdk); + if (isStaticSharedLibrary) { + // must use v2 signing scheme + minSignatureScheme = SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2; + } + final ParseResult verified; + if (skipVerify) { + // systemDir APKs are already trusted, save time by not verifying + verified = ApkSignatureVerifier.unsafeGetCertsWithoutVerification(input, baseCodePath, + minSignatureScheme, skipVerify); + } else { + verified = ApkSignatureVerifier.verify(input, baseCodePath, minSignatureScheme); + } + + if (verified.isError()) { + return input.error(verified); + } + + // 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, verified.getResult())) { + return input.error(INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, + baseCodePath + " has mismatched certificates"); + } + + return input.success(existingSigningDetails); + } + } + + /** + * @hide + */ + public static void setCompatibilityModeEnabled(boolean compatibilityModeEnabled) { + sCompatibilityModeEnabled = compatibilityModeEnabled; + } + + /** + * @hide + */ + public static void readConfigUseRoundIcon(Resources r) { + if (r != null) { + sUseRoundIcon = r.getBoolean(com.android.internal.R.bool.config_useRoundIcon); + return; + } + + final ApplicationInfo androidAppInfo; + try { + androidAppInfo = ActivityThread.getPackageManager().getApplicationInfo( + "android", 0 /* flags */, + UserHandle.myUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + final Resources systemResources = Resources.getSystem(); + + // Create in-flight as this overlayable resource is only used when config changes + final Resources overlayableRes = ResourcesManager.getInstance().getResources( + null /* activityToken */, + null /* resDir */, + null /* splitResDirs */, + androidAppInfo.resourceDirs, + androidAppInfo.overlayPaths, + androidAppInfo.sharedLibraryFiles, + null /* overrideDisplayId */, + null /* overrideConfig */, + systemResources.getCompatibilityInfo(), + systemResources.getClassLoader(), + null /* loaders */); + + sUseRoundIcon = overlayableRes.getBoolean(com.android.internal.R.bool.config_useRoundIcon); + } + + /* + 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 anInteger(int defaultValue, @StyleableRes int attribute, TypedArray sa) { + return sa.getInteger(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); + } + + /** + * Writes the keyset mapping to the provided package. {@code null} mappings are permitted. + */ + public static void writeKeySetMapping(@NonNull Parcel dest, + @NonNull Map> keySetMapping) { + if (keySetMapping == null) { + dest.writeInt(-1); + return; + } + + final int N = keySetMapping.size(); + dest.writeInt(N); + + for (String key : keySetMapping.keySet()) { + dest.writeString(key); + ArraySet keys = keySetMapping.get(key); + if (keys == null) { + dest.writeInt(-1); + continue; + } + + final int M = keys.size(); + dest.writeInt(M); + for (int j = 0; j < M; j++) { + dest.writeSerializable(keys.valueAt(j)); + } + } + } + + /** + * Reads a keyset mapping from the given parcel at the given data position. May return + * {@code null} if the serialized mapping was {@code null}. + */ + @NonNull + public static ArrayMap> readKeySetMapping(@NonNull Parcel in) { + final int N = in.readInt(); + if (N == -1) { + return null; + } + + ArrayMap> keySetMapping = new ArrayMap<>(); + for (int i = 0; i < N; ++i) { + String key = in.readString(); + final int M = in.readInt(); + if (M == -1) { + keySetMapping.put(key, null); + continue; + } + + ArraySet keys = new ArraySet<>(M); + for (int j = 0; j < M; ++j) { + PublicKey pk = (PublicKey) in.readSerializable(java.security.PublicKey.class.getClassLoader(), java.security.PublicKey.class); + keys.add(pk); + } + + keySetMapping.put(key, keys); + } + + return keySetMapping; + } + + /** + * Callback interface for retrieving information that may be needed while parsing + * a package. + */ + public interface Callback { + boolean hasFeature(String feature); + + ParsingPackage startParsingPackage(@NonNull String packageName, + @NonNull String baseApkPath, @NonNull String path, + @NonNull TypedArray manifestArray, boolean isCoreApp); + + @NonNull Set getHiddenApiWhitelistedApps(); + + @NonNull Set getInstallConstraintsAllowlist(); + } +} diff --git a/aosp/frameworks/base/services/core/java/com/android/server/BinaryTransparencyService.java b/aosp/frameworks/base/services/core/java/com/android/server/BinaryTransparencyService.java new file mode 100644 index 0000000000000000000000000000000000000000..6095d586f2ee83d5464b29ac07c08856e975d22e --- /dev/null +++ b/aosp/frameworks/base/services/core/java/com/android/server/BinaryTransparencyService.java @@ -0,0 +1,1711 @@ +/* + * Copyright (C) 2022 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 com.android.server; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.apex.ApexInfo; +import android.apex.IApexService; +import android.app.compat.CompatChanges; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.compat.annotation.ChangeId; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApexStagedEvent; +import android.content.pm.ApplicationInfo; +import android.content.pm.IBackgroundInstallControlService; +import android.content.pm.IPackageManagerNative; +import android.content.pm.IStagedApexObserver; +import android.content.pm.InstallSourceInfo; +import android.content.pm.ModuleInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ParceledListSlice; +import android.content.pm.SharedLibraryInfo; +import android.content.pm.Signature; +import android.content.pm.SigningDetails; +import android.content.pm.SigningInfo; +import android.content.pm.parsing.result.ParseInput; +import android.content.pm.parsing.result.ParseResult; +import android.content.pm.parsing.result.ParseTypeImpl; +import android.hardware.biometrics.SensorProperties; +import android.hardware.biometrics.SensorProperties.ComponentInfo; +import android.hardware.face.FaceManager; +import android.hardware.face.FaceSensorProperties; +import android.hardware.face.FaceSensorPropertiesInternal; +import android.hardware.face.IFaceAuthenticatorsRegisteredCallback; +import android.hardware.fingerprint.FingerprintManager; +import android.hardware.fingerprint.FingerprintSensorProperties; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback; +import android.net.Uri; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.ServiceManager; +import android.os.ShellCallback; +import android.os.ShellCommand; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.provider.DeviceConfig; +import android.text.TextUtils; +import android.util.PackageUtils; +import android.util.Slog; +import android.util.apk.ApkSignatureVerifier; +import android.util.apk.ApkSigningBlockUtils; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.IBinaryTransparencyService; +import com.android.internal.util.FrameworkStatsLog; +import com.android.modules.expresslog.Histogram; +import com.android.server.pm.ApexManager; +import com.android.server.pm.pkg.AndroidPackage; +import com.android.server.pm.pkg.AndroidPackageSplit; +import com.android.server.pm.pkg.PackageState; + +import libcore.util.HexEncoding; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +/** + * @hide + */ +public class BinaryTransparencyService extends SystemService { + private static final String TAG = "TransparencyService"; + + @VisibleForTesting + static final String VBMETA_DIGEST_UNINITIALIZED = "vbmeta-digest-uninitialized"; + @VisibleForTesting + static final String VBMETA_DIGEST_UNAVAILABLE = "vbmeta-digest-unavailable"; + @VisibleForTesting + static final String SYSPROP_NAME_VBETA_DIGEST = "ro.boot.vbmeta.digest"; + + @VisibleForTesting + static final String BINARY_HASH_ERROR = "SHA256HashError"; + + static final long RECORD_MEASUREMENTS_COOLDOWN_MS = 24 * 60 * 60 * 1000; + + static final String APEX_PRELOAD_LOCATION_ERROR = "could-not-be-determined"; + + // Copy from the atom. Consistent for both ApexInfoGathered and MobileBundledAppInfoGathered. + static final int DIGEST_ALGORITHM_UNKNOWN = 0; + static final int DIGEST_ALGORITHM_CHUNKED_SHA256 = 1; + static final int DIGEST_ALGORITHM_CHUNKED_SHA512 = 2; + static final int DIGEST_ALGORITHM_VERITY_CHUNKED_SHA256 = 3; + static final int DIGEST_ALGORITHM_SHA256 = 4; + + // used for indicating any type of error during MBA measurement + static final int MBA_STATUS_ERROR = 0; + // used for indicating factory condition preloads + static final int MBA_STATUS_PRELOADED = 1; + // used for indicating preloaded apps that are updated + static final int MBA_STATUS_UPDATED_PRELOAD = 2; + // used for indicating newly installed MBAs + static final int MBA_STATUS_NEW_INSTALL = 3; + // used for indicating newly installed MBAs that are updated (but unused currently) + static final int MBA_STATUS_UPDATED_NEW_INSTALL = 4; + + @VisibleForTesting + static final String KEY_ENABLE_BIOMETRIC_PROPERTY_VERIFICATION = + "enable_biometric_property_verification"; + + private static final boolean DEBUG = false; // toggle this for local debug + + private static final Histogram digestAllPackagesLatency = new Histogram( + "binary_transparency.value_digest_all_packages_latency_uniform", + new Histogram.UniformOptions(50, 0, 500)); + + private final Context mContext; + private String mVbmetaDigest; + // the system time (in ms) the last measurement was taken + private long mMeasurementsLastRecordedMs; + private PackageManagerInternal mPackageManagerInternal; + private BiometricLogger mBiometricLogger; + + /** + * Guards whether or not measurements of MBA to be performed. When this change is enabled, + * measurements of MBAs are performed. But when it is disabled, only measurements of APEX + * and modules are done. + */ + @ChangeId + public static final long LOG_MBA_INFO = 245692487L; + + final class BinaryTransparencyServiceImpl extends IBinaryTransparencyService.Stub { + + @Override + public String getSignedImageInfo() { + return mVbmetaDigest; + } + + /** + * A helper function to compute the SHA256 digest of APK package signer. + * @param signingInfo The signingInfo of a package, usually {@link PackageInfo#signingInfo}. + * @return an array of {@code String} representing hex encoded string of the + * SHA256 digest of APK signer(s). The number of signers will be reflected by the + * size of the array. + * However, {@code null} is returned if there is any error. + */ + private String[] computePackageSignerSha256Digests(@Nullable SigningInfo signingInfo) { + if (signingInfo == null) { + Slog.e(TAG, "signingInfo is null"); + return null; + } + + Signature[] packageSigners = signingInfo.getApkContentsSigners(); + List resultList = new ArrayList<>(); + for (Signature packageSigner : packageSigners) { + byte[] digest = PackageUtils.computeSha256DigestBytes(packageSigner.toByteArray()); + String digestHexString = HexEncoding.encodeToString(digest, false); + resultList.add(digestHexString); + } + return resultList.toArray(new String[1]); + } + + /* + * Perform basic measurement (i.e. content digest) on a given app, including the split APKs. + * + * @param packageState The package to be measured. + * @param mbaStatus Assign this value of MBA status to the returned elements. + * @return a @{@code List} + */ + private @NonNull List collectAppInfo( + PackageState packageState, int mbaStatus) { + // compute content digest + if (DEBUG) { + Slog.d(TAG, "Computing content digest for " + packageState.getPackageName() + " at " + + packageState.getPath()); + } + + var results = new ArrayList(); + + // Same attributes across base and splits. + String packageName = packageState.getPackageName(); + long versionCode = packageState.getVersionCode(); + String[] signerDigests = + computePackageSignerSha256Digests(packageState.getSigningInfo()); + + AndroidPackage pkg = packageState.getAndroidPackage(); + for (AndroidPackageSplit split : pkg.getSplits()) { + var appInfo = new IBinaryTransparencyService.AppInfo(); + appInfo.packageName = packageName; + appInfo.longVersion = versionCode; + appInfo.splitName = split.getName(); // base's split name is null + // Signer digests are consistent between splits, guaranteed by Package Manager. + appInfo.signerDigests = signerDigests; + appInfo.mbaStatus = mbaStatus; + + // Only digest and split name are different between splits. + Digest digest = measureApk(split.getPath()); + appInfo.digest = digest.value(); + appInfo.digestAlgorithm = digest.algorithm(); + + results.add(appInfo); + } + + // InstallSourceInfo is only available per package name, so store it only on the base + // APK. It's not current currently available in PackageState (there's a TODO), to we + // need to extract manually with another call. + // + // Base APK is already the 0-th split from getSplits() and can't be null. + AppInfo base = results.get(0); + InstallSourceInfo installSourceInfo = getInstallSourceInfo( + packageState.getPackageName()); + if (installSourceInfo != null) { + base.initiator = installSourceInfo.getInitiatingPackageName(); + SigningInfo initiatorSignerInfo = + installSourceInfo.getInitiatingPackageSigningInfo(); + if (initiatorSignerInfo != null) { + base.initiatorSignerDigests = + computePackageSignerSha256Digests(initiatorSignerInfo); + } + base.installer = installSourceInfo.getInstallingPackageName(); + base.originator = installSourceInfo.getOriginatingPackageName(); + } + + return results; + } + + /** + * Perform basic measurement (i.e. content digest) on a given APK. + * + * @param apkPath The APK (or APEX, since it's also an APK) file to be measured. + * @return a {@link #Digest} with preferred digest algorithm type and the value. + */ + private @Nullable Digest measureApk(@NonNull String apkPath) { + // compute content digest + Map contentDigests = computeApkContentDigest(apkPath); + if (contentDigests == null) { + Slog.d(TAG, "Failed to compute content digest for " + apkPath); + } else { + // in this iteration, we'll be supporting only 2 types of digests: + // CHUNKED_SHA256 and CHUNKED_SHA512. + // And only one of them will be available per package. + if (contentDigests.containsKey( + ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256)) { + return new Digest( + DIGEST_ALGORITHM_CHUNKED_SHA256, + contentDigests.get(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256)); + } else if (contentDigests.containsKey( + ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512)) { + return new Digest( + DIGEST_ALGORITHM_CHUNKED_SHA512, + contentDigests.get(ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512)); + } + } + // When something went wrong, fall back to simple sha256. + byte[] digest = PackageUtils.computeSha256DigestForLargeFileAsBytes(apkPath, + PackageUtils.createLargeFileBuffer()); + return new Digest(DIGEST_ALGORITHM_SHA256, digest); + } + + + /** + * Measures and records digests for *all* covered binaries/packages. + * + * This method will be called in a Job scheduled to take measurements periodically. If the + * last measurement was performaned recently (less than RECORD_MEASUREMENT_COOLDOWN_MS + * ago), the measurement and recording will be skipped. + * + * Packages that are covered so far are: + * - all APEXs (introduced in Android T) + * - all mainline modules (introduced in Android T) + * - all preloaded apps and their update(s) (new in Android U) + * - dynamically installed mobile bundled apps (MBAs) (new in Android U) + */ + public void recordMeasurementsForAllPackages() { + // check if we should measure and record + long currentTimeMs = System.currentTimeMillis(); + if ((currentTimeMs - mMeasurementsLastRecordedMs) < RECORD_MEASUREMENTS_COOLDOWN_MS) { + Slog.d(TAG, "Skip measurement since the last measurement was only taken at " + + mMeasurementsLastRecordedMs + " within the cooldown period"); + return; + } + Slog.d(TAG, "Measurement was last taken at " + mMeasurementsLastRecordedMs + + " and is now updated to: " + currentTimeMs); + mMeasurementsLastRecordedMs = currentTimeMs; + + Bundle packagesMeasured = new Bundle(); + + // measure all APEXs first + if (DEBUG) { + Slog.d(TAG, "Measuring APEXs..."); + } + List allApexInfo = collectAllApexInfo( + /* includeTestOnly */ false); + for (IBinaryTransparencyService.ApexInfo apexInfo : allApexInfo) { + packagesMeasured.putBoolean(apexInfo.packageName, true); + + recordApexInfo(apexInfo); + } + if (DEBUG) { + Slog.d(TAG, "Measured " + packagesMeasured.size() + + " packages after considering APEXs."); + } + + // proceed with all preloaded apps + List allUpdatedPreloadInfo = + collectAllUpdatedPreloadInfo(packagesMeasured); + for (IBinaryTransparencyService.AppInfo appInfo : allUpdatedPreloadInfo) { + packagesMeasured.putBoolean(appInfo.packageName, true); + writeAppInfoToLog(appInfo); + } + if (DEBUG) { + Slog.d(TAG, "Measured " + packagesMeasured.size() + + " packages after considering preloads"); + } + + if (CompatChanges.isChangeEnabled(LOG_MBA_INFO)) { + // lastly measure all newly installed MBAs + List allMbaInfo = + collectAllSilentInstalledMbaInfo(packagesMeasured); + for (IBinaryTransparencyService.AppInfo appInfo : allMbaInfo) { + packagesMeasured.putBoolean(appInfo.packageName, true); + writeAppInfoToLog(appInfo); + } + } + long timeSpentMeasuring = System.currentTimeMillis() - currentTimeMs; + digestAllPackagesLatency.logSample(timeSpentMeasuring); + if (DEBUG) { + Slog.d(TAG, "Measured " + packagesMeasured.size() + + " packages altogether in " + timeSpentMeasuring + "ms"); + } + } + + @Override + public List collectAllApexInfo( + boolean includeTestOnly) { + var results = new ArrayList(); + for (PackageInfo packageInfo : getCurrentInstalledApexs()) { + PackageState packageState = mPackageManagerInternal.getPackageStateInternal( + packageInfo.packageName); + if (packageState == null) { + Slog.w(TAG, "Package state is unavailable, ignoring the APEX " + + packageInfo.packageName); + continue; + } + + AndroidPackage pkg = packageState.getAndroidPackage(); + if (pkg == null) { + Slog.w(TAG, "Skipping the missing APK in " + pkg.getPath()); + continue; + } + Digest apexChecksum = measureApk(pkg.getPath()); + if (apexChecksum == null) { + Slog.w(TAG, "Skipping the missing APEX in " + pkg.getPath()); + continue; + } + + var apexInfo = new IBinaryTransparencyService.ApexInfo(); + apexInfo.packageName = packageState.getPackageName(); + apexInfo.longVersion = packageState.getVersionCode(); + apexInfo.digest = apexChecksum.value(); + apexInfo.digestAlgorithm = apexChecksum.algorithm(); + apexInfo.signerDigests = + computePackageSignerSha256Digests(packageState.getSigningInfo()); + + if (includeTestOnly) { + apexInfo.moduleName = apexPackageNameToModuleName( + packageState.getPackageName()); + } + + results.add(apexInfo); + } + return results; + } + + @Override + public List collectAllUpdatedPreloadInfo( + Bundle packagesToSkip) { + final var results = new ArrayList(); + + PackageManager pm = mContext.getPackageManager(); + mPackageManagerInternal.forEachPackageState((packageState) -> { + if (!packageState.isUpdatedSystemApp()) { + return; + } + if (packagesToSkip.containsKey(packageState.getPackageName())) { + return; + } + + Slog.d(TAG, "Preload " + packageState.getPackageName() + " at " + + packageState.getPath() + " has likely been updated."); + + List resultsForApp = collectAppInfo( + packageState, MBA_STATUS_UPDATED_PRELOAD); + results.addAll(resultsForApp); + }); + return results; + } + + @Override + public List collectAllSilentInstalledMbaInfo( + Bundle packagesToSkip) { + var results = new ArrayList(); + for (PackageInfo packageInfo : getNewlyInstalledMbas()) { + if (packagesToSkip.containsKey(packageInfo.packageName)) { + continue; + } + PackageState packageState = mPackageManagerInternal.getPackageStateInternal( + packageInfo.packageName); + if (packageState == null) { + Slog.w(TAG, "Package state is unavailable, ignoring the package " + + packageInfo.packageName); + continue; + } + + List resultsForApp = collectAppInfo( + packageState, MBA_STATUS_NEW_INSTALL); + results.addAll(resultsForApp); + } + return results; + } + + private void recordApexInfo(IBinaryTransparencyService.ApexInfo apexInfo) { + // Must order by the proto's field number. + FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED, + apexInfo.packageName, + apexInfo.longVersion, + (apexInfo.digest != null) ? HexEncoding.encodeToString(apexInfo.digest, false) + : null, + apexInfo.digestAlgorithm, + apexInfo.signerDigests); + } + + private void writeAppInfoToLog(IBinaryTransparencyService.AppInfo appInfo) { + // Must order by the proto's field number. + FrameworkStatsLog.write(FrameworkStatsLog.MOBILE_BUNDLED_APP_INFO_GATHERED, + appInfo.packageName, + appInfo.longVersion, + (appInfo.digest != null) ? HexEncoding.encodeToString(appInfo.digest, false) + : null, + appInfo.digestAlgorithm, + appInfo.signerDigests, + appInfo.mbaStatus, + appInfo.initiator, + appInfo.initiatorSignerDigests, + appInfo.installer, + appInfo.originator, + appInfo.splitName); + } + + /** + * A wrapper around + * {@link ApkSignatureVerifier#verifySignaturesInternal(ParseInput, String, int, boolean)}. + * @param pathToApk The APK's installation path + * @return a {@code Map} with algorithm type as the key and content + * digest as the value. + * a {@code null} is returned upon encountering any error. + */ + private Map computeApkContentDigest(String pathToApk) { + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + ParseResult parseResult = + ApkSignatureVerifier.verifySignaturesInternal(input, + pathToApk, + SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2, false, false); + if (parseResult.isError()) { + Slog.e(TAG, "Failed to compute content digest for " + + pathToApk + " due to: " + + parseResult.getErrorMessage()); + return null; + } + return parseResult.getResult().contentDigests; + } + + @Override + public void onShellCommand(@Nullable FileDescriptor in, + @Nullable FileDescriptor out, + @Nullable FileDescriptor err, + @NonNull String[] args, + @Nullable ShellCallback callback, + @NonNull ResultReceiver resultReceiver) throws RemoteException { + (new ShellCommand() { + + private int printSignedImageInfo() { + final PrintWriter pw = getOutPrintWriter(); + boolean listAllPartitions = false; + String opt; + + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-a": + listAllPartitions = true; + break; + default: + pw.println("ERROR: Unknown option: " + opt); + return 1; + } + } + + final String signedImageInfo = getSignedImageInfo(); + pw.println("Image Info:"); + pw.println(Build.FINGERPRINT); + pw.println(signedImageInfo); + pw.println(""); + + if (listAllPartitions) { + PackageManager pm = mContext.getPackageManager(); + if (pm == null) { + pw.println("ERROR: Failed to obtain an instance of package manager."); + return -1; + } + + pw.println("Other partitions:"); + List buildPartitions = Build.getFingerprintedPartitions(); + for (Build.Partition buildPartition : buildPartitions) { + pw.println("Name: " + buildPartition.getName()); + pw.println("Fingerprint: " + buildPartition.getFingerprint()); + pw.println("Build time (ms): " + buildPartition.getBuildTimeMillis()); + } + } + return 0; + } + + private void printPackageMeasurements(PackageInfo packageInfo, + boolean useSha256, + final PrintWriter pw) { + Map contentDigests = computeApkContentDigest( + packageInfo.applicationInfo.sourceDir); + if (contentDigests == null) { + pw.println("ERROR: Failed to compute package content digest for " + + packageInfo.applicationInfo.sourceDir); + return; + } + + if (useSha256) { + byte[] fileBuff = PackageUtils.createLargeFileBuffer(); + String hexEncodedSha256Digest = + PackageUtils.computeSha256DigestForLargeFile( + packageInfo.applicationInfo.sourceDir, fileBuff); + pw.print(hexEncodedSha256Digest + ","); + } + + for (Map.Entry entry : contentDigests.entrySet()) { + Integer algorithmId = entry.getKey(); + byte[] contentDigest = entry.getValue(); + + pw.print(translateContentDigestAlgorithmIdToString(algorithmId)); + pw.print(":"); + pw.print(HexEncoding.encodeToString(contentDigest, false)); + pw.print("\n"); + } + } + + private void printPackageInstallationInfo(PackageInfo packageInfo, + boolean useSha256, + final PrintWriter pw) { + pw.println("--- Package Installation Info ---"); + pw.println("Current install location: " + + packageInfo.applicationInfo.sourceDir); + if (packageInfo.applicationInfo.sourceDir.startsWith("/data/apex/")) { + String origPackageFilepath = getOriginalApexPreinstalledLocation( + packageInfo.packageName); + pw.println("|--> Pre-installed package install location: " + + origPackageFilepath); + + if (!origPackageFilepath.equals(APEX_PRELOAD_LOCATION_ERROR)) { + if (useSha256) { + String sha256Digest = PackageUtils.computeSha256DigestForLargeFile( + origPackageFilepath, PackageUtils.createLargeFileBuffer()); + pw.println("|--> Pre-installed package SHA-256 digest: " + + sha256Digest); + } + + Map contentDigests = computeApkContentDigest( + origPackageFilepath); + if (contentDigests == null) { + pw.println("|--> ERROR: Failed to compute package content digest " + + "for " + origPackageFilepath); + } else { + for (Map.Entry entry : contentDigests.entrySet()) { + Integer algorithmId = entry.getKey(); + byte[] contentDigest = entry.getValue(); + pw.println("|--> Pre-installed package content digest: " + + HexEncoding.encodeToString(contentDigest, false)); + pw.println("|--> Pre-installed package content digest " + + "algorithm: " + + translateContentDigestAlgorithmIdToString( + algorithmId)); + } + } + } + } + pw.println("First install time (ms): " + packageInfo.firstInstallTime); + pw.println("Last update time (ms): " + packageInfo.lastUpdateTime); + // TODO(b/261493591): Determination of whether a package is preinstalled can be + // made more robust + boolean isPreloaded = (packageInfo.firstInstallTime + == packageInfo.lastUpdateTime); + pw.println("Is preloaded: " + isPreloaded); + + InstallSourceInfo installSourceInfo = getInstallSourceInfo( + packageInfo.packageName); + if (installSourceInfo == null) { + pw.println("ERROR: Unable to obtain installSourceInfo of " + + packageInfo.packageName); + } else { + pw.println("Installation initiated by: " + + installSourceInfo.getInitiatingPackageName()); + pw.println("Installation done by: " + + installSourceInfo.getInstallingPackageName()); + pw.println("Installation originating from: " + + installSourceInfo.getOriginatingPackageName()); + } + + if (packageInfo.isApex) { + pw.println("Is an active APEX: " + packageInfo.isActiveApex); + } + } + + private void printPackageSignerDetails(SigningInfo signerInfo, + final PrintWriter pw) { + if (signerInfo == null) { + pw.println("ERROR: Package's signingInfo is null."); + return; + } + pw.println("--- Package Signer Info ---"); + pw.println("Has multiple signers: " + signerInfo.hasMultipleSigners()); + pw.println("Signing key has been rotated: " + + signerInfo.hasPastSigningCertificates()); + Signature[] packageSigners = signerInfo.getApkContentsSigners(); + for (Signature packageSigner : packageSigners) { + byte[] packageSignerDigestBytes = + PackageUtils.computeSha256DigestBytes(packageSigner.toByteArray()); + String packageSignerDigestHextring = + HexEncoding.encodeToString(packageSignerDigestBytes, false); + pw.println("Signer cert's SHA256-digest: " + packageSignerDigestHextring); + try { + PublicKey publicKey = packageSigner.getPublicKey(); + pw.println("Signing key algorithm: " + publicKey.getAlgorithm()); + } catch (CertificateException e) { + Slog.e(TAG, + "Failed to obtain public key of signer for cert with hash: " + + packageSignerDigestHextring, e); + } + } + + if (!signerInfo.hasMultipleSigners() + && signerInfo.hasPastSigningCertificates()) { + pw.println("== Signing Cert Lineage (Excluding The Most Recent) =="); + pw.println("(Certs are sorted in the order of rotation, beginning with the " + + "original signing cert)"); + Signature[] signingCertHistory = signerInfo.getSigningCertificateHistory(); + for (int i = 0; i < (signingCertHistory.length - 1); i++) { + Signature signature = signingCertHistory[i]; + byte[] signatureDigestBytes = PackageUtils.computeSha256DigestBytes( + signature.toByteArray()); + String certHashHexString = HexEncoding.encodeToString( + signatureDigestBytes, false); + pw.println(" ++ Signer cert #" + (i + 1) + " ++"); + pw.println(" Cert SHA256-digest: " + certHashHexString); + try { + PublicKey publicKey = signature.getPublicKey(); + pw.println(" Signing key algorithm: " + publicKey.getAlgorithm()); + } catch (CertificateException e) { + Slog.e(TAG, "Failed to obtain public key of signer for cert " + + "with hash: " + certHashHexString, e); + } + } + } + + } + + private void printModuleDetails(ModuleInfo moduleInfo, final PrintWriter pw) { + pw.println("--- Module Details ---"); + pw.println("Module name: " + moduleInfo.getName()); + pw.println("Module visibility: " + + (moduleInfo.isHidden() ? "hidden" : "visible")); + } + + private void printAppDetails(PackageInfo packageInfo, + boolean printLibraries, + final PrintWriter pw) { + pw.println("--- App Details ---"); + pw.println("Name: " + packageInfo.applicationInfo.name); + pw.println("Label: " + mContext.getPackageManager().getApplicationLabel( + packageInfo.applicationInfo)); + pw.println("Description: " + packageInfo.applicationInfo.loadDescription( + mContext.getPackageManager())); + pw.println("Has code: " + packageInfo.applicationInfo.hasCode()); + pw.println("Is enabled: " + packageInfo.applicationInfo.enabled); + pw.println("Is suspended: " + ((packageInfo.applicationInfo.flags + & ApplicationInfo.FLAG_SUSPENDED) != 0)); + + pw.println("Compile SDK version: " + packageInfo.compileSdkVersion); + pw.println("Target SDK version: " + + packageInfo.applicationInfo.targetSdkVersion); + + pw.println("Is privileged: " + + packageInfo.applicationInfo.isPrivilegedApp()); + pw.println("Is a stub: " + packageInfo.isStub); + pw.println("Is a core app: " + packageInfo.coreApp); + pw.println("SEInfo: " + packageInfo.applicationInfo.seInfo); + pw.println("Component factory: " + + packageInfo.applicationInfo.appComponentFactory); + pw.println("Process name: " + packageInfo.applicationInfo.processName); + pw.println("Task affinity: " + packageInfo.applicationInfo.taskAffinity); + pw.println("UID: " + packageInfo.applicationInfo.uid); + pw.println("Shared UID: " + packageInfo.sharedUserId); + + if (printLibraries) { + pw.println("== App's Shared Libraries =="); + List sharedLibraryInfos = + packageInfo.applicationInfo.getSharedLibraryInfos(); + if (sharedLibraryInfos == null || sharedLibraryInfos.isEmpty()) { + pw.println(""); + } + + for (int i = 0; i < sharedLibraryInfos.size(); i++) { + SharedLibraryInfo sharedLibraryInfo = sharedLibraryInfos.get(i); + pw.println(" ++ Library #" + (i + 1) + " ++"); + pw.println(" Lib name: " + sharedLibraryInfo.getName()); + long libVersion = sharedLibraryInfo.getLongVersion(); + pw.print(" Lib version: "); + if (libVersion == SharedLibraryInfo.VERSION_UNDEFINED) { + pw.print("undefined"); + } else { + pw.print(libVersion); + } + pw.print("\n"); + + pw.println(" Lib package name (if available): " + + sharedLibraryInfo.getPackageName()); + pw.println(" Lib path: " + sharedLibraryInfo.getPath()); + pw.print(" Lib type: "); + switch (sharedLibraryInfo.getType()) { + case SharedLibraryInfo.TYPE_BUILTIN: + pw.print("built-in"); + break; + case SharedLibraryInfo.TYPE_DYNAMIC: + pw.print("dynamic"); + break; + case SharedLibraryInfo.TYPE_STATIC: + pw.print("static"); + break; + case SharedLibraryInfo.TYPE_SDK_PACKAGE: + pw.print("SDK"); + break; + case SharedLibraryInfo.VERSION_UNDEFINED: + default: + pw.print("undefined"); + break; + } + pw.print("\n"); + pw.println(" Is a native lib: " + sharedLibraryInfo.isNative()); + } + } + + } + + private void printHeadersHelper(@NonNull String packageType, + boolean useSha256, + @NonNull final PrintWriter pw) { + pw.print(packageType + " Info [Format: package_name,package_version,"); + if (useSha256) { + pw.print("package_sha256_digest,"); + } + pw.print("content_digest_algorithm:content_digest]:\n"); + } + + private int printAllApexs() { + final PrintWriter pw = getOutPrintWriter(); + boolean verbose = false; + boolean useSha256 = false; + boolean printHeaders = true; + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-v": + case "--verbose": + verbose = true; + break; + case "-o": + case "--old": + useSha256 = true; + break; + case "--no-headers": + printHeaders = false; + break; + default: + pw.println("ERROR: Unknown option: " + opt); + return 1; + } + } + + PackageManager pm = mContext.getPackageManager(); + if (pm == null) { + pw.println("ERROR: Failed to obtain an instance of package manager."); + return -1; + } + + if (!verbose && printHeaders) { + printHeadersHelper("APEX", useSha256, pw); + } + for (PackageInfo packageInfo : getCurrentInstalledApexs()) { + if (verbose && printHeaders) { + printHeadersHelper("APEX", useSha256, pw); + } + String packageName = packageInfo.packageName; + pw.print(packageName + "," + + packageInfo.getLongVersionCode() + ","); + printPackageMeasurements(packageInfo, useSha256, pw); + + if (verbose) { + ModuleInfo moduleInfo; + try { + moduleInfo = pm.getModuleInfo(packageInfo.packageName, 0); + pw.println("Is a module: true"); + printModuleDetails(moduleInfo, pw); + } catch (PackageManager.NameNotFoundException e) { + pw.println("Is a module: false"); + } + + printPackageInstallationInfo(packageInfo, useSha256, pw); + printPackageSignerDetails(packageInfo.signingInfo, pw); + pw.println(""); + } + } + return 0; + } + + private int printAllModules() { + final PrintWriter pw = getOutPrintWriter(); + boolean verbose = false; + boolean useSha256 = false; + boolean printHeaders = true; + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-v": + case "--verbose": + verbose = true; + break; + case "-o": + case "--old": + useSha256 = true; + break; + case "--no-headers": + printHeaders = false; + break; + default: + pw.println("ERROR: Unknown option: " + opt); + return 1; + } + } + + PackageManager pm = mContext.getPackageManager(); + if (pm == null) { + pw.println("ERROR: Failed to obtain an instance of package manager."); + return -1; + } + + if (!verbose && printHeaders) { + printHeadersHelper("Module", useSha256, pw); + } + for (ModuleInfo module : pm.getInstalledModules(PackageManager.MATCH_ALL)) { + String packageName = module.getPackageName(); + if (verbose && printHeaders) { + printHeadersHelper("Module", useSha256, pw); + } + try { + PackageInfo packageInfo = pm.getPackageInfo(packageName, + PackageManager.MATCH_APEX + | PackageManager.GET_SIGNING_CERTIFICATES); + pw.print(packageInfo.packageName + ","); + pw.print(packageInfo.getLongVersionCode() + ","); + printPackageMeasurements(packageInfo, useSha256, pw); + + if (verbose) { + printModuleDetails(module, pw); + printPackageInstallationInfo(packageInfo, useSha256, pw); + printPackageSignerDetails(packageInfo.signingInfo, pw); + pw.println(""); + } + } catch (PackageManager.NameNotFoundException e) { + pw.println(packageName + + ",ERROR:Unable to find PackageInfo for this module."); + if (verbose) { + printModuleDetails(module, pw); + pw.println(""); + } + continue; + } + } + return 0; + } + + private int printAllMbas() { + final PrintWriter pw = getOutPrintWriter(); + boolean verbose = false; + boolean printLibraries = false; + boolean useSha256 = false; + boolean printHeaders = true; + boolean preloadsOnly = false; + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-v": + case "--verbose": + verbose = true; + break; + case "-l": + printLibraries = true; + break; + case "-o": + case "--old": + useSha256 = true; + break; + case "--no-headers": + printHeaders = false; + break; + case "--preloads-only": + preloadsOnly = true; + break; + default: + pw.println("ERROR: Unknown option: " + opt); + return 1; + } + } + + if (!verbose && printHeaders) { + if (preloadsOnly) { + printHeadersHelper("Preload", useSha256, pw); + } else { + printHeadersHelper("MBA", useSha256, pw); + } + } + + PackageManager pm = mContext.getPackageManager(); + for (PackageInfo packageInfo : pm.getInstalledPackages( + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_FACTORY_ONLY + | PackageManager.GET_SIGNING_CERTIFICATES))) { + if (packageInfo.signingInfo == null) { + PackageInfo origPackageInfo = packageInfo; + try { + pm.getPackageInfo(packageInfo.packageName, + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL + | PackageManager.GET_SIGNING_CERTIFICATES)); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Failed to obtain an updated PackageInfo of " + + origPackageInfo.packageName); + packageInfo = origPackageInfo; + } + } + + if (verbose && printHeaders) { + printHeadersHelper("Preload", useSha256, pw); + } + pw.print(packageInfo.packageName + ","); + pw.print(packageInfo.getLongVersionCode() + ","); + printPackageMeasurements(packageInfo, useSha256, pw); + + if (verbose) { + printAppDetails(packageInfo, printLibraries, pw); + printPackageInstallationInfo(packageInfo, useSha256, pw); + printPackageSignerDetails(packageInfo.signingInfo, pw); + pw.println(""); + } + } + + if (preloadsOnly) { + return 0; + } + for (PackageInfo packageInfo : getNewlyInstalledMbas()) { + if (verbose && printHeaders) { + printHeadersHelper("MBA", useSha256, pw); + } + pw.print(packageInfo.packageName + ","); + pw.print(packageInfo.getLongVersionCode() + ","); + printPackageMeasurements(packageInfo, useSha256, pw); + + if (verbose) { + printAppDetails(packageInfo, printLibraries, pw); + printPackageInstallationInfo(packageInfo, useSha256, pw); + printPackageSignerDetails(packageInfo.signingInfo, pw); + pw.println(""); + } + } + return 0; + } + + @Override + public int onCommand(String cmd) { + if (cmd == null) { + return handleDefaultCommands(cmd); + } + + final PrintWriter pw = getOutPrintWriter(); + switch (cmd) { + case "get": { + final String infoType = getNextArg(); + if (infoType == null) { + printHelpMenu(); + return -1; + } + + switch (infoType) { + case "image_info": + return printSignedImageInfo(); + case "apex_info": + return printAllApexs(); + case "module_info": + return printAllModules(); + case "mba_info": + return printAllMbas(); + default: + pw.println(String.format("ERROR: Unknown info type '%s'", + infoType)); + return 1; + } + } + default: + return handleDefaultCommands(cmd); + } + } + + private void printHelpMenu() { + final PrintWriter pw = getOutPrintWriter(); + pw.println("Transparency manager (transparency) commands:"); + pw.println(" help"); + pw.println(" Print this help text."); + pw.println(""); + pw.println(" get image_info [-a]"); + pw.println(" Print information about loaded image (firmware). Options:"); + pw.println(" -a: lists all other identifiable partitions."); + pw.println(""); + pw.println(" get apex_info [-o] [-v] [--no-headers]"); + pw.println(" Print information about installed APEXs on device."); + pw.println(" -o: also uses the old digest scheme (SHA256) to compute " + + "APEX hashes. WARNING: This can be a very slow and CPU-intensive " + + "computation."); + pw.println(" -v: lists more verbose information about each APEX."); + pw.println(" --no-headers: does not print the header if specified."); + pw.println(""); + pw.println(" get module_info [-o] [-v] [--no-headers]"); + pw.println(" Print information about installed modules on device."); + pw.println(" -o: also uses the old digest scheme (SHA256) to compute " + + "module hashes. WARNING: This can be a very slow and " + + "CPU-intensive computation."); + pw.println(" -v: lists more verbose information about each module."); + pw.println(" --no-headers: does not print the header if specified."); + pw.println(""); + pw.println(" get mba_info [-o] [-v] [-l] [--no-headers] [--preloads-only]"); + pw.println(" Print information about installed mobile bundle apps " + + "(MBAs on device)."); + pw.println(" -o: also uses the old digest scheme (SHA256) to compute " + + "MBA hashes. WARNING: This can be a very slow and CPU-intensive " + + "computation."); + pw.println(" -v: lists more verbose information about each app."); + pw.println(" -l: lists shared library info. (This option only works " + + "when -v option is also specified)"); + pw.println(" --no-headers: does not print the header if specified."); + pw.println(" --preloads-only: lists only preloaded apps. This options can " + + "also be combined with others."); + pw.println(""); + } + + @Override + public void onHelp() { + printHelpMenu(); + } + }).exec(this, in, out, err, args, callback, resultReceiver); + } + } + private final BinaryTransparencyServiceImpl mServiceImpl; + + /** + * A wrapper of {@link FrameworkStatsLog} for easier testing + */ + @VisibleForTesting + public static class BiometricLogger { + private static final String TAG = "BiometricLogger"; + + private static final BiometricLogger sInstance = new BiometricLogger(); + + private BiometricLogger() {} + + public static BiometricLogger getInstance() { + return sInstance; + } + + /** + * A wrapper of {@link FrameworkStatsLog} + * + * @param sensorId The sensorId of the biometric to be logged + * @param modality The modality of the biometric + * @param sensorType The sensor type of the biometric + * @param sensorStrength The sensor strength of the biometric + * @param componentId The component Id of a component of the biometric + * @param hardwareVersion The hardware version of a component of the biometric + * @param firmwareVersion The firmware version of a component of the biometric + * @param serialNumber The serial number of a component of the biometric + * @param softwareVersion The software version of a component of the biometric + */ + public void logStats(int sensorId, int modality, int sensorType, int sensorStrength, + String componentId, String hardwareVersion, String firmwareVersion, + String serialNumber, String softwareVersion) { + FrameworkStatsLog.write(FrameworkStatsLog.BIOMETRIC_PROPERTIES_COLLECTED, + sensorId, modality, sensorType, sensorStrength, componentId, hardwareVersion, + firmwareVersion, serialNumber, softwareVersion); + } + } + + public BinaryTransparencyService(Context context) { + this(context, BiometricLogger.getInstance()); + } + + @VisibleForTesting + BinaryTransparencyService(Context context, BiometricLogger biometricLogger) { + super(context); + mContext = context; + mServiceImpl = new BinaryTransparencyServiceImpl(); + mVbmetaDigest = VBMETA_DIGEST_UNINITIALIZED; + mMeasurementsLastRecordedMs = 0; + mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); + mBiometricLogger = biometricLogger; + } + + /** + * Called when the system service should publish a binder service using + * {@link #publishBinderService(String, IBinder).} + */ + @Override + public void onStart() { + try { + publishBinderService(Context.BINARY_TRANSPARENCY_SERVICE, mServiceImpl); + Slog.i(TAG, "Started BinaryTransparencyService"); + } catch (Throwable t) { + Slog.e(TAG, "Failed to start BinaryTransparencyService.", t); + } + } + + /** + * Called on each phase of the boot process. Phases before the service's start phase + * (as defined in the @Service annotation) are never received. + * + * @param phase The current boot phase. + */ + @Override + public void onBootPhase(int phase) { + + // we are only interested in doing things at PHASE_BOOT_COMPLETED + if (phase == PHASE_BOOT_COMPLETED) { + Slog.i(TAG, "Boot completed. Getting boot integrity data."); + collectBootIntegrityInfo(); + + // Log to statsd + // TODO(b/264061957): For now, biometric system properties are always collected if users + // share usage & diagnostics information. In the future, collect biometric system + // properties only when transparency log verification of the target partitions fails + // (e.g. when the system/vendor partitions have been changed) once the binary + // transparency infrastructure is ready. + Slog.i(TAG, "Boot completed. Collecting biometric system properties."); + collectBiometricProperties(); + + // to avoid the risk of holding up boot time, computations to measure APEX, Module, and + // MBA digests are scheduled here, but only executed when the device is idle and plugged + // in. + Slog.i(TAG, "Scheduling measurements to be taken."); + UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext, + BinaryTransparencyService.this); + + registerAllPackageUpdateObservers(); + } + } + + /** + * JobService to measure all covered binaries and record results to statsd. + */ + public static class UpdateMeasurementsJobService extends JobService { + private static long sTimeLastRanMs = 0; + private static final int DO_BINARY_MEASUREMENTS_JOB_ID = 1740526926; + + @Override + public boolean onStartJob(JobParameters params) { + Slog.d(TAG, "Job to update binary measurements started."); + if (params.getJobId() != DO_BINARY_MEASUREMENTS_JOB_ID) { + return false; + } + + // we'll perform binary measurements via threads to be mindful of low-end devices + // where this operation might take longer than expected, and so that we don't block + // system_server's main thread. + Executors.defaultThreadFactory().newThread(() -> { + IBinder b = ServiceManager.getService(Context.BINARY_TRANSPARENCY_SERVICE); + IBinaryTransparencyService iBtsService = + IBinaryTransparencyService.Stub.asInterface(b); + try { + iBtsService.recordMeasurementsForAllPackages(); + } catch (RemoteException e) { + Slog.e(TAG, "Taking binary measurements was interrupted.", e); + return; + } + sTimeLastRanMs = System.currentTimeMillis(); + jobFinished(params, false); + }).start(); + + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } + + @SuppressLint("DefaultLocale") + static void scheduleBinaryMeasurements(Context context, BinaryTransparencyService service) { + Slog.i(TAG, "Scheduling binary content-digest computation job"); + final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + if (jobScheduler == null) { + Slog.e(TAG, "Failed to obtain an instance of JobScheduler."); + return; + } + + if (jobScheduler.getPendingJob(DO_BINARY_MEASUREMENTS_JOB_ID) != null) { + Slog.d(TAG, "A measurement job has already been scheduled."); + return; + } + + long minWaitingPeriodMs = 0; + if (sTimeLastRanMs != 0) { + minWaitingPeriodMs = RECORD_MEASUREMENTS_COOLDOWN_MS + - (System.currentTimeMillis() - sTimeLastRanMs); + // bound the range of minWaitingPeriodMs in the case where > 24h has elapsed + minWaitingPeriodMs = Math.max(0, + Math.min(minWaitingPeriodMs, RECORD_MEASUREMENTS_COOLDOWN_MS)); + Slog.d(TAG, "Scheduling the next measurement to be done at least " + + minWaitingPeriodMs + "ms from now."); + } + + final JobInfo jobInfo = new JobInfo.Builder(DO_BINARY_MEASUREMENTS_JOB_ID, + new ComponentName(context, UpdateMeasurementsJobService.class)) + .setRequiresDeviceIdle(true) + .setRequiresCharging(true) + .setMinimumLatency(minWaitingPeriodMs) + .build(); + if (jobScheduler.schedule(jobInfo) != JobScheduler.RESULT_SUCCESS) { + Slog.e(TAG, "Failed to schedule job to measure binaries."); + return; + } + Slog.d(TAG, TextUtils.formatSimple( + "Job %d to measure binaries was scheduled successfully.", + DO_BINARY_MEASUREMENTS_JOB_ID)); + } + } + + /** + * Convert a {@link FingerprintSensorProperties} sensor type to the corresponding enum to be + * logged. + * + * @param sensorType See {@link FingerprintSensorProperties} + * @return The enum to be logged + */ + private int toFingerprintSensorType(@FingerprintSensorProperties.SensorType int sensorType) { + switch (sensorType) { + case FingerprintSensorProperties.TYPE_REAR: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FP_REAR; + case FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FP_UDFPS_ULTRASONIC; + case FingerprintSensorProperties.TYPE_UDFPS_OPTICAL: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FP_UDFPS_OPTICAL; + case FingerprintSensorProperties.TYPE_POWER_BUTTON: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FP_POWER_BUTTON; + case FingerprintSensorProperties.TYPE_HOME_BUTTON: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FP_HOME_BUTTON; + default: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_UNKNOWN; + } + } + + /** + * Convert a {@link FaceSensorProperties} sensor type to the corresponding enum to be logged. + * + * @param sensorType See {@link FaceSensorProperties} + * @return The enum to be logged + */ + private int toFaceSensorType(@FaceSensorProperties.SensorType int sensorType) { + switch (sensorType) { + case FaceSensorProperties.TYPE_RGB: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FACE_RGB; + case FaceSensorProperties.TYPE_IR: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_FACE_IR; + default: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_TYPE__SENSOR_UNKNOWN; + } + } + + /** + * Convert a {@link SensorProperties} sensor strength to the corresponding enum to be logged. + * + * @param sensorStrength See {@link SensorProperties} + * @return The enum to be logged + */ + private int toSensorStrength(@SensorProperties.Strength int sensorStrength) { + switch (sensorStrength) { + case SensorProperties.STRENGTH_CONVENIENCE: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_STRENGTH__STRENGTH_CONVENIENCE; + case SensorProperties.STRENGTH_WEAK: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_STRENGTH__STRENGTH_WEAK; + case SensorProperties.STRENGTH_STRONG: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_STRENGTH__STRENGTH_STRONG; + default: + return FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__SENSOR_STRENGTH__STRENGTH_UNKNOWN; + } + } + + /** + * A helper function to log detailed biometric sensor properties to statsd. + * + * @param prop The biometric sensor properties to be logged + * @param modality The modality of the biometric (e.g. fingerprint, face) to be logged + * @param sensorType The specific type of the biometric to be logged + */ + private void logBiometricProperties(SensorProperties prop, int modality, int sensorType) { + final int sensorId = prop.getSensorId(); + final int sensorStrength = toSensorStrength(prop.getSensorStrength()); + + // Log data for each component + // Note: none of the component info is a device identifier since every device of a given + // model and build share the same biometric system info (see b/216195167) + for (ComponentInfo componentInfo : prop.getComponentInfo()) { + mBiometricLogger.logStats( + sensorId, + modality, + sensorType, + sensorStrength, + componentInfo.getComponentId().trim(), + componentInfo.getHardwareVersion().trim(), + componentInfo.getFirmwareVersion().trim(), + componentInfo.getSerialNumber().trim(), + componentInfo.getSoftwareVersion().trim()); + } + } + + @VisibleForTesting + void collectBiometricProperties() { + // Check the flag to determine whether biometric property verification is enabled. It's + // disabled by default. + if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BIOMETRICS, + KEY_ENABLE_BIOMETRIC_PROPERTY_VERIFICATION, true)) { + if (DEBUG) { + Slog.d(TAG, "Do not collect/verify biometric properties. Feature disabled by " + + "DeviceConfig"); + } + return; + } + + PackageManager pm = mContext.getPackageManager(); + FingerprintManager fpManager = null; + FaceManager faceManager = null; + if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { + fpManager = mContext.getSystemService(FingerprintManager.class); + } + if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_FACE)) { + faceManager = mContext.getSystemService(FaceManager.class); + } + + if (fpManager != null) { + final int fpModality = FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__MODALITY__MODALITY_FINGERPRINT; + fpManager.addAuthenticatorsRegisteredCallback( + new IFingerprintAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List sensors) { + if (DEBUG) { + Slog.d(TAG, "Retrieve fingerprint sensor properties. " + + "sensors.size()=" + sensors.size()); + } + // Log data for each fingerprint sensor + for (FingerprintSensorPropertiesInternal propInternal : sensors) { + final FingerprintSensorProperties prop = + FingerprintSensorProperties.from(propInternal); + logBiometricProperties(prop, + fpModality, + toFingerprintSensorType(prop.getSensorType())); + } + } + }); + } + + if (faceManager != null) { + final int faceModality = FrameworkStatsLog + .BIOMETRIC_PROPERTIES_COLLECTED__MODALITY__MODALITY_FACE; + faceManager.addAuthenticatorsRegisteredCallback( + new IFaceAuthenticatorsRegisteredCallback.Stub() { + @Override + public void onAllAuthenticatorsRegistered( + List sensors) { + if (DEBUG) { + Slog.d(TAG, "Retrieve face sensor properties. sensors.size()=" + + sensors.size()); + } + // Log data for each face sensor + for (FaceSensorPropertiesInternal propInternal : sensors) { + final FaceSensorProperties prop = + FaceSensorProperties.from(propInternal); + logBiometricProperties(prop, + faceModality, + toFaceSensorType(prop.getSensorType())); + } + } + }); + } + } + + private void collectBootIntegrityInfo() { + mVbmetaDigest = SystemProperties.get(SYSPROP_NAME_VBETA_DIGEST, VBMETA_DIGEST_UNAVAILABLE); + Slog.d(TAG, String.format("VBMeta Digest: %s", mVbmetaDigest)); + FrameworkStatsLog.write(FrameworkStatsLog.VBMETA_DIGEST_REPORTED, mVbmetaDigest); + + if (android.security.Flags.binaryTransparencySepolicyHash()) { + IoThread.getExecutor().execute(() -> { + byte[] sepolicyHash = PackageUtils.computeSha256DigestForLargeFileAsBytes( + "/sys/fs/selinux/policy", PackageUtils.createLargeFileBuffer()); + String sepolicyHashEncoded = null; + if (sepolicyHash != null) { + sepolicyHashEncoded = HexEncoding.encodeToString(sepolicyHash, false); + Slog.d(TAG, "sepolicy hash: " + sepolicyHashEncoded); + } + FrameworkStatsLog.write(FrameworkStatsLog.BOOT_INTEGRITY_INFO_REPORTED, + sepolicyHashEncoded, mVbmetaDigest); + }); + } + } + + /** + * Listen for APK updates. + * + * There are two ways available to us to do this: + * 1. Register an observer using + * {@link PackageManagerInternal#getPackageList(PackageManagerInternal.PackageListObserver)}. + * 2. Register broadcast receivers, listening to either {@code ACTION_PACKAGE_ADDED} or + * {@code ACTION_PACKAGE_REPLACED}. + * + * After experimentation, we found that Option #1 does not catch updates to non-staged APEXs. + * Thus, we are implementing Option #2 here. More specifically, listening to + * {@link Intent#ACTION_PACKAGE_ADDED} allows us to capture all events we care about. + * + * We did not use {@link Intent#ACTION_PACKAGE_REPLACED} because it unfortunately does not + * detect updates to non-staged APEXs. Thus, we rely on {@link Intent#EXTRA_REPLACING} to + * filter out new installation from updates instead. + */ + private void registerApkAndNonStagedApexUpdateListener() { + Slog.d(TAG, "Registering APK & Non-Staged APEX updates..."); + IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); + filter.addDataScheme("package"); // this is somehow necessary + mContext.registerReceiver(new PackageUpdatedReceiver(), filter); + } + + /** + * Listen for staged-APEX updates. + * + * This method basically covers cases that are not caught by + * {@link #registerApkAndNonStagedApexUpdateListener()}, namely updates to APEXs that are staged + * for the subsequent reboot. + */ + private void registerStagedApexUpdateObserver() { + Slog.d(TAG, "Registering APEX updates..."); + IPackageManagerNative iPackageManagerNative = IPackageManagerNative.Stub.asInterface( + ServiceManager.getService("package_native")); + if (iPackageManagerNative == null) { + Slog.e(TAG, "IPackageManagerNative is null"); + return; + } + + try { + iPackageManagerNative.registerStagedApexObserver(new IStagedApexObserver.Stub() { + @Override + public void onApexStaged(ApexStagedEvent event) throws RemoteException { + Slog.d(TAG, "A new APEX has been staged for update. There are currently " + + event.stagedApexModuleNames.length + " APEX(s) staged for update. " + + "Scheduling measurement..."); + UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext, + BinaryTransparencyService.this); + } + }); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to register a StagedApexObserver."); + } + } + + private boolean isPackagePreloaded(String packageName) { + PackageManager pm = mContext.getPackageManager(); + try { + pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of( + PackageManager.MATCH_FACTORY_ONLY)); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + return true; + } + + private boolean isPackageAnApex(String packageName) { + PackageManager pm = mContext.getPackageManager(); + try { + PackageInfo packageInfo = pm.getPackageInfo(packageName, + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX)); + return packageInfo.isApex; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + private class PackageUpdatedReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!intent.getAction().equals(Intent.ACTION_PACKAGE_ADDED)) { + return; + } + + Uri data = intent.getData(); + if (data == null) { + Slog.e(TAG, "Shouldn't happen: intent data is null!"); + return; + } + + if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + Slog.d(TAG, "Not an update. Skipping..."); + return; + } + + String packageName = data.getSchemeSpecificPart(); + // now we've got to check what package is this + if (isPackagePreloaded(packageName) || isPackageAnApex(packageName)) { + Slog.d(TAG, packageName + " was updated. Scheduling measurement..."); + UpdateMeasurementsJobService.scheduleBinaryMeasurements(mContext, + BinaryTransparencyService.this); + } + } + } + + /** + * Register observers for APK and APEX updates. The current implementation breaks this process + * into 2 cases/methods because PackageManager does not offer a unified interface to register + * for all package updates in a universal and comprehensive manner. + * Thus, the observers will be invoked when either + * i) APK or non-staged APEX update; or + * ii) APEX staging happens. + * This will then be used as signals to schedule measurement for the relevant binaries. + */ + private void registerAllPackageUpdateObservers() { + registerApkAndNonStagedApexUpdateListener(); + registerStagedApexUpdateObserver(); + } + + private String translateContentDigestAlgorithmIdToString(int algorithmId) { + switch (algorithmId) { + case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256: + return "CHUNKED_SHA256"; + case ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512: + return "CHUNKED_SHA512"; + case ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256: + return "VERITY_CHUNKED_SHA256"; + case ApkSigningBlockUtils.CONTENT_DIGEST_SHA256: + return "SHA256"; + default: + return "UNKNOWN_ALGO_ID(" + algorithmId + ")"; + } + } + + @NonNull + private List getCurrentInstalledApexs() { + List results = new ArrayList<>(); + PackageManager pm = mContext.getPackageManager(); + if (pm == null) { + Slog.e(TAG, "Error obtaining an instance of PackageManager."); + return results; + } + List allPackages = pm.getInstalledPackages( + PackageManager.PackageInfoFlags.of(PackageManager.MATCH_APEX + | PackageManager.GET_SIGNING_CERTIFICATES)); + if (allPackages == null) { + Slog.e(TAG, "Error obtaining installed packages (including APEX)"); + return results; + } + + results = allPackages.stream().filter(p -> p.isApex).collect(Collectors.toList()); + return results; + } + + @Nullable + private InstallSourceInfo getInstallSourceInfo(String packageName) { + PackageManager pm = mContext.getPackageManager(); + if (pm == null) { + Slog.e(TAG, "Error obtaining an instance of PackageManager."); + return null; + } + try { + return pm.getInstallSourceInfo(packageName); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return null; + } + } + + @NonNull + private String getOriginalApexPreinstalledLocation(String packageName) { + try { + final String moduleName = apexPackageNameToModuleName(packageName); + IApexService apexService = IApexService.Stub.asInterface( + Binder.allowBlocking(ServiceManager.waitForService("apexservice"))); + for (ApexInfo info : apexService.getAllPackages()) { + if (moduleName.equals(info.moduleName)) { + return info.preinstalledModulePath; + } + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to get package list from apexservice", e); + } + return APEX_PRELOAD_LOCATION_ERROR; + } + + private String apexPackageNameToModuleName(String packageName) { + // It appears that only apexd knows the preinstalled location, and it uses module name as + // the identifier instead of package name. Given the input is a package name, we need to + // covert to module name. + return ApexManager.getInstance().getApexModuleNameForPackageName(packageName); + } + + /** + * Wrapper method to call into IBICS to get a list of all newly installed MBAs. + * + * We expect IBICS to maintain an accurate list of installed MBAs, and we merely make use of + * the results within this service. This means we do not further check whether the + * apps in the returned slice is still installed or not, esp. considering that preloaded apps + * could be updated, or post-setup installed apps *might* be deleted in real time. + * + * Note that we do *not* cache the results from IBICS because of the more dynamic nature of + * MBAs v.s. other binaries that we measure. + * + * @return a list of preloaded apps + dynamically installed apps that fit the definition of MBA. + */ + @NonNull + private List getNewlyInstalledMbas() { + List result = new ArrayList<>(); + IBackgroundInstallControlService iBics = IBackgroundInstallControlService.Stub.asInterface( + ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE)); + if (iBics == null) { + Slog.e(TAG, + "Failed to obtain an IBinder instance of IBackgroundInstallControlService"); + return result; + } + ParceledListSlice slice; + try { + slice = iBics.getBackgroundInstalledPackages( + PackageManager.MATCH_ALL | PackageManager.GET_SIGNING_CERTIFICATES, + UserHandle.USER_SYSTEM); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to get a list of MBAs.", e); + return result; + } + return slice.getList(); + } + + private record Digest(int algorithm, byte[] value) {} +} diff --git a/aosp/frameworks/base/services/core/java/com/android/server/pm/ApkChecksums.java b/aosp/frameworks/base/services/core/java/com/android/server/pm/ApkChecksums.java new file mode 100644 index 0000000000000000000000000000000000000000..13369d39454bd1dc03613674737de13784060291 --- /dev/null +++ b/aosp/frameworks/base/services/core/java/com/android/server/pm/ApkChecksums.java @@ -0,0 +1,864 @@ +/* + * 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 com.android.server.pm; + +import static android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256; +import static android.content.pm.Checksum.TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512; +import static android.content.pm.Checksum.TYPE_WHOLE_MD5; +import static android.content.pm.Checksum.TYPE_WHOLE_MERKLE_ROOT_4K_SHA256; +import static android.content.pm.Checksum.TYPE_WHOLE_SHA1; +import static android.content.pm.Checksum.TYPE_WHOLE_SHA256; +import static android.content.pm.Checksum.TYPE_WHOLE_SHA512; +import static android.content.pm.parsing.ApkLiteParseUtils.APK_FILE_EXTENSION; +import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA256; +import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_CHUNKED_SHA512; +import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApkChecksum; +import android.content.pm.Checksum; +import android.content.pm.IOnChecksumsReadyListener; +import android.content.pm.PackageManagerInternal; +import android.content.pm.Signature; +import android.content.pm.SigningDetails.SignatureSchemeVersion; +import android.content.pm.parsing.ApkLiteParseUtils; +import android.content.pm.parsing.result.ParseResult; +import android.content.pm.parsing.result.ParseTypeImpl; +import android.os.Handler; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.incremental.IncrementalManager; +import android.os.incremental.IncrementalStorage; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Pair; +import android.util.Slog; +import android.util.apk.ApkSignatureSchemeV2Verifier; +import android.util.apk.ApkSignatureSchemeV3Verifier; +import android.util.apk.ApkSignatureSchemeV4Verifier; +import android.util.apk.ApkSignatureVerifier; +import android.util.apk.ApkSigningBlockUtils; +import android.util.apk.ByteBufferFactory; +import android.util.apk.SignatureInfo; +import android.util.apk.SignatureNotFoundException; +import android.util.apk.VerityBuilder; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.security.VerityUtils; +import com.android.server.pm.pkg.AndroidPackage; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.security.DigestException; +import java.security.InvalidParameterException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import sun.security.pkcs.PKCS7; +import sun.security.pkcs.SignerInfo; + +/** + * Provides checksums for APK. + */ +public class ApkChecksums { + static final String TAG = "ApkChecksums"; + + private static final String DIGESTS_FILE_EXTENSION = ".digests"; + private static final String DIGESTS_SIGNATURE_FILE_EXTENSION = ".signature"; + + // MessageDigest algorithms. + static final String ALGO_MD5 = "MD5"; + static final String ALGO_SHA1 = "SHA1"; + static final String ALGO_SHA256 = "SHA256"; + static final String ALGO_SHA512 = "SHA512"; + + private static final Certificate[] EMPTY_CERTIFICATE_ARRAY = {}; + + /** + * Arbitrary size restriction for the signature, used to sign the checksums. + */ + private static final int MAX_SIGNATURE_SIZE_BYTES = 35 * 1024; + + /** + * Check back in 1 second after we detected we needed to wait for the APK to be fully available. + */ + private static final long PROCESS_REQUIRED_CHECKSUMS_DELAY_MILLIS = 1000; + + /** + * 24 hours timeout to wait till all files are loaded. + */ + private static final long PROCESS_REQUIRED_CHECKSUMS_TIMEOUT_MILLIS = 1000 * 3600 * 24; + + /** + * Unit tests will instantiate, extend and/or mock to mock dependencies / behaviors. + * + * NOTE: All getters should return the same instance for every call. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + static class Injector { + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + interface Producer { + /** Produce an instance of type {@link T} */ + T produce(); + } + + private final Producer mContext; + private final Producer mHandlerProducer; + private final Producer mIncrementalManagerProducer; + private final Producer mPackageManagerInternalProducer; + + Injector(Producer context, Producer handlerProducer, + Producer incrementalManagerProducer, + Producer packageManagerInternalProducer) { + mContext = context; + mHandlerProducer = handlerProducer; + mIncrementalManagerProducer = incrementalManagerProducer; + mPackageManagerInternalProducer = packageManagerInternalProducer; + } + + public Context getContext() { + return mContext.produce(); + } + + public Handler getHandler() { + return mHandlerProducer.produce(); + } + + public IncrementalManager getIncrementalManager() { + return mIncrementalManagerProducer.produce(); + } + + public PackageManagerInternal getPackageManagerInternal() { + return mPackageManagerInternalProducer.produce(); + } + } + + /** + * Return the digests path associated with the given code path + * (replaces '.apk' extension with '.digests') + * + * @throws IllegalArgumentException if the code path is not an .apk. + */ + public static String buildDigestsPathForApk(String codePath) { + if (!ApkLiteParseUtils.isApkPath(codePath)) { + throw new IllegalStateException("Code path is not an apk " + codePath); + } + return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length()) + + DIGESTS_FILE_EXTENSION; + } + + /** + * Return the signature path associated with the given digests path. + * (appends '.signature' to the end) + */ + public static String buildSignaturePathForDigests(String digestsPath) { + return digestsPath + DIGESTS_SIGNATURE_FILE_EXTENSION; + } + + /** Returns true if the given file looks like containing digests or digests' signature. */ + public static boolean isDigestOrDigestSignatureFile(File file) { + final String name = file.getName(); + return name.endsWith(DIGESTS_FILE_EXTENSION) || name.endsWith( + DIGESTS_SIGNATURE_FILE_EXTENSION); + } + + /** + * Search for the digests file associated with the given target file. + * If it exists, the method returns the digests file; otherwise it returns null. + */ + public static File findDigestsForFile(File targetFile) { + String digestsPath = buildDigestsPathForApk(targetFile.getAbsolutePath()); + File digestsFile = new File(digestsPath); + return digestsFile.exists() ? digestsFile : null; + } + + /** + * Search for the signature file associated with the given digests file. + * If it exists, the method returns the signature file; otherwise it returns null. + */ + public static File findSignatureForDigests(File digestsFile) { + String signaturePath = buildSignaturePathForDigests(digestsFile.getAbsolutePath()); + File signatureFile = new File(signaturePath); + return signatureFile.exists() ? signatureFile : null; + } + + /** + * Serialize checksums to the stream in binary format. + */ + public static void writeChecksums(OutputStream os, Checksum[] checksums) + throws IOException { + try (DataOutputStream dos = new DataOutputStream(os)) { + for (Checksum checksum : checksums) { + Checksum.writeToStream(dos, checksum); + } + } + } + + private static Checksum[] readChecksums(File file) throws IOException { + try (InputStream is = new FileInputStream(file)) { + return readChecksums(is); + } + } + + /** + * Deserialize array of checksums previously stored in + * {@link #writeChecksums(OutputStream, Checksum[])}. + */ + public static Checksum[] readChecksums(InputStream is) throws IOException { + try (DataInputStream dis = new DataInputStream(is)) { + ArrayList checksums = new ArrayList<>(); + try { + // 100 is an arbitrary very big number. We should stop at EOF. + for (int i = 0; i < 100; ++i) { + checksums.add(Checksum.readFromStream(dis)); + } + } catch (EOFException e) { + // expected + } + return checksums.toArray(new Checksum[checksums.size()]); + } + } + + /** + * Verifies signature over binary serialized checksums. + * @param checksums array of checksums + * @param signature detached PKCS7 signature in DER format + * @return all certificates that passed verification + * @throws SignatureException if verification fails + */ + public static @NonNull Certificate[] verifySignature(Checksum[] checksums, byte[] signature) + throws NoSuchAlgorithmException, IOException, SignatureException { + if (signature == null || signature.length > MAX_SIGNATURE_SIZE_BYTES) { + throw new SignatureException("Invalid signature"); + } + + final byte[] blob; + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + writeChecksums(os, checksums); + blob = os.toByteArray(); + } + + PKCS7 pkcs7 = new PKCS7(signature); + + final Certificate[] certs = pkcs7.getCertificates(); + if (certs == null || certs.length == 0) { + throw new SignatureException("Signature missing certificates"); + } + + final SignerInfo[] signerInfos = pkcs7.verify(blob); + if (signerInfos == null || signerInfos.length == 0) { + throw new SignatureException("Verification failed"); + } + + ArrayList certificates = new ArrayList<>(signerInfos.length); + for (SignerInfo signerInfo : signerInfos) { + ArrayList chain = signerInfo.getCertificateChain(pkcs7); + if (chain == null) { + throw new SignatureException( + "Verification passed, but certification chain is empty."); + } + certificates.addAll(chain); + } + + return certificates.toArray(new Certificate[certificates.size()]); + } + + /** + * Fetch or calculate checksums for the collection of files. + * + * @param filesToChecksum split name, null for base and File to fetch checksums for + * @param optional mask to fetch readily available checksums + * @param required mask to forcefully calculate if not available + * @param installerPackageName package name of the installer of the packages + * @param trustedInstallers array of certificate to trust, two specific cases: + * null - trust anybody, + * [] - trust nobody. + * @param onChecksumsReadyListener to receive the resulting checksums + */ + public static void getChecksums(List> filesToChecksum, + @Checksum.TypeMask int optional, + @Checksum.TypeMask int required, + @Nullable String installerPackageName, + @Nullable Certificate[] trustedInstallers, + @NonNull IOnChecksumsReadyListener onChecksumsReadyListener, + @NonNull Injector injector) { + List> result = new ArrayList<>(filesToChecksum.size()); + for (int i = 0, size = filesToChecksum.size(); i < size; ++i) { + final String split = filesToChecksum.get(i).first; + final File file = filesToChecksum.get(i).second; + Map checksums = new ArrayMap<>(); + result.add(checksums); + + try { + getAvailableApkChecksums(split, file, optional | required, installerPackageName, + trustedInstallers, checksums, injector); + } catch (Throwable e) { + Slog.e(TAG, "Preferred checksum calculation error", e); + } + } + + long startTime = SystemClock.uptimeMillis(); + processRequiredChecksums(filesToChecksum, result, required, onChecksumsReadyListener, + injector, startTime); + } + + private static void processRequiredChecksums(List> filesToChecksum, + List> result, + @Checksum.TypeMask int required, + @NonNull IOnChecksumsReadyListener onChecksumsReadyListener, + @NonNull Injector injector, + long startTime) { + final boolean timeout = + SystemClock.uptimeMillis() - startTime >= PROCESS_REQUIRED_CHECKSUMS_TIMEOUT_MILLIS; + List allChecksums = new ArrayList<>(); + for (int i = 0, size = filesToChecksum.size(); i < size; ++i) { + final String split = filesToChecksum.get(i).first; + final File file = filesToChecksum.get(i).second; + Map checksums = result.get(i); + + try { + if (!timeout || required != 0) { + if (needToWait(file, required, checksums, injector)) { + // Not ready, come back later. + injector.getHandler().postDelayed(() -> { + processRequiredChecksums(filesToChecksum, result, required, + onChecksumsReadyListener, injector, startTime); + }, PROCESS_REQUIRED_CHECKSUMS_DELAY_MILLIS); + return; + } + + getRequiredApkChecksums(split, file, required, checksums); + } + allChecksums.addAll(checksums.values()); + } catch (Throwable e) { + Slog.e(TAG, "Required checksum calculation error", e); + } + } + + try { + onChecksumsReadyListener.onChecksumsReady(allChecksums); + } catch (RemoteException e) { + Slog.w(TAG, e); + } + } + + /** + * Fetch readily available checksums - enforced by kernel or provided by Installer. + * + * @param split split name, null for base + * @param file to fetch checksums for + * @param types mask to fetch checksums + * @param installerPackageName package name of the installer of the packages + * @param trustedInstallers array of certificate to trust, two specific cases: + * null - trust anybody, + * [] - trust nobody. + * @param checksums resulting checksums + */ + private static void getAvailableApkChecksums(String split, File file, + @Checksum.TypeMask int types, + @Nullable String installerPackageName, + @Nullable Certificate[] trustedInstallers, + Map checksums, + @NonNull Injector injector) { + if (!file.exists()) { + return; + } + final String filePath = file.getAbsolutePath(); + + // Always available: FSI or IncFs. + if (isRequired(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, types, checksums)) { + // Hashes in fs-verity and IncFS are always verified. + ApkChecksum checksum = extractHashFromFS(split, filePath); + if (checksum != null) { + checksums.put(checksum.getType(), checksum); + } + } + + // System enforced: v2/v3. + if (isRequired(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, types, checksums) || isRequired( + TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, types, checksums)) { + Map v2v3checksums = extractHashFromV2V3Signature( + split, filePath, types); + if (v2v3checksums != null) { + checksums.putAll(v2v3checksums); + } + } + + // Note: this compares installer and system digests internally and + // has to be called right after all system digests are populated. + getInstallerChecksums(split, file, types, installerPackageName, trustedInstallers, + checksums, injector); + } + + private static void getInstallerChecksums(String split, File file, + @Checksum.TypeMask int types, + @Nullable String installerPackageName, + @Nullable Certificate[] trustedInstallers, + Map checksums, + @NonNull Injector injector) { + if (PackageManagerServiceUtils.isInstalledByAdb(installerPackageName)) { + return; + } + if (trustedInstallers != null && trustedInstallers.length == 0) { + return; + } + + final File digestsFile = findDigestsForFile(file); + if (digestsFile == null) { + return; + } + final File signatureFile = findSignatureForDigests(digestsFile); + + try { + final Checksum[] digests = readChecksums(digestsFile); + final Signature[] certs; + final Signature[] pastCerts; + + if (signatureFile != null) { + final Certificate[] certificates = verifySignature(digests, + Files.readAllBytes(signatureFile.toPath())); + if (certificates == null || certificates.length == 0) { + Slog.e(TAG, "Error validating signature"); + return; + } + + certs = new Signature[certificates.length]; + for (int i = 0, size = certificates.length; i < size; i++) { + certs[i] = new Signature(certificates[i].getEncoded()); + } + + pastCerts = null; + } else { + final AndroidPackage installer = injector.getPackageManagerInternal().getPackage( + installerPackageName); + if (installer == null) { + Slog.e(TAG, "Installer package not found."); + return; + } + + // Obtaining array of certificates used for signing the installer package. + certs = installer.getSigningDetails().getSignatures(); + pastCerts = installer.getSigningDetails().getPastSigningCertificates(); + } + if (certs == null || certs.length == 0 || certs[0] == null) { + Slog.e(TAG, "Can't obtain certificates."); + return; + } + + // According to V2/V3 signing schema, the first certificate corresponds to the public + // key in the signing block. + byte[] trustedCertBytes = certs[0].toByteArray(); + + final Set trusted = convertToSet(trustedInstallers); + + if (trusted != null && !trusted.isEmpty()) { + // Obtaining array of certificates used for signing the installer package. + Signature trustedCert = isTrusted(certs, trusted); + if (trustedCert == null) { + trustedCert = isTrusted(pastCerts, trusted); + } + if (trustedCert == null) { + return; + } + trustedCertBytes = trustedCert.toByteArray(); + } + + // Compare OS-enforced digests. + for (Checksum digest : digests) { + final ApkChecksum system = checksums.get(digest.getType()); + if (system != null && !Arrays.equals(system.getValue(), digest.getValue())) { + throw new InvalidParameterException("System digest " + digest.getType() + + " mismatch, can't bind installer-provided digests to the APK."); + } + } + + // Append missing digests. + for (Checksum digest : digests) { + if (isRequired(digest.getType(), types, checksums)) { + checksums.put(digest.getType(), + new ApkChecksum(split, digest, installerPackageName, trustedCertBytes)); + } + } + } catch (IOException e) { + Slog.e(TAG, "Error reading .digests or .signature", e); + } catch (NoSuchAlgorithmException | SignatureException | InvalidParameterException e) { + Slog.e(TAG, "Error validating digests. Invalid digests will be removed", e); + try { + Files.deleteIfExists(digestsFile.toPath()); + if (signatureFile != null) { + Files.deleteIfExists(signatureFile.toPath()); + } + } catch (IOException ignored) { + } + } catch (CertificateEncodingException e) { + Slog.e(TAG, "Error encoding trustedInstallers", e); + } + } + + /** + * Whether the file is available for checksumming or we need to wait. + */ + private static boolean needToWait(File file, + @Checksum.TypeMask int types, + Map checksums, + @NonNull Injector injector) throws IOException { + if (!isRequired(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, types, checksums) + && !isRequired(TYPE_WHOLE_MD5, types, checksums) + && !isRequired(TYPE_WHOLE_SHA1, types, checksums) + && !isRequired(TYPE_WHOLE_SHA256, types, checksums) + && !isRequired(TYPE_WHOLE_SHA512, types, checksums) + && !isRequired(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, types, checksums) + && !isRequired(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, types, checksums)) { + return false; + } + + final String filePath = file.getAbsolutePath(); + if (!IncrementalManager.isIncrementalPath(filePath)) { + return false; + } + + IncrementalManager manager = injector.getIncrementalManager(); + if (manager == null) { + Slog.e(TAG, "IncrementalManager is missing."); + return false; + } + IncrementalStorage storage = manager.openStorage(filePath); + if (storage == null) { + Slog.e(TAG, "IncrementalStorage is missing for a path on IncFs: " + filePath); + return false; + } + + return !storage.isFileFullyLoaded(filePath); + } + + /** + * Fetch or calculate checksums for the specific file. + * + * @param split split name, null for base + * @param file to fetch checksums for + * @param types mask to forcefully calculate if not available + * @param checksums resulting checksums + */ + private static void getRequiredApkChecksums(String split, File file, + @Checksum.TypeMask int types, + Map checksums) { + final String filePath = file.getAbsolutePath(); + + // Manually calculating required checksums if not readily available. + if (isRequired(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, types, checksums)) { + try { + byte[] generatedRootHash = VerityBuilder.generateFsVerityRootHash( + filePath, /*salt=*/null, + new ByteBufferFactory() { + @Override + public ByteBuffer create(int capacity) { + return ByteBuffer.allocate(capacity); + } + }); + checksums.put(TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, + new ApkChecksum(split, TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, + verityHashForFile(file, generatedRootHash))); + } catch (IOException | NoSuchAlgorithmException | DigestException e) { + Slog.e(TAG, "Error calculating WHOLE_MERKLE_ROOT_4K_SHA256", e); + } + } + + calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_MD5); + calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_SHA1); + calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_SHA256); + calculateChecksumIfRequested(checksums, split, file, types, TYPE_WHOLE_SHA512); + + calculatePartialChecksumsIfRequested(checksums, split, file, types); + } + + private static boolean isRequired(@Checksum.Type int type, + @Checksum.TypeMask int types, Map checksums) { + if ((types & type) == 0) { + return false; + } + if (checksums.containsKey(type)) { + return false; + } + return true; + } + + /** + * Signature class provides a fast way to compare certificates using their hashes. + * The hash is exactly the same as in X509/Certificate. + */ + private static Set convertToSet(@Nullable Certificate[] array) throws + CertificateEncodingException { + if (array == null) { + return null; + } + final Set set = new ArraySet<>(array.length); + for (Certificate item : array) { + set.add(new Signature(item.getEncoded())); + } + return set; + } + + private static Signature isTrusted(Signature[] signatures, Set trusted) { + if (signatures == null) { + return null; + } + for (Signature signature : signatures) { + if (trusted.contains(signature)) { + return signature; + } + } + return null; + } + + private static ApkChecksum extractHashFromFS(String split, String filePath) { + // verity first + if (VerityUtils.hasFsverity(filePath)) { + byte[] verityHash = VerityUtils.getFsverityDigest(filePath); + if (verityHash != null) { + return new ApkChecksum(split, TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, verityHash); + } + } + // v4 next + try { + ApkSignatureSchemeV4Verifier.VerifiedSigner signer = + ApkSignatureSchemeV4Verifier.extractCertificates(filePath); + byte[] rootHash = signer.contentDigests.getOrDefault( + CONTENT_DIGEST_VERITY_CHUNKED_SHA256, null); + if (rootHash != null) { + return new ApkChecksum(split, TYPE_WHOLE_MERKLE_ROOT_4K_SHA256, + verityHashForFile(new File(filePath), rootHash)); + } + } catch (SignatureNotFoundException e) { + // Nothing + } catch (SignatureException | SecurityException e) { + Slog.e(TAG, "V4 signature error", e); + } + return null; + } + + /** + * Returns fs-verity digest as described in + * https://www.kernel.org/doc/html/latest/filesystems/fsverity.html#fs-verity-descriptor + * @param file the Merkle tree is built over + * @param rootHash Merkle tree root hash + */ + static byte[] verityHashForFile(File file, byte[] rootHash) { + try { + ByteBuffer buffer = ByteBuffer.allocate(256); // sizeof(fsverity_descriptor) + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) 1); // __u8 version, must be 1 + buffer.put((byte) 1); // __u8 hash_algorithm, FS_VERITY_HASH_ALG_SHA256 + buffer.put((byte) 12); // __u8, FS_VERITY_LOG_BLOCKSIZE + buffer.put((byte) 0); // __u8, size of salt in bytes; 0 if none + buffer.putInt(0); // __le32 __reserved_0x04, must be 0 + buffer.putLong(file.length()); // __le64 data_size + buffer.put(rootHash); // root_hash, first 32 bytes + final int padding = 32 + 32 + 144; // root_hash, last 32 bytes, we are using sha256. + // salt, 32 bytes + // reserved, 144 bytes + for (int i = 0; i < padding; ++i) { + buffer.put((byte) 0); + } + + buffer.flip(); + + final MessageDigest md = MessageDigest.getInstance(ALGO_SHA256); + md.update(buffer); + return md.digest(); + } catch (NoSuchAlgorithmException e) { + Slog.e(TAG, "Device does not support MessageDigest algorithm", e); + return null; + } + } + + private static Map extractHashFromV2V3Signature( + String split, String filePath, int types) { + Map contentDigests = null; + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final ParseResult result = + ApkSignatureVerifier.verifySignaturesInternal(input, filePath, + SignatureSchemeVersion.SIGNING_BLOCK_V2, false /*verifyFull*/, false); + if (result.isError()) { + if (!(result.getException() instanceof SignatureNotFoundException)) { + Slog.e(TAG, "Signature verification error", result.getException()); + } + } else { + contentDigests = result.getResult().contentDigests; + } + + if (contentDigests == null) { + return null; + } + + Map checksums = new ArrayMap<>(); + if ((types & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256) != 0) { + byte[] hash = contentDigests.getOrDefault(CONTENT_DIGEST_CHUNKED_SHA256, null); + if (hash != null) { + checksums.put(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, + new ApkChecksum(split, TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256, hash)); + } + } + if ((types & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512) != 0) { + byte[] hash = contentDigests.getOrDefault(CONTENT_DIGEST_CHUNKED_SHA512, null); + if (hash != null) { + checksums.put(TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, + new ApkChecksum(split, TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512, hash)); + } + } + return checksums; + } + + private static String getMessageDigestAlgoForChecksumKind(int type) + throws NoSuchAlgorithmException { + switch (type) { + case TYPE_WHOLE_MD5: + return ALGO_MD5; + case TYPE_WHOLE_SHA1: + return ALGO_SHA1; + case TYPE_WHOLE_SHA256: + return ALGO_SHA256; + case TYPE_WHOLE_SHA512: + return ALGO_SHA512; + default: + throw new NoSuchAlgorithmException("Invalid checksum type: " + type); + } + } + + private static void calculateChecksumIfRequested(Map checksums, + String split, File file, int required, int type) { + if ((required & type) != 0 && !checksums.containsKey(type)) { + final byte[] checksum = getApkChecksum(file, type); + if (checksum != null) { + checksums.put(type, new ApkChecksum(split, type, checksum)); + } + } + } + + static final int MIN_BUFFER_SIZE = 4 * 1024; + static final int MAX_BUFFER_SIZE = 128 * 1024; + + private static byte[] getApkChecksum(File file, int type) { + final int bufferSize = (int) Math.max(MIN_BUFFER_SIZE, + Math.min(MAX_BUFFER_SIZE, file.length())); + try (FileInputStream fis = new FileInputStream(file)) { + final byte[] buffer = new byte[bufferSize]; + int nread = 0; + + final String algo = getMessageDigestAlgoForChecksumKind(type); + MessageDigest md = MessageDigest.getInstance(algo); + while ((nread = fis.read(buffer)) != -1) { + md.update(buffer, 0, nread); + } + + return md.digest(); + } catch (IOException e) { + Slog.e(TAG, "Error reading " + file.getAbsolutePath() + " to compute hash.", e); + return null; + } catch (NoSuchAlgorithmException e) { + Slog.e(TAG, "Device does not support MessageDigest algorithm", e); + return null; + } + } + + private static int[] getContentDigestAlgos(boolean needSignatureSha256, + boolean needSignatureSha512) { + if (needSignatureSha256 && needSignatureSha512) { + // Signature block present, but no digests??? + return new int[]{CONTENT_DIGEST_CHUNKED_SHA256, CONTENT_DIGEST_CHUNKED_SHA512}; + } else if (needSignatureSha256) { + return new int[]{CONTENT_DIGEST_CHUNKED_SHA256}; + } else { + return new int[]{CONTENT_DIGEST_CHUNKED_SHA512}; + } + } + + private static int getChecksumKindForContentDigestAlgo(int contentDigestAlgo) { + switch (contentDigestAlgo) { + case CONTENT_DIGEST_CHUNKED_SHA256: + return TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256; + case CONTENT_DIGEST_CHUNKED_SHA512: + return TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512; + default: + return -1; + } + } + + private static void calculatePartialChecksumsIfRequested(Map checksums, + String split, File file, int required) { + boolean needSignatureSha256 = + (required & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256) != 0 && !checksums.containsKey( + TYPE_PARTIAL_MERKLE_ROOT_1M_SHA256); + boolean needSignatureSha512 = + (required & TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512) != 0 && !checksums.containsKey( + TYPE_PARTIAL_MERKLE_ROOT_1M_SHA512); + if (!needSignatureSha256 && !needSignatureSha512) { + return; + } + + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + SignatureInfo signatureInfo = null; + try { + signatureInfo = ApkSignatureSchemeV3Verifier.findSignature(raf); + } catch (SignatureNotFoundException e) { + try { + signatureInfo = ApkSignatureSchemeV2Verifier.findSignature(raf); + } catch (SignatureNotFoundException ee) { + } + } + if (signatureInfo == null) { + Slog.e(TAG, "V2/V3 signatures not found in " + file.getAbsolutePath()); + return; + } + + final int[] digestAlgos = getContentDigestAlgos(needSignatureSha256, + needSignatureSha512); + byte[][] digests = ApkSigningBlockUtils.computeContentDigestsPer1MbChunk(digestAlgos, + raf.getFD(), signatureInfo); + for (int i = 0, size = digestAlgos.length; i < size; ++i) { + int checksumKind = getChecksumKindForContentDigestAlgo(digestAlgos[i]); + if (checksumKind != -1) { + checksums.put(checksumKind, new ApkChecksum(split, checksumKind, digests[i])); + } + } + } catch (IOException | DigestException e) { + Slog.e(TAG, "Error computing hash.", e); + } + } +} diff --git a/aosp/frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java b/aosp/frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java new file mode 100644 index 0000000000000000000000000000000000000000..21e4e6bef4fb4d7e3950b9d90a940d7af99b2034 --- /dev/null +++ b/aosp/frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -0,0 +1,5917 @@ +/* + * Copyright (C) 2014 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 com.android.server.pm; + +import static android.app.AppOpsManager.MODE_DEFAULT; +import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_INSTALLED_BY_DO; +import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_UPDATED_BY_DO; +import static android.content.pm.DataLoaderType.INCREMENTAL; +import static android.content.pm.DataLoaderType.STREAMING; +import static android.content.pm.PackageInstaller.LOCATION_DATA_APP; +import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; +import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; +import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH; +import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED; +import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE; +import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; +import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR; +import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; +import static android.content.pm.PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE; +import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SPLIT; +import static android.content.pm.PackageManager.INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE; +import static android.content.pm.PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES; +import static android.content.pm.PackageManager.INSTALL_STAGED; +import static android.content.pm.PackageManager.INSTALL_SUCCEEDED; +import static android.os.Process.INVALID_UID; +import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE; +import static android.system.OsConstants.O_CREAT; +import static android.system.OsConstants.O_RDONLY; +import static android.system.OsConstants.O_WRONLY; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.internal.util.XmlUtils.readBitmapAttribute; +import static com.android.internal.util.XmlUtils.readByteArrayAttribute; +import static com.android.internal.util.XmlUtils.readStringAttribute; +import static com.android.internal.util.XmlUtils.readUriAttribute; +import static com.android.internal.util.XmlUtils.writeBooleanAttribute; +import static com.android.internal.util.XmlUtils.writeByteArrayAttribute; +import static com.android.internal.util.XmlUtils.writeStringAttribute; +import static com.android.internal.util.XmlUtils.writeUriAttribute; +import static com.android.server.pm.PackageInstallerService.prepareStageDir; +import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_NAME; +import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; +import static com.android.server.pm.PackageManagerServiceUtils.isInstalledByAdb; +import static com.android.server.pm.PackageManagerShellCommandDataLoader.Metadata; + +import android.Manifest; +import android.annotation.AnyThread; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.WorkerThread; +import android.app.AppOpsManager; +import android.app.BroadcastOptions; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.admin.DevicePolicyEventLogger; +import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManagerInternal; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; +import android.compat.annotation.EnabledSince; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.pm.ApplicationInfo; +import android.content.pm.Checksum; +import android.content.pm.DataLoaderManager; +import android.content.pm.DataLoaderParams; +import android.content.pm.DataLoaderParamsParcel; +import android.content.pm.FileSystemControlParcel; +import android.content.pm.IDataLoader; +import android.content.pm.IDataLoaderStatusListener; +import android.content.pm.IOnChecksumsReadyListener; +import android.content.pm.IPackageInstallObserver2; +import android.content.pm.IPackageInstallerSession; +import android.content.pm.IPackageInstallerSessionFileSystemConnector; +import android.content.pm.IPackageLoadingProgressCallback; +import android.content.pm.InstallSourceInfo; +import android.content.pm.InstallationFile; +import android.content.pm.InstallationFileParcel; +import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageInstaller.PreapprovalDetails; +import android.content.pm.PackageInstaller.SessionInfo; +import android.content.pm.PackageInstaller.SessionParams; +import android.content.pm.PackageInstaller.UnarchivalStatus; +import android.content.pm.PackageInstaller.UserActionReason; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.PackageInfoFlags; +import android.content.pm.PackageManagerInternal; +import android.content.pm.SigningDetails; +import android.content.pm.dex.DexMetadataHelper; +import android.content.pm.parsing.ApkLite; +import android.content.pm.parsing.ApkLiteParseUtils; +import android.content.pm.parsing.PackageLite; +import android.content.pm.parsing.result.ParseResult; +import android.content.pm.parsing.result.ParseTypeImpl; +import android.content.pm.verify.domain.DomainSet; +import android.content.res.ApkAssets; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.icu.util.ULocale; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.FileBridge; +import android.os.FileUtils; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.ParcelableException; +import android.os.Process; +import android.os.RemoteException; +import android.os.RevocableFileDescriptor; +import android.os.SELinux; +import android.os.ServiceManager; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.os.incremental.IStorageHealthListener; +import android.os.incremental.IncrementalFileStorages; +import android.os.incremental.IncrementalManager; +import android.os.incremental.PerUidReadTimeouts; +import android.os.incremental.StorageHealthCheckParams; +import android.os.incremental.V4Signature; +import android.os.storage.StorageManager; +import android.provider.DeviceConfig; +import android.provider.Settings.Global; +import android.stats.devicepolicy.DevicePolicyEnums; +import android.system.ErrnoException; +import android.system.Int64Ref; +import android.system.Os; +import android.system.OsConstants; +import android.system.StructStat; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.EventLog; +import android.util.ExceptionUtils; +import android.util.IntArray; +import android.util.Log; +import android.util.MathUtils; +import android.util.Slog; +import android.util.SparseArray; +import android.util.apk.ApkSignatureVerifier; + +import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.compat.IPlatformCompat; +import com.android.internal.content.InstallLocationUtils; +import com.android.internal.content.NativeLibraryHelper; +import com.android.internal.messages.nano.SystemMessageProto; +import com.android.internal.os.SomeArgs; +import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; +import com.android.internal.security.VerityUtils; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.CollectionUtils; +import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.IndentingPrintWriter; +import com.android.internal.util.Preconditions; +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.LocalServices; +import com.android.server.pm.Installer.InstallerException; +import com.android.server.pm.dex.DexManager; +import com.android.server.pm.pkg.AndroidPackage; +import com.android.server.pm.pkg.PackageStateInternal; + +import libcore.io.IoUtils; +import libcore.util.EmptyArray; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileFilter; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +public class PackageInstallerSession extends IPackageInstallerSession.Stub { + private static final String TAG = "PackageInstallerSession"; + private static final boolean LOGD = true; + private static final String REMOVE_MARKER_EXTENSION = ".removed"; + + private static final int MSG_ON_SESSION_SEALED = 1; + private static final int MSG_STREAM_VALIDATE_AND_COMMIT = 2; + private static final int MSG_INSTALL = 3; + private static final int MSG_ON_PACKAGE_INSTALLED = 4; + private static final int MSG_SESSION_VALIDATION_FAILURE = 5; + private static final int MSG_PRE_APPROVAL_REQUEST = 6; + + /** XML constants used for persisting a session */ + static final String TAG_SESSION = "session"; + static final String TAG_CHILD_SESSION = "childSession"; + static final String TAG_SESSION_FILE = "sessionFile"; + static final String TAG_SESSION_CHECKSUM = "sessionChecksum"; + static final String TAG_SESSION_CHECKSUM_SIGNATURE = "sessionChecksumSignature"; + private static final String TAG_GRANTED_RUNTIME_PERMISSION = "granted-runtime-permission"; + private static final String TAG_GRANT_PERMISSION = "grant-permission"; + private static final String TAG_DENY_PERMISSION = "deny-permission"; + private static final String TAG_WHITELISTED_RESTRICTED_PERMISSION = + "whitelisted-restricted-permission"; + private static final String TAG_AUTO_REVOKE_PERMISSIONS_MODE = + "auto-revoke-permissions-mode"; + + static final String TAG_PRE_VERIFIED_DOMAINS = "preVerifiedDomains"; + private static final String ATTR_SESSION_ID = "sessionId"; + private static final String ATTR_USER_ID = "userId"; + private static final String ATTR_INSTALLER_PACKAGE_NAME = "installerPackageName"; + private static final String ATTR_INSTALLER_PACKAGE_UID = "installerPackageUid"; + private static final String ATTR_UPDATE_OWNER_PACKAGE_NAME = "updateOwnererPackageName"; + private static final String ATTR_INSTALLER_ATTRIBUTION_TAG = "installerAttributionTag"; + private static final String ATTR_INSTALLER_UID = "installerUid"; + private static final String ATTR_INITIATING_PACKAGE_NAME = + "installInitiatingPackageName"; + private static final String ATTR_ORIGINATING_PACKAGE_NAME = + "installOriginatingPackageName"; + private static final String ATTR_CREATED_MILLIS = "createdMillis"; + private static final String ATTR_UPDATED_MILLIS = "updatedMillis"; + private static final String ATTR_COMMITTED_MILLIS = "committedMillis"; + private static final String ATTR_SESSION_STAGE_DIR = "sessionStageDir"; + private static final String ATTR_SESSION_STAGE_CID = "sessionStageCid"; + private static final String ATTR_PREPARED = "prepared"; + private static final String ATTR_COMMITTED = "committed"; + private static final String ATTR_DESTROYED = "destroyed"; + private static final String ATTR_SEALED = "sealed"; + private static final String ATTR_MULTI_PACKAGE = "multiPackage"; + private static final String ATTR_PARENT_SESSION_ID = "parentSessionId"; + private static final String ATTR_STAGED_SESSION = "stagedSession"; + private static final String ATTR_IS_READY = "isReady"; + private static final String ATTR_IS_FAILED = "isFailed"; + private static final String ATTR_IS_APPLIED = "isApplied"; + private static final String ATTR_PACKAGE_SOURCE = "packageSource"; + private static final String ATTR_SESSION_ERROR_CODE = "errorCode"; + private static final String ATTR_SESSION_ERROR_MESSAGE = "errorMessage"; + private static final String ATTR_MODE = "mode"; + private static final String ATTR_INSTALL_FLAGS = "installFlags"; + private static final String ATTR_INSTALL_LOCATION = "installLocation"; + private static final String ATTR_SIZE_BYTES = "sizeBytes"; + private static final String ATTR_APP_PACKAGE_NAME = "appPackageName"; + @Deprecated + private static final String ATTR_APP_ICON = "appIcon"; + private static final String ATTR_APP_LABEL = "appLabel"; + private static final String ATTR_ORIGINATING_URI = "originatingUri"; + private static final String ATTR_ORIGINATING_UID = "originatingUid"; + private static final String ATTR_REFERRER_URI = "referrerUri"; + private static final String ATTR_ABI_OVERRIDE = "abiOverride"; + private static final String ATTR_VOLUME_UUID = "volumeUuid"; + private static final String ATTR_NAME = "name"; + private static final String ATTR_INSTALL_REASON = "installRason"; + private static final String ATTR_IS_DATALOADER = "isDataLoader"; + private static final String ATTR_DATALOADER_TYPE = "dataLoaderType"; + private static final String ATTR_DATALOADER_PACKAGE_NAME = "dataLoaderPackageName"; + private static final String ATTR_DATALOADER_CLASS_NAME = "dataLoaderClassName"; + private static final String ATTR_DATALOADER_ARGUMENTS = "dataLoaderArguments"; + private static final String ATTR_LOCATION = "location"; + private static final String ATTR_LENGTH_BYTES = "lengthBytes"; + private static final String ATTR_METADATA = "metadata"; + private static final String ATTR_SIGNATURE = "signature"; + private static final String ATTR_CHECKSUM_KIND = "checksumKind"; + private static final String ATTR_CHECKSUM_VALUE = "checksumValue"; + private static final String ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT = + "applicationEnabledSettingPersistent"; + private static final String ATTR_DOMAIN = "domain"; + + private static final String PROPERTY_NAME_INHERIT_NATIVE = "pi.inherit_native_on_dont_kill"; + private static final int[] EMPTY_CHILD_SESSION_ARRAY = EmptyArray.INT; + private static final InstallationFile[] EMPTY_INSTALLATION_FILE_ARRAY = {}; + + private static final String SYSTEM_DATA_LOADER_PACKAGE = "android"; + private static final String APEX_FILE_EXTENSION = ".apex"; + + private static final int INCREMENTAL_STORAGE_BLOCKED_TIMEOUT_MS = 2000; + private static final int INCREMENTAL_STORAGE_UNHEALTHY_TIMEOUT_MS = 7000; + private static final int INCREMENTAL_STORAGE_UNHEALTHY_MONITORING_MS = 60000; + + /** + * If an app being installed targets {@link Build.VERSION_CODES#S API 31} and above, the app + * can be installed without user action. + * See {@link PackageInstaller.SessionParams#setRequireUserAction} for other conditions required + * to be satisfied for a silent install. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.S) + private static final long SILENT_INSTALL_ALLOWED = 265131695L; + + /** + * The system supports pre-approval and update ownership features from + * {@link Build.VERSION_CODES#UPSIDE_DOWN_CAKE API 34}. The change id is used to make sure + * the system includes the fix of pre-approval with update ownership case. When checking the + * change id, if it is disabled, it means the build includes the fix. The more detail is on + * b/293644536. + * See {@link PackageInstaller.SessionParams#setRequestUpdateOwnership(boolean)} and + * {@link #requestUserPreapproval(PreapprovalDetails, IntentSender)} for more details. + */ + @Disabled + @ChangeId + private static final long PRE_APPROVAL_WITH_UPDATE_OWNERSHIP_FIX = 293644536L; + + /** + * The default value of {@link #mValidatedTargetSdk} is {@link Integer#MAX_VALUE}. If {@link + * #mValidatedTargetSdk} is compared with {@link Build.VERSION_CODES#S} before getting the + * target sdk version from a validated apk in {@link #validateApkInstallLocked()}, the compared + * result will not trigger any user action in + * {@link #checkUserActionRequirement(PackageInstallerSession, IntentSender)}. + */ + private static final int INVALID_TARGET_SDK_VERSION = Integer.MAX_VALUE; + + /** + * Byte size limit for app metadata. + * + * Flag type: {@code long} + * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE + */ + private static final String PROPERTY_APP_METADATA_BYTE_SIZE_LIMIT = + "app_metadata_byte_size_limit"; + + /** Default byte size limit for app metadata */ + private static final long DEFAULT_APP_METADATA_BYTE_SIZE_LIMIT = 32000; + + static final int APP_METADATA_FILE_ACCESS_MODE = 0640; + + /** + * Throws IllegalArgumentException if the {@link IntentSender} from an immutable + * {@link android.app.PendingIntent} when caller has a target SDK of API + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} or above. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + private static final long THROW_EXCEPTION_COMMIT_WITH_IMMUTABLE_PENDING_INTENT = 240618202L; + + /** + * Configurable maximum number of pre-verified domains allowed to be added to the session. + * Flag type: {@code long} + * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE + */ + private static final String PROPERTY_PRE_VERIFIED_DOMAINS_COUNT_LIMIT = + "pre_verified_domains_count_limit"; + /** + * Configurable maximum string length of each pre-verified domain. + * Flag type: {@code long} + * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE + */ + private static final String PROPERTY_PRE_VERIFIED_DOMAIN_LENGTH_LIMIT = + "pre_verified_domain_length_limit"; + /** Default max number of pre-verified domains */ + private static final long DEFAULT_PRE_VERIFIED_DOMAINS_COUNT_LIMIT = 1000; + /** Default max string length of each pre-verified domain */ + private static final long DEFAULT_PRE_VERIFIED_DOMAIN_LENGTH_LIMIT = 256; + + // TODO: enforce INSTALL_ALLOW_TEST + // TODO: enforce INSTALL_ALLOW_DOWNGRADE + + private final PackageInstallerService.InternalCallback mCallback; + private final Context mContext; + private final PackageManagerService mPm; + private final Installer mInstaller; + private final Handler mHandler; + private final PackageSessionProvider mSessionProvider; + private final SilentUpdatePolicy mSilentUpdatePolicy; + /** + * Note all calls must be done outside {@link #mLock} to prevent lock inversion. + */ + private final StagingManager mStagingManager; + + final int sessionId; + final int userId; + final SessionParams params; + final long createdMillis; + + /** Used for tracking whether user action was required for an install. */ + @Nullable + private Boolean mUserActionRequired; + + /** Staging location where client data is written. */ + final File stageDir; + final String stageCid; + + private final AtomicInteger mActiveCount = new AtomicInteger(); + + private final Object mLock = new Object(); + + /** + * Used to detect and reject concurrent access to this session object to ensure mutation + * to multiple objects like {@link #addChildSessionId} are done atomically. + */ + private final AtomicBoolean mTransactionLock = new AtomicBoolean(false); + + /** Timestamp of the last time this session changed state */ + @GuardedBy("mLock") + private long updatedMillis; + + /** Timestamp of the time this session is committed */ + @GuardedBy("mLock") + private long committedMillis; + + /** Uid of the creator of this session. */ + private final int mOriginalInstallerUid; + + /** Package name of the app that created the installation session. */ + private final String mOriginalInstallerPackageName; + + /** Uid of the owner of the installer session */ + private volatile int mInstallerUid; + + /** Where this install request came from */ + @GuardedBy("mLock") + private InstallSource mInstallSource; + + private final Object mProgressLock = new Object(); + + @GuardedBy("mProgressLock") + private float mClientProgress = 0; + @GuardedBy("mProgressLock") + private float mInternalProgress = 0; + + @GuardedBy("mProgressLock") + private float mProgress = 0; + @GuardedBy("mProgressLock") + private float mReportedProgress = -1; + @GuardedBy("mProgressLock") + private float mIncrementalProgress = 0; + + /** State of the session. */ + @GuardedBy("mLock") + private boolean mPrepared = false; + @GuardedBy("mLock") + private boolean mSealed = false; + @GuardedBy("mLock") + private boolean mShouldBeSealed = false; + + private final AtomicBoolean mPreapprovalRequested = new AtomicBoolean(false); + private final AtomicBoolean mCommitted = new AtomicBoolean(false); + + /** + * True if staging files are being used by external entities like {@link PackageSessionVerifier} + * or {@link PackageManagerService} which means it is not safe for {@link #abandon()} to clean + * up the files. + */ + @GuardedBy("mLock") + private boolean mStageDirInUse = false; + + /** + * True if the verification is already in progress. This is used to prevent running + * verification again while one is already in progress which will break internal states. + * + * Worker thread only. + */ + private boolean mVerificationInProgress = false; + + /** Permissions have been accepted by the user (see {@link #setPermissionsResult}) */ + @GuardedBy("mLock") + private boolean mPermissionsManuallyAccepted = false; + + @GuardedBy("mLock") + private int mFinalStatus; + @GuardedBy("mLock") + private String mFinalMessage; + + @GuardedBy("mLock") + private final ArrayList mFds = new ArrayList<>(); + @GuardedBy("mLock") + private final ArrayList mBridges = new ArrayList<>(); + + @GuardedBy("mLock") + private IntentSender mRemoteStatusReceiver; + + @GuardedBy("mLock") + private IntentSender mPreapprovalRemoteStatusReceiver; + + @GuardedBy("mLock") + private PreapprovalDetails mPreapprovalDetails; + + /** Fields derived from commit parsing */ + @GuardedBy("mLock") + private String mPackageName; + @GuardedBy("mLock") + private long mVersionCode; + @GuardedBy("mLock") + private SigningDetails mSigningDetails; + @GuardedBy("mLock") + private final SparseArray mChildSessions = new SparseArray<>(); + @GuardedBy("mLock") + private int mParentSessionId; + + @GuardedBy("mLock") + private boolean mHasDeviceAdminReceiver; + + @GuardedBy("mLock") + private int mUserActionRequirement; + + @GuardedBy("mLock") + private DomainSet mPreVerifiedDomains; + + static class FileEntry { + private final int mIndex; + private final InstallationFile mFile; + + FileEntry(int index, InstallationFile file) { + this.mIndex = index; + this.mFile = file; + } + + int getIndex() { + return this.mIndex; + } + + InstallationFile getFile() { + return this.mFile; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FileEntry)) { + return false; + } + final FileEntry rhs = (FileEntry) obj; + return (mFile.getLocation() == rhs.mFile.getLocation()) && TextUtils.equals( + mFile.getName(), rhs.mFile.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(mFile.getLocation(), mFile.getName()); + } + } + + @GuardedBy("mLock") + private final ArraySet mFiles = new ArraySet<>(); + + static class PerFileChecksum { + private final Checksum[] mChecksums; + private final byte[] mSignature; + + PerFileChecksum(Checksum[] checksums, byte[] signature) { + mChecksums = checksums; + mSignature = signature; + } + + Checksum[] getChecksums() { + return this.mChecksums; + } + + byte[] getSignature() { + return this.mSignature; + } + } + + @GuardedBy("mLock") + private final ArrayMap mChecksums = new ArrayMap<>(); + + @GuardedBy("mLock") + private boolean mSessionApplied; + @GuardedBy("mLock") + private boolean mSessionReady; + @GuardedBy("mLock") + private boolean mSessionFailed; + @GuardedBy("mLock") + private int mSessionErrorCode = PackageManager.INSTALL_UNKNOWN; + @GuardedBy("mLock") + private String mSessionErrorMessage; + + @Nullable + final StagedSession mStagedSession; + + /** + * The callback to run when pre-reboot verification has ended. Used by {@link #abandon()} + * to delay session clean-up until it is safe to do so. + */ + @GuardedBy("mLock") + @Nullable + private Runnable mPendingAbandonCallback; + + @VisibleForTesting + public class StagedSession implements StagingManager.StagedSession { + @Override + public List getChildSessions() { + if (!params.isMultiPackage) { + return Collections.EMPTY_LIST; + } + synchronized (mLock) { + int size = mChildSessions.size(); + List childSessions = new ArrayList<>(size); + for (int i = 0; i < size; ++i) { + childSessions.add(mChildSessions.valueAt(i).mStagedSession); + } + return childSessions; + } + } + + @Override + public SessionParams sessionParams() { + return params; + } + + @Override + public boolean isMultiPackage() { + return params.isMultiPackage; + } + + @Override + public boolean isApexSession() { + return (params.installFlags & PackageManager.INSTALL_APEX) != 0; + } + + @Override + public int sessionId() { + return sessionId; + } + + @Override + public boolean containsApexSession() { + return sessionContains((s) -> s.isApexSession()); + } + + @Override + public String getPackageName() { + return PackageInstallerSession.this.getPackageName(); + } + + @Override + public void setSessionReady() { + PackageInstallerSession.this.setSessionReady(); + } + + @Override + public void setSessionFailed(int errorCode, String errorMessage) { + PackageInstallerSession.this.setSessionFailed(errorCode, errorMessage); + } + + @Override + public void setSessionApplied() { + PackageInstallerSession.this.setSessionApplied(); + } + + @Override + public boolean containsApkSession() { + return PackageInstallerSession.this.containsApkSession(); + } + + /** + * Installs apks of staged session while skipping the verification process for a committed + * and ready session. + * + * @return a CompletableFuture that will be completed when installation completes. + */ + @Override + public CompletableFuture installSession() { + assertCallerIsOwnerOrRootOrSystem(); + assertNotChild("StagedSession#installSession"); + Preconditions.checkArgument(isCommitted() && isSessionReady()); + return install(); + } + + @Override + public boolean hasParentSessionId() { + return PackageInstallerSession.this.hasParentSessionId(); + } + + @Override + public int getParentSessionId() { + return PackageInstallerSession.this.getParentSessionId(); + } + + @Override + public boolean isCommitted() { + return PackageInstallerSession.this.isCommitted(); + } + + @Override + public boolean isInTerminalState() { + return PackageInstallerSession.this.isInTerminalState(); + } + + @Override + public boolean isDestroyed() { + return PackageInstallerSession.this.isDestroyed(); + } + + @Override + public long getCommittedMillis() { + return PackageInstallerSession.this.getCommittedMillis(); + } + + @Override + public boolean sessionContains(Predicate filter) { + return PackageInstallerSession.this.sessionContains(s -> filter.test(s.mStagedSession)); + } + + @Override + public boolean isSessionReady() { + return PackageInstallerSession.this.isSessionReady(); + } + + @Override + public boolean isSessionApplied() { + return PackageInstallerSession.this.isSessionApplied(); + } + + @Override + public boolean isSessionFailed() { + return PackageInstallerSession.this.isSessionFailed(); + } + + @Override + public void abandon() { + PackageInstallerSession.this.abandon(); + } + + /** + * Resumes verification process for non-final committed staged session. + * + * Useful if a device gets rebooted before verification is complete and we need to restart + * the verification. + */ + @Override + public void verifySession() { + assertCallerIsOwnerOrRootOrSystem(); + if (isCommittedAndNotInTerminalState()) { + verify(); + } + } + + private boolean isCommittedAndNotInTerminalState() { + String errorMsg = null; + if (!isCommitted()) { + errorMsg = TextUtils.formatSimple("The session %d should be committed", sessionId); + } else if (isSessionApplied()) { + errorMsg = TextUtils.formatSimple("The session %d has applied", sessionId); + } else if (isSessionFailed()) { + synchronized (PackageInstallerSession.this.mLock) { + errorMsg = TextUtils.formatSimple("The session %d has failed with error: %s", + sessionId, PackageInstallerSession.this.mSessionErrorMessage); + } + } + if (errorMsg != null) { + Slog.e(TAG, "verifySession error: " + errorMsg); + setSessionFailed(INSTALL_FAILED_INTERNAL_ERROR, errorMsg); + onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR, errorMsg); + return false; + } + return true; + } + } + + /** + * Path to the validated base APK for this session, which may point at an + * APK inside the session (when the session defines the base), or it may + * point at the existing base APK (when adding splits to an existing app). + *

+ * This is used when confirming permissions, since we can't fully stage the + * session inside an ASEC before confirming with user. + */ + @GuardedBy("mLock") + private File mResolvedBaseFile; + + @GuardedBy("mLock") + private final List mResolvedStagedFiles = new ArrayList<>(); + @GuardedBy("mLock") + private final List mResolvedInheritedFiles = new ArrayList<>(); + @GuardedBy("mLock") + private final List mResolvedInstructionSets = new ArrayList<>(); + @GuardedBy("mLock") + private final List mResolvedNativeLibPaths = new ArrayList<>(); + + @GuardedBy("mLock") + private final Set mUnarchivalListeners = new ArraySet<>(); + + @GuardedBy("mLock") + private File mInheritedFilesBase; + @GuardedBy("mLock") + private boolean mVerityFoundForApks; + + /** + * Both flags should be guarded with mLock whenever changes need to be in lockstep. + * Ok to check without mLock in case the proper check is done later, e.g. status callbacks + * for DataLoaders with deferred processing. + */ + private volatile boolean mDestroyed = false; + private volatile boolean mDataLoaderFinished = false; + + @GuardedBy("mLock") + private IncrementalFileStorages mIncrementalFileStorages; + + @GuardedBy("mLock") + private PackageLite mPackageLite; + + /** + * Keep the target sdk of a validated apk. + */ + @GuardedBy("mLock") + private int mValidatedTargetSdk = INVALID_TARGET_SDK_VERSION; + + @UnarchivalStatus + private int mUnarchivalStatus = UNARCHIVAL_STATUS_UNSET; + + private static final FileFilter sAddedApkFilter = new FileFilter() { + @Override + public boolean accept(File file) { + // Installers can't stage directories, so it's fine to ignore + // entries like "lost+found". + if (file.isDirectory()) return false; + if (file.getName().endsWith(REMOVE_MARKER_EXTENSION)) return false; + if (file.getName().endsWith(V4Signature.EXT)) return false; + if (isAppMetadata(file)) return false; + if (DexMetadataHelper.isDexMetadataFile(file)) return false; + if (VerityUtils.isFsveritySignatureFile(file)) return false; + if (ApkChecksums.isDigestOrDigestSignatureFile(file)) return false; + return true; + } + }; + private static final FileFilter sAddedFilter = new FileFilter() { + @Override + public boolean accept(File file) { + // Installers can't stage directories, so it's fine to ignore + // entries like "lost+found". + if (file.isDirectory()) return false; + if (file.getName().endsWith(REMOVE_MARKER_EXTENSION)) return false; + return true; + } + }; + private static final FileFilter sRemovedFilter = new FileFilter() { + @Override + public boolean accept(File file) { + if (file.isDirectory()) return false; + if (!file.getName().endsWith(REMOVE_MARKER_EXTENSION)) return false; + return true; + } + }; + + static boolean isDataLoaderInstallation(SessionParams params) { + return params.dataLoaderParams != null; + } + + static boolean isSystemDataLoaderInstallation(SessionParams params) { + if (!isDataLoaderInstallation(params)) { + return false; + } + return SYSTEM_DATA_LOADER_PACKAGE.equals( + params.dataLoaderParams.getComponentName().getPackageName()); + } + + static boolean isArchivedInstallation(int installFlags) { + return (installFlags & PackageManager.INSTALL_ARCHIVED) != 0; + } + + private final Handler.Callback mHandlerCallback = new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_ON_SESSION_SEALED: + handleSessionSealed(); + break; + case MSG_STREAM_VALIDATE_AND_COMMIT: + handleStreamValidateAndCommit(); + break; + case MSG_INSTALL: + handleInstall(); + break; + case MSG_ON_PACKAGE_INSTALLED: + final SomeArgs args = (SomeArgs) msg.obj; + final String packageName = (String) args.arg1; + final String message = (String) args.arg2; + final Bundle extras = (Bundle) args.arg3; + final IntentSender statusReceiver = (IntentSender) args.arg4; + final int returnCode = args.argi1; + final boolean isPreapproval = args.argi2 == 1; + args.recycle(); + + sendOnPackageInstalled(mContext, statusReceiver, sessionId, + isInstallerDeviceOwnerOrAffiliatedProfileOwner(), userId, + packageName, returnCode, isPreapproval, message, extras); + + break; + case MSG_SESSION_VALIDATION_FAILURE: + final int error = msg.arg1; + final String detailMessage = (String) msg.obj; + onSessionValidationFailure(error, detailMessage); + break; + case MSG_PRE_APPROVAL_REQUEST: + handlePreapprovalRequest(); + break; + } + + return true; + } + }; + + private boolean isDataLoaderInstallation() { + return isDataLoaderInstallation(this.params); + } + + private boolean isStreamingInstallation() { + return isDataLoaderInstallation() && params.dataLoaderParams.getType() == STREAMING; + } + + private boolean isIncrementalInstallation() { + return isDataLoaderInstallation() && params.dataLoaderParams.getType() == INCREMENTAL; + } + + private boolean isSystemDataLoaderInstallation() { + return isSystemDataLoaderInstallation(this.params); + } + + private boolean isArchivedInstallation() { + return isArchivedInstallation(this.params.installFlags); + } + + /** + * @return {@code true} iff the installing is app an device owner or affiliated profile owner. + */ + private boolean isInstallerDeviceOwnerOrAffiliatedProfileOwner() { + assertNotLocked("isInstallerDeviceOwnerOrAffiliatedProfileOwner"); + if (userId != UserHandle.getUserId(getInstallerUid())) { + return false; + } + DevicePolicyManagerInternal dpmi = + LocalServices.getService(DevicePolicyManagerInternal.class); + // It may wait for a long time to finish {@code dpmi.canSilentlyInstallPackage}. + // Please don't acquire mLock before calling {@code dpmi.canSilentlyInstallPackage}. + return dpmi != null && dpmi.canSilentlyInstallPackage( + getInstallSource().mInstallerPackageName, mInstallerUid); + } + + private boolean isEmergencyInstallerEnabled(String packageName, Computer snapshot) { + final PackageStateInternal ps = snapshot.getPackageStateInternal(packageName); + if (ps == null || ps.getPkg() == null) { + return false; + } + String emergencyInstaller = ps.getPkg().getEmergencyInstaller(); + if (emergencyInstaller == null || !ArrayUtils.contains( + snapshot.getPackagesForUid(mInstallerUid), + emergencyInstaller)) { + return false; + } + return (snapshot.checkUidPermission(Manifest.permission.EMERGENCY_INSTALL_PACKAGES, + mInstallerUid) == PackageManager.PERMISSION_GRANTED); + } + + private static final int USER_ACTION_NOT_NEEDED = 0; + private static final int USER_ACTION_REQUIRED = 1; + private static final int USER_ACTION_PENDING_APK_PARSING = 2; + private static final int USER_ACTION_REQUIRED_UPDATE_OWNER_REMINDER = 3; + + @IntDef({ + USER_ACTION_NOT_NEEDED, + USER_ACTION_REQUIRED, + USER_ACTION_PENDING_APK_PARSING, + USER_ACTION_REQUIRED_UPDATE_OWNER_REMINDER, + }) + @interface UserActionRequirement {} + + /** + * Checks if the permissions still need to be confirmed. + * + *

This is dependant on the identity of the installer, hence this cannot be cached if the + * installer might still {@link #transfer(String) change}. + * + * @return {@code true} iff we need to ask to confirm the permissions? + */ + @UserActionRequirement + private int computeUserActionRequirement() { + final String packageName; + final boolean hasDeviceAdminReceiver; + synchronized (mLock) { + if (mPermissionsManuallyAccepted) { + return USER_ACTION_NOT_NEEDED; + } + // For pre-pappvoal case, the mPackageName would be null. + if (mPackageName != null) { + packageName = mPackageName; + } else if (mPreapprovalRequested.get() && mPreapprovalDetails != null) { + packageName = mPreapprovalDetails.getPackageName(); + } else { + packageName = null; + } + hasDeviceAdminReceiver = mHasDeviceAdminReceiver; + } + + // For the below cases, force user action prompt + // 1. installFlags includes INSTALL_FORCE_PERMISSION_PROMPT + // 2. params.requireUserAction is USER_ACTION_REQUIRED + final boolean forceUserActionPrompt = + (params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0 + || params.requireUserAction == SessionParams.USER_ACTION_REQUIRED; + final int userActionNotTypicallyNeededResponse = forceUserActionPrompt + ? USER_ACTION_REQUIRED + : USER_ACTION_NOT_NEEDED; + + // It is safe to access mInstallerUid and mInstallSource without lock + // because they are immutable after sealing. + final Computer snapshot = mPm.snapshotComputer(); + final boolean isInstallPermissionGranted = + (snapshot.checkUidPermission(android.Manifest.permission.INSTALL_PACKAGES, + mInstallerUid) == PackageManager.PERMISSION_GRANTED); + final boolean isSelfUpdatePermissionGranted = + (snapshot.checkUidPermission(android.Manifest.permission.INSTALL_SELF_UPDATES, + mInstallerUid) == PackageManager.PERMISSION_GRANTED); + final boolean isUpdatePermissionGranted = + (snapshot.checkUidPermission(android.Manifest.permission.INSTALL_PACKAGE_UPDATES, + mInstallerUid) == PackageManager.PERMISSION_GRANTED); + final boolean isUpdateWithoutUserActionPermissionGranted = (snapshot.checkUidPermission( + android.Manifest.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION, mInstallerUid) + == PackageManager.PERMISSION_GRANTED); + final boolean isInstallDpcPackagesPermissionGranted = (snapshot.checkUidPermission( + android.Manifest.permission.INSTALL_DPC_PACKAGES, mInstallerUid) + == PackageManager.PERMISSION_GRANTED); + final int targetPackageUid = snapshot.getPackageUid(packageName, 0, userId); + final boolean isUpdate = targetPackageUid != -1 || isApexSession(); + final InstallSourceInfo existingInstallSourceInfo = isUpdate + ? snapshot.getInstallSourceInfo(packageName, userId) + : null; + final String existingInstallerPackageName = existingInstallSourceInfo != null + ? existingInstallSourceInfo.getInstallingPackageName() + : null; + final String existingUpdateOwnerPackageName = existingInstallSourceInfo != null + ? existingInstallSourceInfo.getUpdateOwnerPackageName() + : null; + final boolean isInstallerOfRecord = isUpdate + && Objects.equals(existingInstallerPackageName, getInstallerPackageName()); + final boolean isUpdateOwner = TextUtils.equals(existingUpdateOwnerPackageName, + getInstallerPackageName()); + final boolean isSelfUpdate = targetPackageUid == mInstallerUid; + final boolean isEmergencyInstall = + isEmergencyInstallerEnabled(packageName, snapshot); + final boolean isPermissionGranted = isInstallPermissionGranted + || (isUpdatePermissionGranted && isUpdate) + || (isSelfUpdatePermissionGranted && isSelfUpdate) + || (isInstallDpcPackagesPermissionGranted && hasDeviceAdminReceiver); + final boolean isInstallerRoot = (mInstallerUid == Process.ROOT_UID); + final boolean isInstallerSystem = (mInstallerUid == Process.SYSTEM_UID); + final boolean isInstallerShell = (mInstallerUid == Process.SHELL_UID); + final boolean isFromManagedUserOrProfile = + (params.installFlags & PackageManager.INSTALL_FROM_MANAGED_USER_OR_PROFILE) != 0; + final boolean isUpdateOwnershipEnforcementEnabled = + mPm.isUpdateOwnershipEnforcementAvailable() + && existingUpdateOwnerPackageName != null; + + // Device owners and affiliated profile owners are allowed to silently install packages, so + // the permission check is waived if the installer is the device owner. + final boolean noUserActionNecessary = isInstallerRoot || isInstallerSystem + || isInstallerDeviceOwnerOrAffiliatedProfileOwner() || isEmergencyInstall; + + if (noUserActionNecessary) { + return userActionNotTypicallyNeededResponse; + } + + if (isUpdateOwnershipEnforcementEnabled + && !isApexSession() + && !isUpdateOwner + && !isInstallerShell + // We don't enforce the update ownership for the managed user and profile. + && !isFromManagedUserOrProfile) { + return USER_ACTION_REQUIRED_UPDATE_OWNER_REMINDER; + } + + if (isPermissionGranted) { + return userActionNotTypicallyNeededResponse; + } + + if (snapshot.isInstallDisabledForPackage(getInstallerPackageName(), mInstallerUid, + userId)) { + // show the installer to account for device policy or unknown sources use cases + return USER_ACTION_REQUIRED; + } + + if (params.requireUserAction == SessionParams.USER_ACTION_NOT_REQUIRED + && isUpdateWithoutUserActionPermissionGranted + && ((isUpdateOwnershipEnforcementEnabled ? isUpdateOwner + : isInstallerOfRecord) || isSelfUpdate)) { + return USER_ACTION_PENDING_APK_PARSING; + } + + return USER_ACTION_REQUIRED; + } + + private void updateUserActionRequirement(int requirement) { + synchronized (mLock) { + mUserActionRequirement = requirement; + } + } + + @SuppressWarnings("GuardedBy" /*mPm.mInstaller is {@code final} field*/) + public PackageInstallerSession(PackageInstallerService.InternalCallback callback, + Context context, PackageManagerService pm, + PackageSessionProvider sessionProvider, + SilentUpdatePolicy silentUpdatePolicy, Looper looper, StagingManager stagingManager, + int sessionId, int userId, int installerUid, @NonNull InstallSource installSource, + SessionParams params, long createdMillis, long committedMillis, + File stageDir, String stageCid, InstallationFile[] files, + ArrayMap checksums, + boolean prepared, boolean committed, boolean destroyed, boolean sealed, + @Nullable int[] childSessionIds, int parentSessionId, boolean isReady, + boolean isFailed, boolean isApplied, int sessionErrorCode, + String sessionErrorMessage, DomainSet preVerifiedDomains) { + mCallback = callback; + mContext = context; + mPm = pm; + mInstaller = (mPm != null) ? mPm.mInstaller : null; + mSessionProvider = sessionProvider; + mSilentUpdatePolicy = silentUpdatePolicy; + mHandler = new Handler(looper, mHandlerCallback); + mStagingManager = stagingManager; + + this.sessionId = sessionId; + this.userId = userId; + mOriginalInstallerUid = installerUid; + mInstallerUid = installerUid; + mInstallSource = Objects.requireNonNull(installSource); + mOriginalInstallerPackageName = mInstallSource.mInstallerPackageName; + this.params = params; + this.createdMillis = createdMillis; + this.updatedMillis = createdMillis; + this.committedMillis = committedMillis; + this.stageDir = stageDir; + this.stageCid = stageCid; + this.mShouldBeSealed = sealed; + if (childSessionIds != null) { + for (int childSessionId : childSessionIds) { + // Null values will be resolved to actual object references in + // #onAfterSessionRead later. + mChildSessions.put(childSessionId, null); + } + } + this.mParentSessionId = parentSessionId; + + if (files != null) { + mFiles.ensureCapacity(files.length); + for (int i = 0, size = files.length; i < size; ++i) { + InstallationFile file = files[i]; + if (!mFiles.add(new FileEntry(i, file))) { + throw new IllegalArgumentException( + "Trying to add a duplicate installation file"); + } + } + } + + if (checksums != null) { + mChecksums.putAll(checksums); + } + + if (!params.isMultiPackage && (stageDir == null) == (stageCid == null)) { + throw new IllegalArgumentException( + "Exactly one of stageDir or stageCid stage must be set"); + } + + mPrepared = prepared; + mCommitted.set(committed); + mDestroyed = destroyed; + mSessionReady = isReady; + mSessionApplied = isApplied; + mSessionFailed = isFailed; + mSessionErrorCode = sessionErrorCode; + mSessionErrorMessage = + sessionErrorMessage != null ? sessionErrorMessage : ""; + mStagedSession = params.isStaged ? new StagedSession() : null; + mPreVerifiedDomains = preVerifiedDomains; + + if (isDataLoaderInstallation()) { + if (isApexSession()) { + throw new IllegalArgumentException( + "DataLoader installation of APEX modules is not allowed."); + } + + if (isSystemDataLoaderInstallation() && mContext.checkCallingOrSelfPermission( + Manifest.permission.USE_SYSTEM_DATA_LOADERS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("You need the " + + "com.android.permission.USE_SYSTEM_DATA_LOADERS permission " + + "to use system data loaders"); + } + } + + if (isIncrementalInstallation() && !IncrementalManager.isAllowed()) { + throw new IllegalArgumentException("Incremental installation not allowed."); + } + + if (isArchivedInstallation()) { + if (params.mode != SessionParams.MODE_FULL_INSTALL) { + throw new IllegalArgumentException( + "Archived installation can only be full install."); + } + if (!isStreamingInstallation() || !isSystemDataLoaderInstallation()) { + throw new IllegalArgumentException( + "Archived installation can only use Streaming System DataLoader."); + } + } + } + + PackageInstallerHistoricalSession createHistoricalSession() { + final float progress; + final float clientProgress; + synchronized (mProgressLock) { + progress = mProgress; + clientProgress = mClientProgress; + } + synchronized (mLock) { + return new PackageInstallerHistoricalSession(sessionId, userId, mOriginalInstallerUid, + mOriginalInstallerPackageName, mInstallSource, mInstallerUid, createdMillis, + updatedMillis, committedMillis, stageDir, stageCid, clientProgress, progress, + isCommitted(), isPreapprovalRequested(), mSealed, mPermissionsManuallyAccepted, + mStageDirInUse, mDestroyed, mFds.size(), mBridges.size(), mFinalStatus, + mFinalMessage, params, mParentSessionId, getChildSessionIdsLocked(), + mSessionApplied, mSessionFailed, mSessionReady, mSessionErrorCode, + mSessionErrorMessage, mPreapprovalDetails, mPreVerifiedDomains, mPackageName); + } + } + + /** + * Returns {@code true} if the {@link SessionInfo} object should be produced with potentially + * sensitive data scrubbed from its fields. + * + * @param callingUid the uid of the caller; the recipient of the {@link SessionInfo} that may + * need to be scrubbed + */ + private boolean shouldScrubData(int callingUid) { + return !(callingUid < Process.FIRST_APPLICATION_UID || getInstallerUid() == callingUid); + } + + /** + * Generates a {@link SessionInfo} object for the provided uid. This may result in some fields + * that may contain sensitive info being filtered. + * + * @param includeIcon true if the icon should be included in the object + * @param callingUid the uid of the caller; the recipient of the {@link SessionInfo} that may + * need to be scrubbed + * @see #shouldScrubData(int) + */ + public SessionInfo generateInfoForCaller(boolean includeIcon, int callingUid) { + return generateInfoInternal(includeIcon, shouldScrubData(callingUid)); + } + + /** + * Generates a {@link SessionInfo} object to ensure proper hiding of sensitive fields. + * + * @param includeIcon true if the icon should be included in the object + * @see #generateInfoForCaller(boolean, int) + */ + public SessionInfo generateInfoScrubbed(boolean includeIcon) { + return generateInfoInternal(includeIcon, true /*scrubData*/); + } + + private SessionInfo generateInfoInternal(boolean includeIcon, boolean scrubData) { + final SessionInfo info = new SessionInfo(); + final float progress; + synchronized (mProgressLock) { + progress = mProgress; + } + synchronized (mLock) { + info.sessionId = sessionId; + info.userId = userId; + info.installerPackageName = mInstallSource.mInstallerPackageName; + info.installerAttributionTag = mInstallSource.mInstallerAttributionTag; + info.resolvedBaseCodePath = null; + if (mContext.checkCallingOrSelfPermission( + Manifest.permission.READ_INSTALLED_SESSION_PATHS) + == PackageManager.PERMISSION_GRANTED) { + File file = mResolvedBaseFile; + if (file == null) { + // Try to guess mResolvedBaseFile file. + final List addedFiles = getAddedApksLocked(); + if (addedFiles.size() > 0) { + file = addedFiles.get(0); + } + } + if (file != null) { + info.resolvedBaseCodePath = file.getAbsolutePath(); + } + } + info.progress = progress; + info.sealed = mSealed; + info.isCommitted = isCommitted(); + info.isPreapprovalRequested = isPreapprovalRequested(); + info.active = mActiveCount.get() > 0; + + info.mode = params.mode; + info.installReason = params.installReason; + info.installScenario = params.installScenario; + info.sizeBytes = params.sizeBytes; + info.appPackageName = mPreapprovalDetails != null ? mPreapprovalDetails.getPackageName() + : mPackageName != null ? mPackageName : params.appPackageName; + if (includeIcon) { + info.appIcon = mPreapprovalDetails != null && mPreapprovalDetails.getIcon() != null + ? mPreapprovalDetails.getIcon() : params.appIcon; + } + info.appLabel = + mPreapprovalDetails != null ? mPreapprovalDetails.getLabel() : params.appLabel; + + info.installLocation = params.installLocation; + if (!scrubData) { + info.originatingUri = params.originatingUri; + } + info.originatingUid = params.originatingUid; + if (!scrubData) { + info.referrerUri = params.referrerUri; + } + info.grantedRuntimePermissions = params.getLegacyGrantedRuntimePermissions(); + info.whitelistedRestrictedPermissions = params.whitelistedRestrictedPermissions; + info.autoRevokePermissionsMode = params.autoRevokePermissionsMode; + info.installFlags = params.installFlags; + info.rollbackLifetimeMillis = params.rollbackLifetimeMillis; + info.rollbackImpactLevel = params.rollbackImpactLevel; + info.isMultiPackage = params.isMultiPackage; + info.isStaged = params.isStaged; + info.rollbackDataPolicy = params.rollbackDataPolicy; + info.parentSessionId = mParentSessionId; + info.childSessionIds = getChildSessionIdsLocked(); + info.isSessionApplied = mSessionApplied; + info.isSessionReady = mSessionReady; + info.isSessionFailed = mSessionFailed; + info.setSessionErrorCode(mSessionErrorCode, mSessionErrorMessage); + info.createdMillis = createdMillis; + info.updatedMillis = updatedMillis; + info.requireUserAction = params.requireUserAction; + info.installerUid = mInstallerUid; + info.packageSource = params.packageSource; + info.applicationEnabledSettingPersistent = params.applicationEnabledSettingPersistent; + info.pendingUserActionReason = userActionRequirementToReason(mUserActionRequirement); + } + return info; + } + + public boolean isPrepared() { + synchronized (mLock) { + return mPrepared; + } + } + + public boolean isSealed() { + synchronized (mLock) { + return mSealed; + } + } + + /** @hide */ + boolean isPreapprovalRequested() { + return mPreapprovalRequested.get(); + } + + /** {@hide} */ + boolean isCommitted() { + return mCommitted.get(); + } + + /** {@hide} */ + boolean isDestroyed() { + synchronized (mLock) { + return mDestroyed; + } + } + + private boolean isInTerminalState() { + synchronized (mLock) { + return mSessionApplied || mSessionFailed; + } + } + + /** Returns true if a staged session has reached a final state and can be forgotten about */ + public boolean isStagedAndInTerminalState() { + return params.isStaged && isInTerminalState(); + } + + private void assertNotLocked(String cookie) { + if (Thread.holdsLock(mLock)) { + throw new IllegalStateException(cookie + " is holding mLock"); + } + } + + private void assertSealed(String cookie) { + if (!isSealed()) { + throw new IllegalStateException(cookie + " before sealing"); + } + } + + @GuardedBy("mLock") + private void assertPreparedAndNotPreapprovalRequestedLocked(String cookie) { + assertPreparedAndNotSealedLocked(cookie); + if (isPreapprovalRequested()) { + throw new IllegalStateException(cookie + " not allowed after requesting"); + } + } + + @GuardedBy("mLock") + private void assertPreparedAndNotSealedLocked(String cookie) { + assertPreparedAndNotCommittedOrDestroyedLocked(cookie); + if (mSealed) { + throw new SecurityException(cookie + " not allowed after sealing"); + } + } + + @GuardedBy("mLock") + private void assertPreparedAndNotCommittedOrDestroyedLocked(String cookie) { + assertPreparedAndNotDestroyedLocked(cookie); + if (isCommitted()) { + throw new SecurityException(cookie + " not allowed after commit"); + } + } + + @GuardedBy("mLock") + private void assertPreparedAndNotDestroyedLocked(String cookie) { + if (!mPrepared) { + throw new IllegalStateException(cookie + " before prepared"); + } + if (mDestroyed) { + throw new SecurityException(cookie + " not allowed after destruction"); + } + } + + @GuardedBy("mProgressLock") + private void setClientProgressLocked(float progress) { + // Always publish first staging movement + final boolean forcePublish = (mClientProgress == 0); + mClientProgress = progress; + computeProgressLocked(forcePublish); + } + + @Override + public void setClientProgress(float progress) { + assertCallerIsOwnerOrRoot(); + synchronized (mProgressLock) { + setClientProgressLocked(progress); + } + } + + @Override + public void addClientProgress(float progress) { + assertCallerIsOwnerOrRoot(); + synchronized (mProgressLock) { + setClientProgressLocked(mClientProgress + progress); + } + } + + @GuardedBy("mProgressLock") + private void computeProgressLocked(boolean forcePublish) { + if (!isIncrementalInstallation() || !isCommitted()) { + mProgress = MathUtils.constrain(mClientProgress * 0.8f, 0f, 0.8f) + + MathUtils.constrain(mInternalProgress * 0.2f, 0f, 0.2f); + } else { + // For incremental, publish regular install progress before the session is committed, + // but publish incremental progress afterwards. + if (mIncrementalProgress - mProgress >= 0.01) { + // It takes some time for data loader to write to incremental file system, so at the + // beginning of the commit, the incremental progress might be very small. + // Wait till the incremental progress is larger than what's already displayed. + // This way we don't see the progress ring going backwards. + mProgress = mIncrementalProgress; + } + } + + // Only publish meaningful progress changes. + if (forcePublish || (mProgress - mReportedProgress) >= 0.01) { + mReportedProgress = mProgress; + mCallback.onSessionProgressChanged(this, mProgress); + } + } + + @Override + public String[] getNames() { + assertCallerIsOwnerRootOrVerifier(); + synchronized (mLock) { + assertPreparedAndNotDestroyedLocked("getNames"); + String[] names; + if (!isCommitted()) { + names = getNamesLocked(); + } else { + names = getStageDirContentsLocked(); + } + return ArrayUtils.removeString(names, APP_METADATA_FILE_NAME); + } + } + + @GuardedBy("mLock") + private String[] getStageDirContentsLocked() { + if (stageDir == null) { + return EmptyArray.STRING; + } + String[] result = stageDir.list(); + if (result == null) { + return EmptyArray.STRING; + } + return result; + } + + @GuardedBy("mLock") + private String[] getNamesLocked() { + if (!isDataLoaderInstallation()) { + return getStageDirContentsLocked(); + } + + InstallationFile[] files = getInstallationFilesLocked(); + String[] result = new String[files.length]; + for (int i = 0, size = files.length; i < size; ++i) { + result[i] = files[i].getName(); + } + return result; + } + + @GuardedBy("mLock") + private InstallationFile[] getInstallationFilesLocked() { + final InstallationFile[] result = new InstallationFile[mFiles.size()]; + for (FileEntry fileEntry : mFiles) { + result[fileEntry.getIndex()] = fileEntry.getFile(); + } + return result; + } + + private static ArrayList filterFiles(File parent, String[] names, FileFilter filter) { + ArrayList result = new ArrayList<>(names.length); + for (String name : names) { + File file = new File(parent, name); + if (filter.accept(file)) { + result.add(file); + } + } + return result; + } + + @GuardedBy("mLock") + private List getAddedApksLocked() { + String[] names = getNamesLocked(); + return filterFiles(stageDir, names, sAddedApkFilter); + } + + @GuardedBy("mLock") + private void enableFsVerityToAddedApksWithIdsig() throws PackageManagerException { + try { + List files = getAddedApksLocked(); + for (var file : files) { + if (new File(file.getPath() + V4Signature.EXT).exists()) { + VerityUtils.setUpFsverity(file.getPath()); + } + } + } catch (IOException e) { + throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE, + "Failed to enable fs-verity to verify with idsig: " + e); + } + } + + @GuardedBy("mLock") + private List getAddedApkLitesLocked() throws PackageManagerException { + if (!isArchivedInstallation()) { + List files = getAddedApksLocked(); + final List result = new ArrayList<>(files.size()); + + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + for (int i = 0, size = files.size(); i < size; ++i) { + final ParseResult parseResult = ApkLiteParseUtils.parseApkLite( + input.reset(), files.get(i), + ParsingPackageUtils.PARSE_COLLECT_CERTIFICATES); + if (parseResult.isError()) { + throw new PackageManagerException(parseResult.getErrorCode(), + parseResult.getErrorMessage(), parseResult.getException()); + } + result.add(parseResult.getResult()); + } + + return result; + } + + InstallationFile[] files = getInstallationFilesLocked(); + final List result = new ArrayList<>(files.length); + + for (int i = 0, size = files.length; i < size; ++i) { + File file = new File(stageDir, files[i].getName()); + if (!sAddedApkFilter.accept(file)) { + continue; + } + + final Metadata metadata; + try { + metadata = Metadata.fromByteArray(files[i].getMetadata()); + } catch (IOException e) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Failed to ", e); + } + if (metadata.getMode() != Metadata.ARCHIVED) { + throw new PackageManagerException(INSTALL_FAILED_VERIFICATION_FAILURE, + "File metadata is not for ARCHIVED package: " + file); + } + + var archPkg = metadata.getArchivedPackage(); + if (archPkg == null) { + throw new PackageManagerException(INSTALL_FAILED_VERIFICATION_FAILURE, + "Metadata does not contain ArchivedPackage: " + file); + } + if (archPkg.packageName == null || archPkg.signingDetails == null) { + throw new PackageManagerException(INSTALL_FAILED_VERIFICATION_FAILURE, + "ArchivedPackage does not contain required info: " + file); + } + result.add(new ApkLite(file.getAbsolutePath(), archPkg)); + } + return result; + } + + @GuardedBy("mLock") + private List getRemovedFilesLocked() { + String[] names = getNamesLocked(); + return filterFiles(stageDir, names, sRemovedFilter); + } + + @Override + public void setChecksums(String name, @NonNull Checksum[] checksums, + @Nullable byte[] signature) { + if (checksums.length == 0) { + return; + } + + final String initiatingPackageName = getInstallSource().mInitiatingPackageName; + final String installerPackageName; + if (!isInstalledByAdb(initiatingPackageName)) { + installerPackageName = initiatingPackageName; + } else { + installerPackageName = getInstallSource().mInstallerPackageName; + } + if (TextUtils.isEmpty(installerPackageName)) { + throw new IllegalStateException("Installer package is empty."); + } + + final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class); + appOps.checkPackage(Binder.getCallingUid(), installerPackageName); + + final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + final AndroidPackage callingInstaller = pmi.getPackage(installerPackageName); + if (callingInstaller == null) { + throw new IllegalStateException("Can't obtain calling installer's package."); + } + + if (signature != null && signature.length != 0) { + try { + Certificate[] ignored = ApkChecksums.verifySignature(checksums, signature); + } catch (IOException | NoSuchAlgorithmException | SignatureException e) { + throw new IllegalArgumentException("Can't verify signature: " + e.getMessage(), e); + } + } + + for (Checksum checksum : checksums) { + if (checksum.getValue() == null + || checksum.getValue().length > Checksum.MAX_CHECKSUM_SIZE_BYTES) { + throw new IllegalArgumentException("Invalid checksum."); + } + } + + assertCallerIsOwnerOrRoot(); + synchronized (mLock) { + assertPreparedAndNotCommittedOrDestroyedLocked("addChecksums"); + + if (mChecksums.containsKey(name)) { + throw new IllegalStateException("Duplicate checksums."); + } + + mChecksums.put(name, new PerFileChecksum(checksums, signature)); + } + } + + @Override + public void requestChecksums(@NonNull String name, @Checksum.TypeMask int optional, + @Checksum.TypeMask int required, @Nullable List trustedInstallers, + @NonNull IOnChecksumsReadyListener onChecksumsReadyListener) { + assertCallerIsOwnerRootOrVerifier(); + final File file = new File(stageDir, name); + final String installerPackageName = PackageManagerServiceUtils.isInstalledByAdb( + getInstallSource().mInitiatingPackageName) + ? getInstallSource().mInstallerPackageName + : getInstallSource().mInitiatingPackageName; + try { + mPm.requestFileChecksums(file, installerPackageName, optional, required, + trustedInstallers, onChecksumsReadyListener); + } catch (FileNotFoundException e) { + throw new ParcelableException(e); + } + } + + @Override + public void removeSplit(String splitName) { + if (isDataLoaderInstallation()) { + throw new IllegalStateException( + "Cannot remove splits in a data loader installation session."); + } + if (TextUtils.isEmpty(params.appPackageName)) { + throw new IllegalStateException("Must specify package name to remove a split"); + } + + assertCallerIsOwnerOrRoot(); + synchronized (mLock) { + assertPreparedAndNotCommittedOrDestroyedLocked("removeSplit"); + + try { + createRemoveSplitMarkerLocked(splitName); + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + } + + private static String getRemoveMarkerName(String name) { + final String markerName = name + REMOVE_MARKER_EXTENSION; + if (!FileUtils.isValidExtFilename(markerName)) { + throw new IllegalArgumentException("Invalid marker: " + markerName); + } + return markerName; + } + + @GuardedBy("mLock") + private void createRemoveSplitMarkerLocked(String splitName) throws IOException { + try { + final File target = new File(stageDir, getRemoveMarkerName(splitName)); + target.createNewFile(); + Os.chmod(target.getAbsolutePath(), 0 /*mode*/); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + private void assertShellOrSystemCalling(String operation) { + switch (Binder.getCallingUid()) { + case android.os.Process.SHELL_UID: + case android.os.Process.ROOT_UID: + case android.os.Process.SYSTEM_UID: + break; + default: + throw new SecurityException(operation + " only supported from shell or system"); + } + } + + private void assertCanWrite(boolean reverseMode) { + if (isDataLoaderInstallation()) { + throw new IllegalStateException( + "Cannot write regular files in a data loader installation session."); + } + assertCallerIsOwnerOrRoot(); + synchronized (mLock) { + assertPreparedAndNotSealedLocked("assertCanWrite"); + } + if (reverseMode) { + assertShellOrSystemCalling("Reverse mode"); + } + } + + private File getTmpAppMetadataFile() { + return new File(Environment.getDataAppDirectory(params.volumeUuid), + sessionId + "-" + APP_METADATA_FILE_NAME); + } + + private File getStagedAppMetadataFile() { + return new File(stageDir, APP_METADATA_FILE_NAME); + } + + private static boolean isAppMetadata(String name) { + return name.endsWith(APP_METADATA_FILE_NAME); + } + + private static boolean isAppMetadata(File file) { + return isAppMetadata(file.getName()); + } + + @Override + public ParcelFileDescriptor getAppMetadataFd() { + assertCallerIsOwnerOrRoot(); + synchronized (mLock) { + assertPreparedAndNotCommittedOrDestroyedLocked("getAppMetadataFd"); + if (!getStagedAppMetadataFile().exists()) { + return null; + } + try { + return openReadInternalLocked(APP_METADATA_FILE_NAME); + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + } + + @Override + public void removeAppMetadata() { + File file = getStagedAppMetadataFile(); + if (file.exists()) { + file.delete(); + } + } + + static long getAppMetadataSizeLimit() { + final long token = Binder.clearCallingIdentity(); + try { + return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE, + PROPERTY_APP_METADATA_BYTE_SIZE_LIMIT, DEFAULT_APP_METADATA_BYTE_SIZE_LIMIT); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public ParcelFileDescriptor openWriteAppMetadata() { + assertCallerIsOwnerOrRoot(); + synchronized (mLock) { + assertPreparedAndNotSealedLocked("openWriteAppMetadata"); + } + try { + return doWriteInternal(APP_METADATA_FILE_NAME, /* offsetBytes= */ 0, + /* lengthBytes= */ -1, null); + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + + @Override + public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) { + assertCanWrite(false); + try { + return doWriteInternal(name, offsetBytes, lengthBytes, null); + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + + @Override + public void write(String name, long offsetBytes, long lengthBytes, + ParcelFileDescriptor fd) { + assertCanWrite(fd != null); + try { + doWriteInternal(name, offsetBytes, lengthBytes, fd); + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + + @Override + public void stageViaHardLink(String path) { + final int callingUid = Binder.getCallingUid(); + if (callingUid != Process.SYSTEM_UID) { + throw new SecurityException("link() can only be run by the system"); + } + + final File target = new File(path); + final File source = new File(stageDir, target.getName()); + var sourcePath = source.getAbsolutePath(); + try { + try { + Os.link(path, sourcePath); + // Grant READ access for APK to be read successfully + Os.chmod(sourcePath, DEFAULT_FILE_ACCESS_MODE); + } catch (ErrnoException e) { + e.rethrowAsIOException(); + } + if (!SELinux.restorecon(source)) { + throw new IOException("Can't relabel file: " + source); + } + } catch (IOException e) { + try { + Os.unlink(sourcePath); + } catch (Exception ignored) { + Slog.d(TAG, "Failed to unlink session file: " + sourcePath); + } + + throw ExceptionUtils.wrap(e); + } + } + + private ParcelFileDescriptor openTargetInternal(String path, int flags, int mode) + throws IOException, ErrnoException { + // TODO: this should delegate to DCS so the system process avoids + // holding open FDs into containers. + final FileDescriptor fd = Os.open(path, flags, mode); + return new ParcelFileDescriptor(fd); + } + + private ParcelFileDescriptor createRevocableFdInternal(RevocableFileDescriptor fd, + ParcelFileDescriptor pfd) throws IOException { + int releasedFdInt = pfd.detachFd(); + FileDescriptor releasedFd = new FileDescriptor(); + releasedFd.setInt$(releasedFdInt); + fd.init(mContext, releasedFd); + return fd.getRevocableFileDescriptor(); + } + + private ParcelFileDescriptor doWriteInternal(String name, long offsetBytes, long lengthBytes, + ParcelFileDescriptor incomingFd) throws IOException { + // Quick validity check of state, and allocate a pipe for ourselves. We + // then do heavy disk allocation outside the lock, but this open pipe + // will block any attempted install transitions. + final RevocableFileDescriptor fd; + final FileBridge bridge; + synchronized (mLock) { + if (PackageInstaller.ENABLE_REVOCABLE_FD) { + fd = new RevocableFileDescriptor(); + bridge = null; + mFds.add(fd); + } else { + fd = null; + bridge = new FileBridge(); + mBridges.add(bridge); + } + } + + try { + // Use installer provided name for now; we always rename later + if (!FileUtils.isValidExtFilename(name)) { + throw new IllegalArgumentException("Invalid name: " + name); + } + final File target; + final long identity = Binder.clearCallingIdentity(); + try { + target = new File(stageDir, name); + } finally { + Binder.restoreCallingIdentity(identity); + } + + // If file is app metadata then set permission to 0640 to deny user read access since it + // might contain sensitive information. + int mode = name.equals(APP_METADATA_FILE_NAME) + ? APP_METADATA_FILE_ACCESS_MODE : DEFAULT_FILE_ACCESS_MODE; + ParcelFileDescriptor targetPfd = openTargetInternal(target.getAbsolutePath(), + O_CREAT | O_WRONLY, mode); + Os.chmod(target.getAbsolutePath(), mode); + + // If caller specified a total length, allocate it for them. Free up + // cache space to grow, if needed. + if (stageDir != null && lengthBytes > 0) { + mContext.getSystemService(StorageManager.class).allocateBytes( + targetPfd.getFileDescriptor(), lengthBytes, + InstallLocationUtils.translateAllocateFlags(params.installFlags)); + } + + if (offsetBytes > 0) { + Os.lseek(targetPfd.getFileDescriptor(), offsetBytes, OsConstants.SEEK_SET); + } + + if (incomingFd != null) { + // In "reverse" mode, we're streaming data ourselves from the + // incoming FD, which means we never have to hand out our + // sensitive internal FD. We still rely on a "bridge" being + // inserted above to hold the session active. + try { + final Int64Ref last = new Int64Ref(0); + FileUtils.copy(incomingFd.getFileDescriptor(), targetPfd.getFileDescriptor(), + lengthBytes, null, Runnable::run, + (long progress) -> { + if (params.sizeBytes > 0) { + final long delta = progress - last.value; + last.value = progress; + synchronized (mProgressLock) { + setClientProgressLocked(mClientProgress + + (float) delta / (float) params.sizeBytes); + } + } + }); + } finally { + IoUtils.closeQuietly(targetPfd); + IoUtils.closeQuietly(incomingFd); + + // We're done here, so remove the "bridge" that was holding + // the session active. + synchronized (mLock) { + if (PackageInstaller.ENABLE_REVOCABLE_FD) { + mFds.remove(fd); + } else { + bridge.forceClose(); + mBridges.remove(bridge); + } + } + } + return null; + } else if (PackageInstaller.ENABLE_REVOCABLE_FD) { + return createRevocableFdInternal(fd, targetPfd); + } else { + bridge.setTargetFile(targetPfd); + bridge.start(); + return bridge.getClientSocket(); + } + + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + @Override + public ParcelFileDescriptor openRead(String name) { + if (isDataLoaderInstallation()) { + throw new IllegalStateException( + "Cannot read regular files in a data loader installation session."); + } + assertCallerIsOwnerOrRoot(); + synchronized (mLock) { + assertPreparedAndNotCommittedOrDestroyedLocked("openRead"); + try { + return openReadInternalLocked(name); + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + } + + @GuardedBy("mLock") + private ParcelFileDescriptor openReadInternalLocked(String name) throws IOException { + try { + if (!FileUtils.isValidExtFilename(name)) { + throw new IllegalArgumentException("Invalid name: " + name); + } + final File target = new File(stageDir, name); + final FileDescriptor targetFd = Os.open(target.getAbsolutePath(), O_RDONLY, 0); + return new ParcelFileDescriptor(targetFd); + } catch (ErrnoException e) { + throw e.rethrowAsIOException(); + } + } + + /** + * Check if the caller is the owner of this session or a verifier. + * Otherwise throw a {@link SecurityException}. + */ + private void assertCallerIsOwnerRootOrVerifier() { + final int callingUid = Binder.getCallingUid(); + if (callingUid == Process.ROOT_UID || callingUid == mInstallerUid) { + return; + } + if (isSealed() && mContext.checkCallingOrSelfPermission( + android.Manifest.permission.PACKAGE_VERIFICATION_AGENT) + == PackageManager.PERMISSION_GRANTED) { + return; + } + throw new SecurityException("Session does not belong to uid " + callingUid); + } + + /** + * Check if the caller is the owner of this session. Otherwise throw a + * {@link SecurityException}. + */ + private void assertCallerIsOwnerOrRoot() { + final int callingUid = Binder.getCallingUid(); + if (callingUid != Process.ROOT_UID && callingUid != mInstallerUid) { + throw new SecurityException("Session does not belong to uid " + callingUid); + } + } + + /** + * Check if the caller is the owner of this session. Otherwise throw a + * {@link SecurityException}. + */ + private void assertCallerIsOwnerOrRootOrSystem() { + final int callingUid = Binder.getCallingUid(); + if (callingUid != Process.ROOT_UID && callingUid != mInstallerUid + && callingUid != Process.SYSTEM_UID) { + throw new SecurityException("Session does not belong to uid " + callingUid); + } + } + + /** + * If anybody is reading or writing data of the session, throw an {@link SecurityException}. + */ + @GuardedBy("mLock") + private void assertNoWriteFileTransfersOpenLocked() { + // Verify that all writers are hands-off + for (RevocableFileDescriptor fd : mFds) { + if (!fd.isRevoked()) { + throw new SecurityException("Files still open"); + } + } + for (FileBridge bridge : mBridges) { + if (!bridge.isClosed()) { + throw new SecurityException("Files still open"); + } + } + } + + @Override + public void commit(@NonNull IntentSender statusReceiver, boolean forTransfer) { + assertNotChild("commit"); + boolean throwsExceptionCommitImmutableCheck = CompatChanges.isChangeEnabled( + THROW_EXCEPTION_COMMIT_WITH_IMMUTABLE_PENDING_INTENT, Binder.getCallingUid()); + if (throwsExceptionCommitImmutableCheck && statusReceiver.isImmutable()) { + throw new IllegalArgumentException( + "The commit() status receiver should come from a mutable PendingIntent"); + } + + if (!markAsSealed(statusReceiver, forTransfer)) { + return; + } + if (isMultiPackage()) { + synchronized (mLock) { + boolean sealFailed = false; + for (int i = mChildSessions.size() - 1; i >= 0; --i) { + // seal all children, regardless if any of them fail; we'll throw/return + // as appropriate once all children have been processed + if (!mChildSessions.valueAt(i).markAsSealed(null, forTransfer)) { + sealFailed = true; + } + } + if (sealFailed) { + return; + } + } + } + + File appMetadataFile = getStagedAppMetadataFile(); + if (appMetadataFile.exists()) { + long sizeLimit = getAppMetadataSizeLimit(); + if (appMetadataFile.length() > sizeLimit) { + appMetadataFile.delete(); + throw new IllegalArgumentException( + "App metadata size exceeds the maximum allowed limit of " + sizeLimit); + } + if (isIncrementalInstallation()) { + // Incremental requires stageDir to be empty so move the app metadata file to a + // temporary location and move back after commit. + appMetadataFile.renameTo(getTmpAppMetadataFile()); + } + } + + dispatchSessionSealed(); + } + + @Override + public void seal() { + assertNotChild("seal"); + assertCallerIsOwnerOrRoot(); + try { + sealInternal(); + for (var child : getChildSessions()) { + child.sealInternal(); + } + } catch (PackageManagerException e) { + throw new IllegalStateException("Package is not valid", e); + } + } + + private void sealInternal() throws PackageManagerException { + synchronized (mLock) { + sealLocked(); + } + } + + @Override + public List fetchPackageNames() { + assertNotChild("fetchPackageNames"); + assertCallerIsOwnerOrRoot(); + var sessions = getSelfOrChildSessions(); + var result = new ArrayList(sessions.size()); + for (var s : sessions) { + result.add(s.fetchPackageName()); + } + return result; + } + + private String fetchPackageName() { + assertSealed("fetchPackageName"); + synchronized (mLock) { + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final List addedFiles = getAddedApksLocked(); + for (File addedFile : addedFiles) { + final ParseResult result = + ApkLiteParseUtils.parseApkLite(input.reset(), addedFile, 0); + if (result.isError()) { + throw new IllegalStateException( + "Can't parse package for session=" + sessionId, result.getException()); + } + final ApkLite apk = result.getResult(); + var packageName = apk.getPackageName(); + if (packageName != null) { + return packageName; + } + } + throw new IllegalStateException("Can't fetch package name for session=" + sessionId); + } + } + + /** + * Kicks off the install flow. The first step is to persist 'sealed' flags + * to prevent mutations of hard links created later. + */ + private void dispatchSessionSealed() { + mHandler.obtainMessage(MSG_ON_SESSION_SEALED).sendToTarget(); + } + + private void handleSessionSealed() { + assertSealed("dispatchSessionSealed"); + // Persist the fact that we've sealed ourselves to prevent + // mutations of any hard links we create. + mCallback.onSessionSealedBlocking(this); + dispatchStreamValidateAndCommit(); + } + + private void dispatchStreamValidateAndCommit() { + mHandler.obtainMessage(MSG_STREAM_VALIDATE_AND_COMMIT).sendToTarget(); + } + + @WorkerThread + private void handleStreamValidateAndCommit() { + try { + // This will track whether the session and any children were validated and are ready to + // progress to the next phase of install + boolean allSessionsReady = true; + for (PackageInstallerSession child : getChildSessions()) { + allSessionsReady &= child.streamValidateAndCommit(); + } + if (allSessionsReady && streamValidateAndCommit()) { + mHandler.obtainMessage(MSG_INSTALL).sendToTarget(); + } + } catch (PackageManagerException e) { + String msg = ExceptionUtils.getCompleteMessage(e); + destroy(msg); + dispatchSessionFinished(e.error, msg, null); + maybeFinishChildSessions(e.error, msg); + } + } + + @WorkerThread + private void handlePreapprovalRequest() { + /** + * Stops the process if the session needs user action. When the user answers the yes, + * {@link #setPermissionsResult(boolean)} is called and then + * {@link #MSG_PRE_APPROVAL_REQUEST} is handled to come back here to check again. + */ + if (sendPendingUserActionIntentIfNeeded(/* forPreapproval= */true)) { + return; + } + + dispatchSessionPreapproved(); + } + + private final class FileSystemConnector extends + IPackageInstallerSessionFileSystemConnector.Stub { + final Set mAddedFiles = new ArraySet<>(); + + FileSystemConnector(List addedFiles) { + for (InstallationFileParcel file : addedFiles) { + mAddedFiles.add(file.name); + } + } + + @Override + public void writeData(String name, long offsetBytes, long lengthBytes, + ParcelFileDescriptor incomingFd) { + if (incomingFd == null) { + throw new IllegalArgumentException("incomingFd can't be null"); + } + if (!mAddedFiles.contains(name)) { + throw new SecurityException("File name is not in the list of added files."); + } + try { + doWriteInternal(name, offsetBytes, lengthBytes, incomingFd); + } catch (IOException e) { + throw ExceptionUtils.wrap(e); + } + } + } + + /** + * Returns whether or not a package can be installed while Secure FRP is enabled. + *

+ * Only callers with the INSTALL_PACKAGES permission are allowed to install. However, + * prevent the package installer from installing anything because, while it has the + * permission, it will allows packages to be installed from anywhere. + */ + private static boolean isSecureFrpInstallAllowed(Context context, int callingUid) { + final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + final String[] systemInstaller = pmi.getKnownPackageNames( + KnownPackages.PACKAGE_INSTALLER, UserHandle.USER_SYSTEM); + final AndroidPackage callingInstaller = pmi.getPackage(callingUid); + if (callingInstaller != null + && ArrayUtils.contains(systemInstaller, callingInstaller.getPackageName())) { + // don't allow the system package installer to install while under secure FRP + return false; + } + + // require caller to hold the INSTALL_PACKAGES permission + return context.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES) + == PackageManager.PERMISSION_GRANTED; + } + + private boolean isInstallationAllowed(PackageStateInternal psi) { + if (psi == null || psi.getPkg() == null) { + return true; + } + if (psi.getPkg().isUpdatableSystem()) { + return true; + } + if (mOriginalInstallerUid == Process.ROOT_UID) { + Slog.w(TAG, "Overriding updatableSystem because the installer is root: " + + psi.getPackageName()); + return true; + } + return false; + } + + /** + * Check if this package can be installed archived. + */ + private static boolean isArchivedInstallationAllowed(PackageStateInternal psi) { + if (psi == null) { + return true; + } + return false; + } + + /** + * Checks if the package can be installed on IncFs. + */ + private static boolean isIncrementalInstallationAllowed(PackageStateInternal psi) { + if (psi == null || psi.getPkg() == null) { + return true; + } + return !psi.isSystem() && !psi.isUpdatedSystemApp(); + } + + /** + * If this was not already called, the session will be sealed. + * + * This method may be called multiple times to update the status receiver validate caller + * permissions. + */ + private boolean markAsSealed(@Nullable IntentSender statusReceiver, boolean forTransfer) { + Preconditions.checkState(statusReceiver != null || hasParentSessionId(), + "statusReceiver can't be null for the root session"); + assertCallerIsOwnerOrRoot(); + + synchronized (mLock) { + assertPreparedAndNotDestroyedLocked("commit of session " + sessionId); + assertNoWriteFileTransfersOpenLocked(); + + final boolean isSecureFrpEnabled = + Global.getInt(mContext.getContentResolver(), Global.SECURE_FRP_MODE, 0) == 1; + + if (isSecureFrpEnabled + && !isSecureFrpInstallAllowed(mContext, Binder.getCallingUid())) { + throw new SecurityException("Can't install packages while in secure FRP"); + } + + if (forTransfer) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES, null); + if (mInstallerUid == mOriginalInstallerUid) { + throw new IllegalArgumentException("Session has not been transferred"); + } + } else { + if (mInstallerUid != mOriginalInstallerUid) { + throw new IllegalArgumentException("Session has been transferred"); + } + } + + setRemoteStatusReceiver(statusReceiver); + + // After updating the observer, we can skip re-sealing. + if (mSealed) { + return true; + } + + try { + sealLocked(); + } catch (PackageManagerException e) { + return false; + } + } + + return true; + } + + /** + * Returns true if the session is successfully validated and committed. Returns false if the + * dataloader could not be prepared. This can be called multiple times so long as no + * exception is thrown. + * @throws PackageManagerException on an unrecoverable error. + */ + @WorkerThread + private boolean streamValidateAndCommit() throws PackageManagerException { + // TODO(patb): since the work done here for a parent session in a multi-package install is + // mostly superficial, consider splitting this method for the parent and + // single / child sessions. + try { + synchronized (mLock) { + if (isCommitted()) { + return true; + } + // Read transfers from the original owner stay open, but as the session's data + // cannot be modified anymore, there is no leak of information. For staged sessions, + // further validation is performed by the staging manager. + if (!params.isMultiPackage) { + if (!prepareDataLoaderLocked()) { + return false; + } + + if (isApexSession()) { + validateApexInstallLocked(); + } else { + validateApkInstallLocked(); + } + } + if (mDestroyed) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session destroyed"); + } + if (!isIncrementalInstallation()) { + synchronized (mProgressLock) { + // For non-incremental installs, client staging is fully done at this point + mClientProgress = 1f; + computeProgressLocked(true); + } + } + + // This ongoing commit should keep session active, even though client + // will probably close their end. + mActiveCount.incrementAndGet(); + + if (!mCommitted.compareAndSet(false /*expect*/, true /*update*/)) { + throw new PackageManagerException( + INSTALL_FAILED_INTERNAL_ERROR, + TextUtils.formatSimple( + "The mCommitted of session %d should be false originally", + sessionId)); + } + committedMillis = System.currentTimeMillis(); + } + return true; + } catch (PackageManagerException e) { + throw e; + } catch (Throwable e) { + // Convert all exceptions into package manager exceptions as only those are handled + // in the code above. + throw new PackageManagerException(e); + } + } + + @GuardedBy("mLock") + private @NonNull List getChildSessionsLocked() { + List childSessions = Collections.EMPTY_LIST; + if (isMultiPackage()) { + int size = mChildSessions.size(); + childSessions = new ArrayList<>(size); + for (int i = 0; i < size; ++i) { + childSessions.add(mChildSessions.valueAt(i)); + } + } + return childSessions; + } + + @NonNull List getChildSessions() { + synchronized (mLock) { + return getChildSessionsLocked(); + } + } + + @NonNull + private List getSelfOrChildSessions() { + return isMultiPackage() ? getChildSessions() : Collections.singletonList(this); + } + + /** + * Seal the session to prevent further modification. + * + *

The session will be sealed after calling this method even if it failed. + * + * @throws PackageManagerException if the session was sealed but something went wrong. If the + * session was sealed this is the only possible exception. + */ + @GuardedBy("mLock") + private void sealLocked() + throws PackageManagerException { + try { + assertNoWriteFileTransfersOpenLocked(); + assertPreparedAndNotDestroyedLocked("sealing of session " + sessionId); + mSealed = true; + } catch (Throwable e) { + // Convert all exceptions into package manager exceptions as only those are handled + // in the code above. + throw onSessionValidationFailure(new PackageManagerException(e)); + } + } + + private PackageManagerException onSessionValidationFailure(PackageManagerException e) { + onSessionValidationFailure(e.error, ExceptionUtils.getCompleteMessage(e)); + return e; + } + + private void onSessionValidationFailure(int error, String detailMessage) { + // Session is sealed but could not be validated, we need to destroy it. + destroyInternal("Failed to validate session, error: " + error + ", " + detailMessage); + // Dispatch message to remove session from PackageInstallerService. + dispatchSessionFinished(error, detailMessage, null); + } + + private void onSessionVerificationFailure(int error, String msg) { + Slog.e(TAG, "Failed to verify session " + sessionId); + // Dispatch message to remove session from PackageInstallerService. + dispatchSessionFinished(error, msg, null); + maybeFinishChildSessions(error, msg); + } + + private void onSystemDataLoaderUnrecoverable() { + final String packageName = getPackageName(); + if (TextUtils.isEmpty(packageName)) { + // The package has not been installed. + return; + } + mHandler.post(() -> { + if (mPm.deletePackageX(packageName, + PackageManager.VERSION_CODE_HIGHEST, UserHandle.USER_SYSTEM, + PackageManager.DELETE_ALL_USERS, true /*removedBySystem*/) + != PackageManager.DELETE_SUCCEEDED) { + Slog.e(TAG, "Failed to uninstall package with failed dataloader: " + packageName); + } + }); + } + + /** + * If session should be sealed, then it's sealed to prevent further modification. + * If the session can't be sealed then it's destroyed. + * + * Additionally for staged APEX/APK sessions read+validate the package and populate req'd + * fields. + * + *

This is meant to be called after all of the sessions are loaded and added to + * PackageInstallerService + * + * @param allSessions All sessions loaded by PackageInstallerService, guaranteed to be + * immutable by the caller during the method call. Used to resolve child + * sessions Ids to actual object reference. + */ + @AnyThread + void onAfterSessionRead(SparseArray allSessions) { + synchronized (mLock) { + // Resolve null values to actual object references + for (int i = mChildSessions.size() - 1; i >= 0; --i) { + int childSessionId = mChildSessions.keyAt(i); + PackageInstallerSession childSession = allSessions.get(childSessionId); + if (childSession != null) { + mChildSessions.setValueAt(i, childSession); + } else { + Slog.e(TAG, "Child session not existed: " + childSessionId); + mChildSessions.removeAt(i); + } + } + + if (!mShouldBeSealed || isStagedAndInTerminalState()) { + return; + } + try { + sealLocked(); + + // Session that are staged, committed and not multi package will be installed or + // restart verification during this boot. As such, we need populate all the fields + // for successful installation. + if (isMultiPackage() || !isStaged() || !isCommitted()) { + return; + } + final PackageInstallerSession root = hasParentSessionId() + ? allSessions.get(getParentSessionId()) + : this; + if (root != null && !root.isStagedAndInTerminalState()) { + if (isApexSession()) { + validateApexInstallLocked(); + } else { + validateApkInstallLocked(); + } + } + } catch (PackageManagerException e) { + Slog.e(TAG, "Package not valid", e); + } + } + } + + /** Update the timestamp of when the staged session last changed state */ + public void markUpdated() { + synchronized (mLock) { + this.updatedMillis = System.currentTimeMillis(); + } + } + + @Override + public void transfer(String packageName) { + Preconditions.checkArgument(!TextUtils.isEmpty(packageName)); + final Computer snapshot = mPm.snapshotComputer(); + ApplicationInfo newOwnerAppInfo = snapshot.getApplicationInfo(packageName, 0, userId); + if (newOwnerAppInfo == null) { + throw new ParcelableException(new PackageManager.NameNotFoundException(packageName)); + } + + if (PackageManager.PERMISSION_GRANTED != snapshot.checkUidPermission( + Manifest.permission.INSTALL_PACKAGES, newOwnerAppInfo.uid)) { + throw new SecurityException("Destination package " + packageName + " does not have " + + "the " + Manifest.permission.INSTALL_PACKAGES + " permission"); + } + + // Only install flags that can be verified by the app the session is transferred to are + // allowed. The parameters can be read via PackageInstaller.SessionInfo. + if (!params.areHiddenOptionsSet()) { + throw new SecurityException("Can only transfer sessions that use public options"); + } + + synchronized (mLock) { + assertCallerIsOwnerOrRoot(); + assertPreparedAndNotSealedLocked("transfer"); + + try { + sealLocked(); + } catch (PackageManagerException e) { + throw new IllegalStateException("Package is not valid", e); + } + + mInstallerUid = newOwnerAppInfo.uid; + mInstallSource = InstallSource.create(packageName, null /* originatingPackageName */, + packageName, mInstallerUid, packageName, null /* installerAttributionTag */, + params.packageSource); + } + } + + @WorkerThread + private static boolean checkUserActionRequirement( + PackageInstallerSession session, IntentSender target) { + if (session.isMultiPackage()) { + return false; + } + + @UserActionRequirement int userActionRequirement = USER_ACTION_NOT_NEEDED; + // TODO(b/159331446): Move this to makeSessionActiveForInstall and update javadoc + userActionRequirement = session.computeUserActionRequirement(); + session.updateUserActionRequirement(userActionRequirement); + if (userActionRequirement == USER_ACTION_REQUIRED + || userActionRequirement == USER_ACTION_REQUIRED_UPDATE_OWNER_REMINDER) { + session.sendPendingUserActionIntent(target); + return true; + } + + if (!session.isApexSession() && userActionRequirement == USER_ACTION_PENDING_APK_PARSING) { + if (!isTargetSdkConditionSatisfied(session)) { + session.sendPendingUserActionIntent(target); + return true; + } + + if (session.params.requireUserAction == SessionParams.USER_ACTION_NOT_REQUIRED) { + if (!session.mSilentUpdatePolicy.isSilentUpdateAllowed( + session.getInstallerPackageName(), session.getPackageName())) { + // Fall back to the non-silent update if a repeated installation is invoked + // within the throttle time. + session.sendPendingUserActionIntent(target); + return true; + } + session.mSilentUpdatePolicy.track(session.getInstallerPackageName(), + session.getPackageName()); + } + } + + return false; + } + + /** + * Checks if the app being installed has a targetSdk more than the minimum required for a + * silent install. See {@link SessionParams#setRequireUserAction(int)} for details about the + * targetSdk requirement. + * @param session Current install session + * @return true if the targetSdk of the app being installed is more than the minimum required, + * resulting in a silent install, false otherwise. + */ + private static boolean isTargetSdkConditionSatisfied(PackageInstallerSession session) { + final int validatedTargetSdk; + final String packageName; + synchronized (session.mLock) { + validatedTargetSdk = session.mValidatedTargetSdk; + packageName = session.mPackageName; + } + + ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.packageName = packageName; + appInfo.targetSdkVersion = validatedTargetSdk; + + IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface( + ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); + try { + // Using manually constructed AppInfo to check if a change is enabled may not work + // in the future. + return validatedTargetSdk != INVALID_TARGET_SDK_VERSION + && platformCompat.isChangeEnabled(SILENT_INSTALL_ALLOWED, appInfo); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get a response from PLATFORM_COMPAT_SERVICE", e); + return false; + } + } + + private static @UserActionReason int userActionRequirementToReason( + @UserActionRequirement int requirement) { + switch (requirement) { + case USER_ACTION_REQUIRED_UPDATE_OWNER_REMINDER: + return PackageInstaller.REASON_REMIND_OWNERSHIP; + default: + return PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE; + } + } + + /** + * Find out any session needs user action. + * + * @return true if the session set requires user action for the installation, otherwise false. + */ + @WorkerThread + private boolean sendPendingUserActionIntentIfNeeded(boolean forPreapproval) { + // To support pre-approval request of atomic install, we allow child session to handle + // the result by itself since it has the status receiver. + if (isCommitted()) { + assertNotChild("PackageInstallerSession#sendPendingUserActionIntentIfNeeded"); + } + // Since there are separate status receivers for session preapproval and commit, + // check whether user action is requested for session preapproval or commit + final IntentSender statusReceiver = forPreapproval ? getPreapprovalRemoteStatusReceiver() + : getRemoteStatusReceiver(); + return sessionContains(s -> checkUserActionRequirement(s, statusReceiver)); + } + + @WorkerThread + private void handleInstall() { + if (isInstallerDeviceOwnerOrAffiliatedProfileOwner()) { + DevicePolicyEventLogger + .createEvent(DevicePolicyEnums.INSTALL_PACKAGE) + .setAdmin(getInstallSource().mInstallerPackageName) + .write(); + } + + /** + * Stops the installation of the whole session set if one session needs user action + * in its belong session set. When the user answers the yes, + * {@link #setPermissionsResult(boolean)} is called and then {@link #MSG_INSTALL} is + * handled to come back here to check again. + * + * {@code mUserActionRequired} is used to track when user action is required for an + * install. Since control may come back here more than 1 time, we must ensure that it's + * value is not overwritten. + */ + boolean wasUserActionIntentSent = + sendPendingUserActionIntentIfNeeded(/* forPreapproval= */false); + if (mUserActionRequired == null) { + mUserActionRequired = wasUserActionIntentSent; + } + if (wasUserActionIntentSent) { + // Commit was keeping session marked as active until now; release + // that extra refcount so session appears idle. + deactivate(); + return; + } else if (mUserActionRequired) { + // If user action is required, control comes back here when the user allows + // the installation. At this point, the session is marked active once again, + // since installation is in progress. + activate(); + } + + if (mVerificationInProgress) { + Slog.w(TAG, "Verification is already in progress for session " + sessionId); + return; + } + mVerificationInProgress = true; + + if (params.isStaged) { + mStagedSession.verifySession(); + } else { + verify(); + } + } + + private void verify() { + try { + List children = getChildSessions(); + if (isMultiPackage()) { + for (PackageInstallerSession child : children) { + child.prepareInheritedFiles(); + child.parseApkAndExtractNativeLibraries(); + } + } else { + prepareInheritedFiles(); + parseApkAndExtractNativeLibraries(); + } + verifyNonStaged(); + } catch (PackageManagerException e) { + final String completeMsg = ExceptionUtils.getCompleteMessage(e); + final String errorMsg = PackageManager.installStatusToString(e.error, completeMsg); + setSessionFailed(e.error, errorMsg); + onSessionVerificationFailure(e.error, errorMsg); + } + } + + private IntentSender getRemoteStatusReceiver() { + synchronized (mLock) { + return mRemoteStatusReceiver; + } + } + + private void setRemoteStatusReceiver(IntentSender remoteStatusReceiver) { + synchronized (mLock) { + mRemoteStatusReceiver = remoteStatusReceiver; + } + } + + private IntentSender getPreapprovalRemoteStatusReceiver() { + synchronized (mLock) { + return mPreapprovalRemoteStatusReceiver; + } + } + + private void setPreapprovalRemoteStatusReceiver(IntentSender remoteStatusReceiver) { + synchronized (mLock) { + mPreapprovalRemoteStatusReceiver = remoteStatusReceiver; + } + } + + /** + * Prepares staged directory with any inherited APKs. + */ + private void prepareInheritedFiles() throws PackageManagerException { + if (isApexSession() || params.mode != SessionParams.MODE_INHERIT_EXISTING) { + return; + } + synchronized (mLock) { + if (mStageDirInUse) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session files in use"); + } + if (mDestroyed) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session destroyed"); + } + if (!mSealed) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session not sealed"); + } + // Inherit any packages and native libraries from existing install that + // haven't been overridden. + try { + final List fromFiles = mResolvedInheritedFiles; + final File toDir = stageDir; + final String tempPackageName = toDir.getName(); + + if (LOGD) Slog.d(TAG, "Inherited files: " + mResolvedInheritedFiles); + if (!mResolvedInheritedFiles.isEmpty() && mInheritedFilesBase == null) { + throw new IllegalStateException("mInheritedFilesBase == null"); + } + + if (isLinkPossible(fromFiles, toDir)) { + if (!mResolvedInstructionSets.isEmpty()) { + final File oatDir = new File(toDir, "oat"); + createOatDirs(tempPackageName, mResolvedInstructionSets, oatDir); + } + // pre-create lib dirs for linking if necessary + if (!mResolvedNativeLibPaths.isEmpty()) { + for (String libPath : mResolvedNativeLibPaths) { + // "/lib/arm64" -> ["lib", "arm64"] + final int splitIndex = libPath.lastIndexOf('/'); + if (splitIndex < 0 || splitIndex >= libPath.length() - 1) { + Slog.e(TAG, + "Skipping native library creation for linking due" + + " to invalid path: " + libPath); + continue; + } + final String libDirPath = libPath.substring(1, splitIndex); + final File libDir = new File(toDir, libDirPath); + if (!libDir.exists()) { + NativeLibraryHelper.createNativeLibrarySubdir(libDir); + } + final String archDirPath = libPath.substring(splitIndex + 1); + NativeLibraryHelper.createNativeLibrarySubdir( + new File(libDir, archDirPath)); + } + } + linkFiles(tempPackageName, fromFiles, toDir, mInheritedFilesBase); + } else { + // TODO: this should delegate to DCS so the system process + // avoids holding open FDs into containers. + copyFiles(fromFiles, toDir); + } + } catch (IOException e) { + throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE, + "Failed to inherit existing install", e); + } + } + } + + @GuardedBy("mLock") + private void markStageDirInUseLocked() throws PackageManagerException { + if (mDestroyed) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session destroyed"); + } + // Set this flag to prevent abandon() from deleting staging files when verification or + // installation is about to start. + mStageDirInUse = true; + } + + private void parseApkAndExtractNativeLibraries() throws PackageManagerException { + synchronized (mLock) { + if (mStageDirInUse) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session files in use"); + } + if (mDestroyed) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session destroyed"); + } + if (!mSealed) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session not sealed"); + } + if (mPackageName == null) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session no package name"); + } + if (mSigningDetails == null) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session no signing data"); + } + if (mResolvedBaseFile == null) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Session no resolved base file"); + } + final PackageLite result; + if (!isApexSession()) { + // For mode inherit existing, it would link/copy existing files to stage dir in + // prepareInheritedFiles(). Therefore, we need to parse the complete package in + // stage dir here. + // Besides, PackageLite may be null for staged sessions that don't complete + // pre-reboot verification. + result = getOrParsePackageLiteLocked(stageDir, /* flags */ 0); + } else { + result = getOrParsePackageLiteLocked(mResolvedBaseFile, /* flags */ 0); + } + if (result != null) { + mPackageLite = result; + if (!isApexSession()) { + synchronized (mProgressLock) { + mInternalProgress = 0.5f; + computeProgressLocked(true); + } + extractNativeLibraries( + mPackageLite, stageDir, params.abiOverride, mayInheritNativeLibs()); + } + } + } + } + + private void verifyNonStaged() + throws PackageManagerException { + synchronized (mLock) { + markStageDirInUseLocked(); + } + mSessionProvider.getSessionVerifier().verify(this, (error, msg) -> { + mHandler.post(() -> { + if (dispatchPendingAbandonCallback()) { + // No need to continue if abandoned + return; + } + if (error == INSTALL_SUCCEEDED) { + onVerificationComplete(); + } else { + onSessionVerificationFailure(error, msg); + } + }); + }); + } + + private static class InstallResult { + public final PackageInstallerSession session; + public final Bundle extras; + InstallResult(PackageInstallerSession session, Bundle extras) { + this.session = session; + this.extras = extras; + } + } + + /** + * Stages installs and do cleanup accordingly depending on whether the installation is + * successful or not. + * + * @return a future that will be completed when the whole process is completed. + */ + private CompletableFuture install() { + // `futures` either contains only one session (`this`) or contains one parent session + // (`this`) and n-1 child sessions. + List> futures = installNonStaged(); + CompletableFuture[] arr = new CompletableFuture[futures.size()]; + return CompletableFuture.allOf(futures.toArray(arr)).whenComplete((r, t) -> { + if (t == null) { + setSessionApplied(); + var multiPackageWarnings = new ArrayList(); + if (isMultiPackage()) { + // This is a parent session. Collect warnings from children. + for (CompletableFuture f : futures) { + InstallResult result = f.join(); + if (result.session != this && result.extras != null) { + ArrayList childWarnings = result.extras.getStringArrayList( + PackageInstaller.EXTRA_WARNINGS); + if (!ArrayUtils.isEmpty(childWarnings)) { + multiPackageWarnings.addAll(childWarnings); + } + } + } + } + for (CompletableFuture f : futures) { + InstallResult result = f.join(); + Bundle extras = result.extras; + if (isMultiPackage() && result.session == this + && !multiPackageWarnings.isEmpty()) { + if (extras == null) { + extras = new Bundle(); + } + extras.putStringArrayList( + PackageInstaller.EXTRA_WARNINGS, multiPackageWarnings); + } + result.session.dispatchSessionFinished( + INSTALL_SUCCEEDED, "Session installed", extras); + } + } else { + PackageManagerException e = (PackageManagerException) t.getCause(); + setSessionFailed(e.error, + PackageManager.installStatusToString(e.error, e.getMessage())); + dispatchSessionFinished(e.error, e.getMessage(), null); + maybeFinishChildSessions(e.error, e.getMessage()); + } + }); + } + + /** + * Stages sessions (including child sessions if any) for install. + * + * @return a list of futures to indicate the install results of each session. + */ + private List> installNonStaged() { + try { + List> futures = new ArrayList<>(); + CompletableFuture future = new CompletableFuture<>(); + futures.add(future); + final InstallingSession installingSession = createInstallingSession(future); + if (isMultiPackage()) { + final List childSessions = getChildSessions(); + List installingChildSessions = + new ArrayList<>(childSessions.size()); + for (int i = 0; i < childSessions.size(); ++i) { + final PackageInstallerSession session = childSessions.get(i); + future = new CompletableFuture<>(); + futures.add(future); + final InstallingSession installingChildSession = + session.createInstallingSession(future); + if (installingChildSession != null) { + installingChildSessions.add(installingChildSession); + } + } + if (!installingChildSessions.isEmpty()) { + Objects.requireNonNull(installingSession).installStage(installingChildSessions); + } + } else if (installingSession != null) { + installingSession.installStage(); + } + + return futures; + } catch (PackageManagerException e) { + List> futures = new ArrayList<>(); + futures.add(CompletableFuture.failedFuture(e)); + return futures; + } + } + + private void sendPendingUserActionIntent(IntentSender target) { + // User needs to confirm installation; + // give installer an intent they can use to involve + // user. + final boolean isPreapproval = isPreapprovalRequested() && !isCommitted(); + final Intent intent = new Intent( + isPreapproval ? PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL + : PackageInstaller.ACTION_CONFIRM_INSTALL); + intent.setPackage(mPm.getPackageInstallerPackageName()); + intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); + sendOnUserActionRequired(mContext, target, sessionId, intent); + } + + @WorkerThread + private void onVerificationComplete() { + if (isStaged()) { + mStagingManager.commitSession(mStagedSession); + sendUpdateToRemoteStatusReceiver(INSTALL_SUCCEEDED, "Session staged", + /* extras= */ null, /* forPreapproval= */ false); + return; + } + install(); + } + + /** + * Stages this session for install and returns a + * {@link InstallingSession} representing this new staged state. + * + * @param future a future that will be completed when this session is completed. + */ + @Nullable + private InstallingSession createInstallingSession(CompletableFuture future) + throws PackageManagerException { + synchronized (mLock) { + if (!mSealed) { + throw new PackageManagerException( + INSTALL_FAILED_INTERNAL_ERROR, "Session not sealed"); + } + markStageDirInUseLocked(); + } + + if (isMultiPackage()) { + // Always treat parent session as success for it has nothing to install + future.complete(new InstallResult(this, null)); + } else if (isApexSession() && params.isStaged) { + // Staged apex sessions have been handled by apexd + future.complete(new InstallResult(this, null)); + return null; + } + + final IPackageInstallObserver2 localObserver = new IPackageInstallObserver2.Stub() { + @Override + public void onUserActionRequired(Intent intent) { + throw new IllegalStateException(); + } + + @Override + public void onPackageInstalled(String basePackageName, int returnCode, String msg, + Bundle extras) { + if (returnCode == INSTALL_SUCCEEDED) { + future.complete(new InstallResult(PackageInstallerSession.this, extras)); + } else { + future.completeExceptionally(new PackageManagerException(returnCode, msg)); + } + } + }; + + final UserHandle user; + if ((params.installFlags & PackageManager.INSTALL_ALL_USERS) != 0) { + user = UserHandle.ALL; + } else { + user = new UserHandle(userId); + } + + if (params.isStaged) { + params.installFlags |= INSTALL_STAGED; + } + + if (!isMultiPackage() && !isApexSession()) { + synchronized (mLock) { + // This shouldn't be null, but have this code path just in case. + if (mPackageLite == null) { + Slog.wtf(TAG, "Session: " + sessionId + ". Don't have a valid PackageLite."); + } + mPackageLite = getOrParsePackageLiteLocked(stageDir, /* flags */ 0); + } + } + + synchronized (mLock) { + return new InstallingSession(sessionId, stageDir, localObserver, params, mInstallSource, + user, mSigningDetails, mInstallerUid, mPackageLite, mPreVerifiedDomains, mPm); + } + } + + @GuardedBy("mLock") + private PackageLite getOrParsePackageLiteLocked(File packageFile, int flags) + throws PackageManagerException { + if (mPackageLite != null) { + return mPackageLite; + } + + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final ParseResult result = + ApkLiteParseUtils.parsePackageLite(input, packageFile, flags); + if (result.isError()) { + throw new PackageManagerException(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, + result.getErrorMessage(), result.getException()); + } + return result.getResult(); + } + + private static void maybeRenameFile(File from, File to) throws PackageManagerException { + if (!from.equals(to)) { + if (!from.renameTo(to)) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Could not rename file " + from + " to " + to); + } + } + } + + private void logDataLoaderInstallationSession(int returnCode) { + // Skip logging the side-loaded app installations, as those are private and aren't reported + // anywhere; app stores already have a record of the installation and that's why reporting + // it here is fine + final String packageName = getPackageName(); + final String packageNameToLog = + (params.installFlags & PackageManager.INSTALL_FROM_ADB) == 0 ? packageName : ""; + final long currentTimestamp = System.currentTimeMillis(); + final int packageUid; + if (returnCode != INSTALL_SUCCEEDED) { + // Package didn't install; no valid uid + packageUid = INVALID_UID; + } else { + packageUid = mPm.snapshotComputer().getPackageUid(packageName, 0, userId); + } + FrameworkStatsLog.write(FrameworkStatsLog.PACKAGE_INSTALLER_V2_REPORTED, + isIncrementalInstallation(), + packageNameToLog, + currentTimestamp - createdMillis, + returnCode, + getApksSize(packageName), + packageUid); + } + + private long getApksSize(String packageName) { + final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + final PackageStateInternal ps = pmi.getPackageStateInternal(packageName); + if (ps == null) { + return 0; + } + final File apkDirOrPath = ps.getPath(); + if (apkDirOrPath == null) { + return 0; + } + if (apkDirOrPath.isFile() && apkDirOrPath.getName().toLowerCase().endsWith(".apk")) { + return apkDirOrPath.length(); + } + if (!apkDirOrPath.isDirectory()) { + return 0; + } + final File[] files = apkDirOrPath.listFiles(); + long apksSize = 0; + for (int i = 0; i < files.length; i++) { + if (files[i].getName().toLowerCase().endsWith(".apk")) { + apksSize += files[i].length(); + } + } + return apksSize; + } + + /** + * Returns true if the session should attempt to inherit any existing native libraries already + * extracted at the current install location. This is necessary to prevent double loading of + * native libraries already loaded by the running app. + */ + private boolean mayInheritNativeLibs() { + return SystemProperties.getBoolean(PROPERTY_NAME_INHERIT_NATIVE, true) && + params.mode == SessionParams.MODE_INHERIT_EXISTING && + (params.installFlags & PackageManager.DONT_KILL_APP) != 0; + } + + /** + * Returns true if the session is installing an APEX package. + */ + boolean isApexSession() { + return (params.installFlags & PackageManager.INSTALL_APEX) != 0; + } + + boolean sessionContains(Predicate filter) { + if (!isMultiPackage()) { + return filter.test(this); + } + final List childSessions; + synchronized (mLock) { + childSessions = getChildSessionsLocked(); + } + for (PackageInstallerSession child: childSessions) { + if (filter.test(child)) { + return true; + } + } + return false; + } + + boolean containsApkSession() { + return sessionContains((s) -> !s.isApexSession()); + } + + /** + * Validate apex install. + *

+ * Sets {@link #mResolvedBaseFile} for RollbackManager to use. Sets {@link #mPackageName} for + * StagingManager to use. + */ + @GuardedBy("mLock") + private void validateApexInstallLocked() + throws PackageManagerException { + final List addedFiles = getAddedApksLocked(); + if (addedFiles.isEmpty()) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + TextUtils.formatSimple("Session: %d. No packages staged in %s", sessionId, + stageDir.getAbsolutePath())); + } + + if (ArrayUtils.size(addedFiles) > 1) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Too many files for apex install"); + } + + File addedFile = addedFiles.get(0); // there is only one file + + // Ensure file name has proper suffix + final String sourceName = addedFile.getName(); + final String targetName = sourceName.endsWith(APEX_FILE_EXTENSION) + ? sourceName + : sourceName + APEX_FILE_EXTENSION; + if (!FileUtils.isValidExtFilename(targetName)) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Invalid filename: " + targetName); + } + + final File targetFile = new File(stageDir, targetName); + resolveAndStageFileLocked(addedFile, targetFile, null); + mResolvedBaseFile = targetFile; + + // Populate package name of the apex session + mPackageName = null; + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final ParseResult ret = ApkLiteParseUtils.parseApkLite(input.reset(), + mResolvedBaseFile, ParsingPackageUtils.PARSE_COLLECT_CERTIFICATES); + if (ret.isError()) { + throw new PackageManagerException(ret.getErrorCode(), ret.getErrorMessage(), + ret.getException()); + } + final ApkLite apk = ret.getResult(); + + if (mPackageName == null) { + mPackageName = apk.getPackageName(); + mVersionCode = apk.getLongVersionCode(); + } + + mSigningDetails = apk.getSigningDetails(); + mHasDeviceAdminReceiver = apk.isHasDeviceAdminReceiver(); + } + + /** + * Validate install by confirming that all application packages are have + * consistent package name, version code, and signing certificates. + *

+ * Clears and populates {@link #mResolvedBaseFile}, + * {@link #mResolvedStagedFiles}, and {@link #mResolvedInheritedFiles}. + *

+ * Renames package files in stage to match split names defined inside. + *

+ * Note that upgrade compatibility is still performed by + * {@link PackageManagerService}. + * @return a {@link PackageLite} representation of the validated APK(s). + */ + @GuardedBy("mLock") + private PackageLite validateApkInstallLocked() throws PackageManagerException { + ApkLite baseApk = null; + final PackageLite packageLite; + mPackageLite = null; + mPackageName = null; + mVersionCode = -1; + mSigningDetails = SigningDetails.UNKNOWN; + + mResolvedBaseFile = null; + mResolvedStagedFiles.clear(); + mResolvedInheritedFiles.clear(); + + final PackageInfo pkgInfo = mPm.snapshotComputer().getPackageInfo( + params.appPackageName, PackageManager.GET_SIGNATURES + | PackageManager.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES /*flags*/, userId); + + // Partial installs must be consistent with existing install + if (params.mode == SessionParams.MODE_INHERIT_EXISTING + && (pkgInfo == null || pkgInfo.applicationInfo == null)) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Missing existing base package"); + } + + // Default to require only if existing base apk has fs-verity signature. + mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled() + && params.mode == SessionParams.MODE_INHERIT_EXISTING + && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath()) + && (new File(VerityUtils.getFsveritySignatureFilePath( + pkgInfo.applicationInfo.getBaseCodePath()))).exists(); + + final List removedFiles = getRemovedFilesLocked(); + final List removeSplitList = new ArrayList<>(); + if (!removedFiles.isEmpty()) { + for (File removedFile : removedFiles) { + final String fileName = removedFile.getName(); + final String splitName = fileName.substring( + 0, fileName.length() - REMOVE_MARKER_EXTENSION.length()); + removeSplitList.add(splitName); + } + } + + // Needs to happen before the first v4 signature verification, which happens in + // getAddedApkLitesLocked. + if (android.security.Flags.extendVbChainToUpdatedApk()) { + if (!isIncrementalInstallation()) { + enableFsVerityToAddedApksWithIdsig(); + } + } + + final List addedFiles = getAddedApkLitesLocked(); + if (addedFiles.isEmpty() + && (removeSplitList.size() == 0 || getStagedAppMetadataFile().exists())) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + TextUtils.formatSimple("Session: %d. No packages staged in %s", sessionId, + stageDir.getAbsolutePath())); + } + + // Verify that all staged packages are internally consistent + final ArraySet stagedSplits = new ArraySet<>(); + final ArraySet stagedSplitTypes = new ArraySet<>(); + final ArraySet requiredSplitTypes = new ArraySet<>(); + final ArrayMap splitApks = new ArrayMap<>(); + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + for (ApkLite apk : addedFiles) { + if (!stagedSplits.add(apk.getSplitName())) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Split " + apk.getSplitName() + " was defined multiple times"); + } + + if (!apk.isUpdatableSystem()) { + if (mOriginalInstallerUid == Process.ROOT_UID) { + Slog.w(TAG, "Overriding updatableSystem because the installer is root for: " + + apk.getPackageName()); + } else { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Non updatable system package can't be installed or updated"); + } + } + + // Use first package to define unknown values + if (mPackageName == null) { + mPackageName = apk.getPackageName(); + mVersionCode = apk.getLongVersionCode(); + } + if (mSigningDetails == SigningDetails.UNKNOWN) { + mSigningDetails = apk.getSigningDetails(); + } + mHasDeviceAdminReceiver = apk.isHasDeviceAdminReceiver(); + + assertApkConsistentLocked(String.valueOf(apk), apk); + + // Take this opportunity to enforce uniform naming + final String targetName = ApkLiteParseUtils.splitNameToFileName(apk); + if (!FileUtils.isValidExtFilename(targetName)) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Invalid filename: " + targetName); + } + + // Yell loudly if installers drop attribute installLocation when apps explicitly set. + if (apk.getInstallLocation() != PackageInfo.INSTALL_LOCATION_UNSPECIFIED) { + final String installerPackageName = getInstallerPackageName(); + if (installerPackageName != null + && (params.installLocation != apk.getInstallLocation())) { + Slog.wtf(TAG, installerPackageName + + " drops manifest attribute android:installLocation in " + targetName + + " for " + mPackageName); + } + } + + final File targetFile = new File(stageDir, targetName); + if (!isArchivedInstallation()) { + final File sourceFile = new File(apk.getPath()); + resolveAndStageFileLocked(sourceFile, targetFile, apk.getSplitName()); + } + + // Base is coming from session + if (apk.getSplitName() == null) { + mResolvedBaseFile = targetFile; + baseApk = apk; + } else { + splitApks.put(apk.getSplitName(), apk); + } + + // Collect the requiredSplitTypes and staged splitTypes + CollectionUtils.addAll(requiredSplitTypes, apk.getRequiredSplitTypes()); + CollectionUtils.addAll(stagedSplitTypes, apk.getSplitTypes()); + } + + if (removeSplitList.size() > 0) { + if (pkgInfo == null) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Missing existing base package for " + mPackageName); + } + + // validate split names marked for removal + for (String splitName : removeSplitList) { + if (!ArrayUtils.contains(pkgInfo.splitNames, splitName)) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Split not found: " + splitName); + } + } + + // ensure we've got appropriate package name, version code and signatures + if (mPackageName == null) { + mPackageName = pkgInfo.packageName; + mVersionCode = pkgInfo.getLongVersionCode(); + } + if (mSigningDetails == SigningDetails.UNKNOWN) { + mSigningDetails = unsafeGetCertsWithoutVerification( + pkgInfo.applicationInfo.sourceDir); + } + } + + final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + final PackageStateInternal existingPkgSetting = pmi.getPackageStateInternal(mPackageName); + + if (!isInstallationAllowed(existingPkgSetting)) { + throw new PackageManagerException( + PackageManager.INSTALL_FAILED_SESSION_INVALID, + "Installation of this package is not allowed."); + } + + if (isArchivedInstallation()) { + if (!isArchivedInstallationAllowed(existingPkgSetting)) { + throw new PackageManagerException( + PackageManager.INSTALL_FAILED_SESSION_INVALID, + "Archived installation of this package is not allowed."); + } + + if (!mPm.mInstallerService.mPackageArchiver.verifySupportsUnarchival( + getInstallSource().mInstallerPackageName, userId)) { + throw new PackageManagerException( + PackageManager.INSTALL_FAILED_SESSION_INVALID, + "Installer has to support unarchival in order to install archived " + + "packages."); + } + } + + if (isIncrementalInstallation()) { + if (!isIncrementalInstallationAllowed(existingPkgSetting)) { + throw new PackageManagerException( + PackageManager.INSTALL_FAILED_SESSION_INVALID, + "Incremental installation of this package is not allowed."); + } + // Since we moved the staged app metadata file so that incfs can be initialized, lets + // now move it back. + File appMetadataFile = getTmpAppMetadataFile(); + if (appMetadataFile.exists()) { + final IncrementalFileStorages incrementalFileStorages = + getIncrementalFileStorages(); + try { + incrementalFileStorages.makeFile(APP_METADATA_FILE_NAME, + Files.readAllBytes(appMetadataFile.toPath()), + APP_METADATA_FILE_ACCESS_MODE); + } catch (IOException e) { + Slog.e(TAG, "Failed to write app metadata to incremental storage", e); + } finally { + appMetadataFile.delete(); + } + } + } + + if (mInstallerUid != mOriginalInstallerUid) { + // Session has been transferred, check package name. + if (TextUtils.isEmpty(mPackageName) || !mPackageName.equals( + mOriginalInstallerPackageName)) { + throw new PackageManagerException(PackageManager.INSTALL_FAILED_PACKAGE_CHANGED, + "Can only transfer sessions that update the original installer"); + } + } + + if (!mChecksums.isEmpty()) { + throw new PackageManagerException( + PackageManager.INSTALL_FAILED_SESSION_INVALID, + "Invalid checksum name(s): " + String.join(",", mChecksums.keySet())); + } + + if (params.mode == SessionParams.MODE_FULL_INSTALL) { + // Full installs must include a base package + if (!stagedSplits.contains(null)) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Full install must include a base package"); + } else if ((params.installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) { + EventLog.writeEvent(0x534e4554, "219044664"); + + // Installing base.apk. Make sure the app is restarted. + params.setDontKillApp(false); + } + if (baseApk.isSplitRequired() && (stagedSplits.size() <= 1 + || !stagedSplitTypes.containsAll(requiredSplitTypes))) { + throw new PackageManagerException(INSTALL_FAILED_MISSING_SPLIT, + "Missing split for " + mPackageName); + } + // For mode full install, we compose package lite for future usage instead of + // re-parsing it again and again. + final ParseResult pkgLiteResult = + ApkLiteParseUtils.composePackageLiteFromApks(input.reset(), stageDir, baseApk, + splitApks, true); + if (pkgLiteResult.isError()) { + throw new PackageManagerException(pkgLiteResult.getErrorCode(), + pkgLiteResult.getErrorMessage(), pkgLiteResult.getException()); + } + mPackageLite = pkgLiteResult.getResult(); + packageLite = mPackageLite; + } else { + final ApplicationInfo appInfo = pkgInfo.applicationInfo; + ParseResult pkgLiteResult = ApkLiteParseUtils.parsePackageLite( + input.reset(), new File(appInfo.getCodePath()), 0); + if (pkgLiteResult.isError()) { + throw new PackageManagerException(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, + pkgLiteResult.getErrorMessage(), pkgLiteResult.getException()); + } + final PackageLite existing = pkgLiteResult.getResult(); + packageLite = existing; + assertPackageConsistentLocked("Existing", existing.getPackageName(), + existing.getLongVersionCode()); + final SigningDetails signingDetails = + unsafeGetCertsWithoutVerification(existing.getBaseApkPath()); + if (!mSigningDetails.signaturesMatchExactly(signingDetails)) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Existing signatures are inconsistent"); + } + + // Inherit base if not overridden. + if (mResolvedBaseFile == null) { + mResolvedBaseFile = new File(appInfo.getBaseCodePath()); + inheritFileLocked(mResolvedBaseFile); + // Collect the requiredSplitTypes from base + CollectionUtils.addAll(requiredSplitTypes, existing.getBaseRequiredSplitTypes()); + } else if ((params.installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) { + EventLog.writeEvent(0x534e4554, "219044664"); + + // Installing base.apk. Make sure the app is restarted. + params.setDontKillApp(false); + } + + boolean existingSplitReplacedOrRemoved = false; + // Inherit splits if not overridden. + if (!ArrayUtils.isEmpty(existing.getSplitNames())) { + for (int i = 0; i < existing.getSplitNames().length; i++) { + final String splitName = existing.getSplitNames()[i]; + final File splitFile = new File(existing.getSplitApkPaths()[i]); + final boolean splitRemoved = removeSplitList.contains(splitName); + final boolean splitReplaced = stagedSplits.contains(splitName); + if (!splitReplaced && !splitRemoved) { + inheritFileLocked(splitFile); + // Collect the requiredSplitTypes and staged splitTypes from splits + CollectionUtils.addAll(requiredSplitTypes, + existing.getRequiredSplitTypes()[i]); + CollectionUtils.addAll(stagedSplitTypes, existing.getSplitTypes()[i]); + } else { + existingSplitReplacedOrRemoved = true; + } + } + } + if (existingSplitReplacedOrRemoved + && (params.installFlags & PackageManager.INSTALL_DONT_KILL_APP) != 0) { + // Some splits are being replaced or removed. Make sure the app is restarted. + params.setDontKillApp(false); + } + + // Inherit compiled oat directory. + final File packageInstallDir = (new File(appInfo.getBaseCodePath())).getParentFile(); + mInheritedFilesBase = packageInstallDir; + final File oatDir = new File(packageInstallDir, "oat"); + if (oatDir.exists()) { + final File[] archSubdirs = oatDir.listFiles(); + + // Keep track of all instruction sets we've seen compiled output for. + // If we're linking (and not copying) inherited files, we can recreate the + // instruction set hierarchy and link compiled output. + if (archSubdirs != null && archSubdirs.length > 0) { + final String[] instructionSets = InstructionSets.getAllDexCodeInstructionSets(); + for (File archSubDir : archSubdirs) { + // Skip any directory that isn't an ISA subdir. + if (!ArrayUtils.contains(instructionSets, archSubDir.getName())) { + continue; + } + + File[] files = archSubDir.listFiles(); + if (files == null || files.length == 0) { + continue; + } + + mResolvedInstructionSets.add(archSubDir.getName()); + mResolvedInheritedFiles.addAll(Arrays.asList(files)); + } + } + } + + // Inherit native libraries for DONT_KILL sessions. + if (mayInheritNativeLibs() && removeSplitList.isEmpty()) { + File[] libDirs = new File[]{ + new File(packageInstallDir, NativeLibraryHelper.LIB_DIR_NAME), + new File(packageInstallDir, NativeLibraryHelper.LIB64_DIR_NAME)}; + for (File libDir : libDirs) { + if (!libDir.exists() || !libDir.isDirectory()) { + continue; + } + final List libDirsToInherit = new ArrayList<>(); + final List libFilesToInherit = new ArrayList<>(); + for (File archSubDir : libDir.listFiles()) { + if (!archSubDir.isDirectory()) { + continue; + } + String relLibPath; + try { + relLibPath = getRelativePath(archSubDir, packageInstallDir); + } catch (IOException e) { + Slog.e(TAG, "Skipping linking of native library directory!", e); + // shouldn't be possible, but let's avoid inheriting these to be safe + libDirsToInherit.clear(); + libFilesToInherit.clear(); + break; + } + + File[] files = archSubDir.listFiles(); + if (files == null || files.length == 0) { + continue; + } + + libDirsToInherit.add(relLibPath); + libFilesToInherit.addAll(Arrays.asList(files)); + } + for (String subDir : libDirsToInherit) { + if (!mResolvedNativeLibPaths.contains(subDir)) { + mResolvedNativeLibPaths.add(subDir); + } + } + mResolvedInheritedFiles.addAll(libFilesToInherit); + } + } + // For the case of split required, failed if no splits existed + if (packageLite.isSplitRequired()) { + final int existingSplits = ArrayUtils.size(existing.getSplitNames()); + final boolean allSplitsRemoved = (existingSplits == removeSplitList.size()); + final boolean onlyBaseFileStaged = (stagedSplits.size() == 1 + && stagedSplits.contains(null)); + if ((allSplitsRemoved && (stagedSplits.isEmpty() || onlyBaseFileStaged)) + || !stagedSplitTypes.containsAll(requiredSplitTypes)) { + throw new PackageManagerException(INSTALL_FAILED_MISSING_SPLIT, + "Missing split for " + mPackageName); + } + } + } + + assertPreapprovalDetailsConsistentIfNeededLocked(packageLite, pkgInfo); + + if (packageLite.isUseEmbeddedDex()) { + for (File file : mResolvedStagedFiles) { + if (file.getName().endsWith(".apk") + && !DexManager.auditUncompressedDexInApk(file.getPath())) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Some dex are not uncompressed and aligned correctly for " + + mPackageName); + } + } + } + + final boolean isInstallerShell = (mInstallerUid == Process.SHELL_UID); + if (isInstallerShell && isIncrementalInstallation() && mIncrementalFileStorages != null) { + if (!packageLite.isDebuggable() && !packageLite.isProfileableByShell()) { + mIncrementalFileStorages.disallowReadLogs(); + } + } + + // {@link #sendPendingUserActionIntentIfNeeded} needs to use + // {@link PackageLite#getTargetSdk()} + mValidatedTargetSdk = packageLite.getTargetSdk(); + + return packageLite; + } + + @GuardedBy("mLock") + private void stageFileLocked(File origFile, File targetFile) + throws PackageManagerException { + mResolvedStagedFiles.add(targetFile); + maybeRenameFile(origFile, targetFile); + } + + @GuardedBy("mLock") + private void maybeStageFsveritySignatureLocked(File origFile, File targetFile, + boolean fsVerityRequired) throws PackageManagerException { + if (android.security.Flags.deprecateFsvSig()) { + return; + } + final File originalSignature = new File( + VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); + if (originalSignature.exists()) { + final File stagedSignature = new File( + VerityUtils.getFsveritySignatureFilePath(targetFile.getPath())); + stageFileLocked(originalSignature, stagedSignature); + } else if (fsVerityRequired) { + throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE, + "Missing corresponding fs-verity signature to " + origFile); + } + } + + @GuardedBy("mLock") + private void maybeStageV4SignatureLocked(File origFile, File targetFile) + throws PackageManagerException { + final File originalSignature = new File(origFile.getPath() + V4Signature.EXT); + if (originalSignature.exists()) { + final File stagedSignature = new File(targetFile.getPath() + V4Signature.EXT); + stageFileLocked(originalSignature, stagedSignature); + } + } + + @GuardedBy("mLock") + private void maybeStageDexMetadataLocked(File origFile, File targetFile) + throws PackageManagerException { + final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(origFile); + if (dexMetadataFile == null) { + return; + } + + if (!FileUtils.isValidExtFilename(dexMetadataFile.getName())) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Invalid filename: " + dexMetadataFile); + } + final File targetDexMetadataFile = new File(stageDir, + DexMetadataHelper.buildDexMetadataPathForApk(targetFile.getName())); + + stageFileLocked(dexMetadataFile, targetDexMetadataFile); + + // Also stage .dm.fsv_sig. .dm may be required to install with fs-verity signature on + // supported on older devices. + maybeStageFsveritySignatureLocked(dexMetadataFile, targetDexMetadataFile, + DexMetadataHelper.isFsVerityRequired()); + } + + private IncrementalFileStorages getIncrementalFileStorages() { + synchronized (mLock) { + return mIncrementalFileStorages; + } + } + + private void storeBytesToInstallationFile(final String localPath, final String absolutePath, + final byte[] bytes) throws IOException { + final IncrementalFileStorages incrementalFileStorages = getIncrementalFileStorages(); + if (!isIncrementalInstallation() || incrementalFileStorages == null) { + FileUtils.bytesToFile(absolutePath, bytes); + } else { + incrementalFileStorages.makeFile(localPath, bytes, 0777); + } + } + + @GuardedBy("mLock") + private void maybeStageDigestsLocked(File origFile, File targetFile, String splitName) + throws PackageManagerException { + final PerFileChecksum perFileChecksum = mChecksums.get(origFile.getName()); + if (perFileChecksum == null) { + return; + } + mChecksums.remove(origFile.getName()); + + final Checksum[] checksums = perFileChecksum.getChecksums(); + if (checksums.length == 0) { + return; + } + + final String targetDigestsPath = ApkChecksums.buildDigestsPathForApk(targetFile.getName()); + final File targetDigestsFile = new File(stageDir, targetDigestsPath); + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + ApkChecksums.writeChecksums(os, checksums); + + final byte[] signature = perFileChecksum.getSignature(); + if (signature != null && signature.length > 0) { + Certificate[] ignored = ApkChecksums.verifySignature(checksums, signature); + } + + // Storing and staging checksums. + storeBytesToInstallationFile(targetDigestsPath, targetDigestsFile.getAbsolutePath(), + os.toByteArray()); + stageFileLocked(targetDigestsFile, targetDigestsFile); + + // Storing and staging signature. + if (signature == null || signature.length == 0) { + return; + } + + final String targetDigestsSignaturePath = ApkChecksums.buildSignaturePathForDigests( + targetDigestsPath); + final File targetDigestsSignatureFile = new File(stageDir, targetDigestsSignaturePath); + storeBytesToInstallationFile(targetDigestsSignaturePath, + targetDigestsSignatureFile.getAbsolutePath(), signature); + stageFileLocked(targetDigestsSignatureFile, targetDigestsSignatureFile); + } catch (IOException e) { + throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE, + "Failed to store digests for " + mPackageName, e); + } catch (NoSuchAlgorithmException | SignatureException e) { + throw new PackageManagerException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, + "Failed to verify digests' signature for " + mPackageName, e); + } + } + + @GuardedBy("mLock") + private boolean isFsVerityRequiredForApk(File origFile, File targetFile) + throws PackageManagerException { + if (mVerityFoundForApks) { + return true; + } + + // We haven't seen .fsv_sig for any APKs. Treat it as not required until we see one. + final File originalSignature = new File( + VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); + if (!originalSignature.exists()) { + return false; + } + mVerityFoundForApks = true; + + // When a signature is found, also check any previous staged APKs since they also need to + // have fs-verity signature consistently. + for (File file : mResolvedStagedFiles) { + if (!file.getName().endsWith(".apk")) { + continue; + } + // Ignore the current targeting file. + if (targetFile.getName().equals(file.getName())) { + continue; + } + throw new PackageManagerException(INSTALL_FAILED_BAD_SIGNATURE, + "Previously staged apk is missing fs-verity signature"); + } + return true; + } + + @GuardedBy("mLock") + private void resolveAndStageFileLocked(File origFile, File targetFile, String splitName) + throws PackageManagerException { + stageFileLocked(origFile, targetFile); + + // Stage APK's fs-verity signature if present. + maybeStageFsveritySignatureLocked(origFile, targetFile, + isFsVerityRequiredForApk(origFile, targetFile)); + // Stage APK's v4 signature if present, and fs-verity is supported. + if (android.security.Flags.extendVbChainToUpdatedApk() + && VerityUtils.isFsVeritySupported()) { + maybeStageV4SignatureLocked(origFile, targetFile); + } + // Stage dex metadata (.dm) and corresponding fs-verity signature if present. + maybeStageDexMetadataLocked(origFile, targetFile); + // Stage checksums (.digests) if present. + maybeStageDigestsLocked(origFile, targetFile, splitName); + } + + @GuardedBy("mLock") + private void maybeInheritFsveritySignatureLocked(File origFile) { + // Inherit the fsverity signature file if present. + final File fsveritySignatureFile = new File( + VerityUtils.getFsveritySignatureFilePath(origFile.getPath())); + if (fsveritySignatureFile.exists()) { + mResolvedInheritedFiles.add(fsveritySignatureFile); + } + } + + @GuardedBy("mLock") + private void maybeInheritV4SignatureLocked(File origFile) { + // Inherit the v4 signature file if present. + final File v4SignatureFile = new File(origFile.getPath() + V4Signature.EXT); + if (v4SignatureFile.exists()) { + mResolvedInheritedFiles.add(v4SignatureFile); + } + } + + @GuardedBy("mLock") + private void inheritFileLocked(File origFile) { + mResolvedInheritedFiles.add(origFile); + + maybeInheritFsveritySignatureLocked(origFile); + if (android.security.Flags.extendVbChainToUpdatedApk()) { + maybeInheritV4SignatureLocked(origFile); + } + + // Inherit the dex metadata if present. + final File dexMetadataFile = + DexMetadataHelper.findDexMetadataForFile(origFile); + if (dexMetadataFile != null) { + mResolvedInheritedFiles.add(dexMetadataFile); + maybeInheritFsveritySignatureLocked(dexMetadataFile); + } + // Inherit the digests if present. + final File digestsFile = ApkChecksums.findDigestsForFile(origFile); + if (digestsFile != null) { + mResolvedInheritedFiles.add(digestsFile); + + final File signatureFile = ApkChecksums.findSignatureForDigests(digestsFile); + if (signatureFile != null) { + mResolvedInheritedFiles.add(signatureFile); + } + } + } + + @GuardedBy("mLock") + private void assertApkConsistentLocked(String tag, ApkLite apk) + throws PackageManagerException { + assertPackageConsistentLocked(tag, apk.getPackageName(), apk.getLongVersionCode()); + if (!mSigningDetails.signaturesMatchExactly(apk.getSigningDetails())) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + tag + " signatures are inconsistent"); + } + } + + @GuardedBy("mLock") + private void assertPackageConsistentLocked(String tag, String packageName, + long versionCode) throws PackageManagerException { + if (!mPackageName.equals(packageName)) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + " package " + + packageName + " inconsistent with " + mPackageName); + } + if (params.appPackageName != null && !params.appPackageName.equals(packageName)) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + + " specified package " + params.appPackageName + + " inconsistent with " + packageName); + } + if (mVersionCode != versionCode) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + + " version code " + versionCode + " inconsistent with " + + mVersionCode); + } + } + + @GuardedBy("mLock") + private void assertPreapprovalDetailsConsistentIfNeededLocked(@NonNull PackageLite packageLite, + @Nullable PackageInfo info) throws PackageManagerException { + if (mPreapprovalDetails == null || !isPreapprovalRequested()) { + return; + } + + if (!TextUtils.equals(mPackageName, mPreapprovalDetails.getPackageName())) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + mPreapprovalDetails + " inconsistent with " + mPackageName); + } + + final PackageManager packageManager = mContext.getPackageManager(); + // The given info isn't null only when params.appPackageName is set. + final PackageInfo existingPackageInfo = + info != null ? info : mPm.snapshotComputer().getPackageInfo(mPackageName, + 0 /* flags */, userId); + // If the app label in PreapprovalDetails matches the existing one, we treat it as valid. + final CharSequence appLabel = mPreapprovalDetails.getLabel(); + if (existingPackageInfo != null) { + final ApplicationInfo existingAppInfo = existingPackageInfo.applicationInfo; + final CharSequence existingAppLabel = packageManager.getApplicationLabel( + existingAppInfo); + if (TextUtils.equals(appLabel, existingAppLabel)) { + return; + } + } + + final PackageInfo packageInfoFromApk = packageManager.getPackageArchiveInfo( + packageLite.getPath(), PackageInfoFlags.of(0)); + if (packageInfoFromApk == null) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Failure to obtain package info from APK files."); + } + + // In case the app label in PreapprovalDetails from different locale in split APK, + // we check all APK files to find the app label. + final List filePaths = packageLite.getAllApkPaths(); + final ULocale appLocale = mPreapprovalDetails.getLocale(); + final ApplicationInfo appInfo = packageInfoFromApk.applicationInfo; + boolean appLabelMatched = false; + for (int i = filePaths.size() - 1; i >= 0 && !appLabelMatched; i--) { + appLabelMatched |= TextUtils.equals(getAppLabel(filePaths.get(i), appLocale, appInfo), + appLabel); + } + if (!appLabelMatched) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + mPreapprovalDetails + " inconsistent with app label"); + } + } + + private CharSequence getAppLabel(String path, ULocale locale, ApplicationInfo appInfo) + throws PackageManagerException { + final Resources pRes = mContext.getResources(); + final AssetManager assetManager = new AssetManager(); + final Configuration config = new Configuration(pRes.getConfiguration()); + final ApkAssets apkAssets; + try { + apkAssets = ApkAssets.loadFromPath(path); + } catch (IOException e) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Failure to get resources from package archive " + path); + } + assetManager.setApkAssets(new ApkAssets[]{apkAssets}, false /* invalidateCaches */); + config.setLocale(locale.toLocale()); + final Resources res = new Resources(assetManager, pRes.getDisplayMetrics(), config); + return TextUtils.trimToSize(tryLoadingAppLabel(res, appInfo), MAX_SAFE_LABEL_LENGTH); + } + + private CharSequence tryLoadingAppLabel(@NonNull Resources res, @NonNull ApplicationInfo info) { + CharSequence label = null; + // Try to load the label from the package's resources. If an app has not explicitly + // specified any label, just use the package name. + if (info.labelRes != 0) { + try { + label = res.getText(info.labelRes).toString().trim(); + } catch (Resources.NotFoundException ignore) { + } + } + if (label == null) { + label = (info.nonLocalizedLabel != null) + ? info.nonLocalizedLabel : info.packageName; + } + + return label; + } + + private SigningDetails unsafeGetCertsWithoutVerification(String path) + throws PackageManagerException { + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final ParseResult result = + ApkSignatureVerifier.unsafeGetCertsWithoutVerification( + input, path, SigningDetails.SignatureSchemeVersion.JAR, false); + if (result.isError()) { + throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, + "Couldn't obtain signatures from APK : " + path); + } + return result.getResult(); + } + + /** + * Determine if creating hard links between source and destination is + * possible. That is, do they all live on the same underlying device. + */ + private static boolean isLinkPossible(List fromFiles, File toDir) { + try { + final StructStat toStat = Os.stat(toDir.getAbsolutePath()); + for (File fromFile : fromFiles) { + final StructStat fromStat = Os.stat(fromFile.getAbsolutePath()); + if (fromStat.st_dev != toStat.st_dev) { + return false; + } + } + } catch (ErrnoException e) { + Slog.w(TAG, "Failed to detect if linking possible: " + e); + return false; + } + return true; + } + + /** + * @return the uid of the owner this session + */ + public int getInstallerUid() { + synchronized (mLock) { + return mInstallerUid; + } + } + + /** + * @return the package name of this session + */ + @VisibleForTesting(visibility = PACKAGE) + public String getPackageName() { + synchronized (mLock) { + return mPackageName; + } + } + + /** + * @return the timestamp of when this session last changed state + */ + public long getUpdatedMillis() { + synchronized (mLock) { + return updatedMillis; + } + } + + long getCommittedMillis() { + synchronized (mLock) { + return committedMillis; + } + } + + String getInstallerPackageName() { + return getInstallSource().mInstallerPackageName; + } + + String getInstallerAttributionTag() { + return getInstallSource().mInstallerAttributionTag; + } + + InstallSource getInstallSource() { + synchronized (mLock) { + return mInstallSource; + } + } + + SigningDetails getSigningDetails() { + synchronized (mLock) { + return mSigningDetails; + } + } + + PackageLite getPackageLite() { + synchronized (mLock) { + return mPackageLite; + } + } + + /** + * @return a boolean value indicating whether user action was requested for the install. + * Returns {@code false} if {@code mUserActionRequired} is {@code null} + */ + public boolean getUserActionRequired() { + if (mUserActionRequired != null) { + return mUserActionRequired.booleanValue(); + } + Slog.wtf(TAG, "mUserActionRequired should not be null."); + return false; + } + + private static String getRelativePath(File file, File base) throws IOException { + final String pathStr = file.getAbsolutePath(); + final String baseStr = base.getAbsolutePath(); + // Don't allow relative paths. + if (pathStr.contains("/.") ) { + throw new IOException("Invalid path (was relative) : " + pathStr); + } + + if (pathStr.startsWith(baseStr)) { + return pathStr.substring(baseStr.length()); + } + + throw new IOException("File: " + pathStr + " outside base: " + baseStr); + } + + private void createOatDirs(String packageName, List instructionSets, File fromDir) + throws PackageManagerException { + for (String instructionSet : instructionSets) { + try { + mInstaller.createOatDir(packageName, fromDir.getAbsolutePath(), instructionSet); + } catch (InstallerException e) { + throw PackageManagerException.from(e); + } + } + } + + private void linkFile(String packageName, String relativePath, String fromBase, String toBase) + throws IOException { + try { + // Try + final IncrementalFileStorages incrementalFileStorages = getIncrementalFileStorages(); + if (incrementalFileStorages != null && incrementalFileStorages.makeLink(relativePath, + fromBase, toBase)) { + return; + } + mInstaller.linkFile(packageName, relativePath, fromBase, toBase); + } catch (InstallerException | IOException e) { + throw new IOException("failed linkOrCreateDir(" + relativePath + ", " + + fromBase + ", " + toBase + ")", e); + } + } + + private void linkFiles(String packageName, List fromFiles, File toDir, File fromDir) + throws IOException { + for (File fromFile : fromFiles) { + final String relativePath = getRelativePath(fromFile, fromDir); + final String fromBase = fromDir.getAbsolutePath(); + final String toBase = toDir.getAbsolutePath(); + + linkFile(packageName, relativePath, fromBase, toBase); + } + + Slog.d(TAG, "Linked " + fromFiles.size() + " files into " + toDir); + } + + private static void copyFiles(List fromFiles, File toDir) throws IOException { + // Remove any partial files from previous attempt + for (File file : toDir.listFiles()) { + if (file.getName().endsWith(".tmp")) { + file.delete(); + } + } + + for (File fromFile : fromFiles) { + final File tmpFile = File.createTempFile("inherit", ".tmp", toDir); + if (LOGD) Slog.d(TAG, "Copying " + fromFile + " to " + tmpFile); + if (!FileUtils.copyFile(fromFile, tmpFile)) { + throw new IOException("Failed to copy " + fromFile + " to " + tmpFile); + } + try { + Os.chmod(tmpFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE); + } catch (ErrnoException e) { + throw new IOException("Failed to chmod " + tmpFile); + } + final File toFile = new File(toDir, fromFile.getName()); + if (LOGD) Slog.d(TAG, "Renaming " + tmpFile + " to " + toFile); + if (!tmpFile.renameTo(toFile)) { + throw new IOException("Failed to rename " + tmpFile + " to " + toFile); + } + } + Slog.d(TAG, "Copied " + fromFiles.size() + " files into " + toDir); + } + + private void extractNativeLibraries(PackageLite packageLite, File packageDir, + String abiOverride, boolean inherit) + throws PackageManagerException { + Objects.requireNonNull(packageLite); + final File libDir = new File(packageDir, NativeLibraryHelper.LIB_DIR_NAME); + if (!inherit) { + // Start from a clean slate + NativeLibraryHelper.removeNativeBinariesFromDirLI(libDir, true); + } + + // Skip native libraries processing for archival installation. + if (isArchivedInstallation()) { + return; + } + + NativeLibraryHelper.Handle handle = null; + try { + handle = NativeLibraryHelper.Handle.create(packageLite); + final int res = NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libDir, + abiOverride, isIncrementalInstallation()); + if (res != INSTALL_SUCCEEDED) { + throw new PackageManagerException(res, + "Failed to extract native libraries, res=" + res); + } + } catch (IOException e) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Failed to extract native libraries", e); + } finally { + IoUtils.closeQuietly(handle); + } + } + + void setPermissionsResult(boolean accepted) { + if (!isSealed() && !isPreapprovalRequested()) { + throw new SecurityException("Must be sealed to accept permissions"); + } + + // To support pre-approval request of atomic install, we allow child session to handle + // the result by itself since it has the status receiver. + final PackageInstallerSession root = hasParentSessionId() && isCommitted() + ? mSessionProvider.getSession(getParentSessionId()) : this; + + if (accepted) { + // Mark and kick off another install pass + synchronized (mLock) { + mPermissionsManuallyAccepted = true; + } + root.mHandler.obtainMessage( + isCommitted() ? MSG_INSTALL : MSG_PRE_APPROVAL_REQUEST).sendToTarget(); + } else { + root.destroy("User rejected permissions"); + root.dispatchSessionFinished(INSTALL_FAILED_ABORTED, "User rejected permissions", null); + root.maybeFinishChildSessions(INSTALL_FAILED_ABORTED, "User rejected permissions"); + } + } + + public void open() throws IOException { + activate(); + boolean wasPrepared; + synchronized (mLock) { + wasPrepared = mPrepared; + if (!mPrepared) { + if (stageDir != null) { + prepareStageDir(stageDir); + } else if (params.isMultiPackage) { + // it's all ok + } else { + throw new IllegalArgumentException("stageDir must be set"); + } + + mPrepared = true; + } + } + + if (!wasPrepared) { + mCallback.onSessionPrepared(this); + } + } + + private void activate() { + if (mActiveCount.getAndIncrement() == 0) { + mCallback.onSessionActiveChanged(this, true); + } + } + + @Override + public void close() { + closeInternal(true); + } + + private void closeInternal(boolean checkCaller) { + synchronized (mLock) { + if (checkCaller) { + assertCallerIsOwnerOrRoot(); + } + } + deactivate(); + } + + private void deactivate() { + int activeCount; + synchronized (mLock) { + activeCount = mActiveCount.decrementAndGet(); + } + if (activeCount == 0) { + mCallback.onSessionActiveChanged(this, false); + } + } + + /** + * Calls dispatchSessionFinished() on all child sessions with the given error code and + * error message to prevent orphaned child sessions. + */ + private void maybeFinishChildSessions(int returnCode, String msg) { + for (PackageInstallerSession child : getChildSessions()) { + child.dispatchSessionFinished(returnCode, msg, null); + } + } + + private void assertNotChild(String cookie) { + if (hasParentSessionId()) { + throw new IllegalStateException(cookie + " can't be called on a child session, id=" + + sessionId + " parentId=" + getParentSessionId()); + } + } + + /** + * Called when verification has completed. Now it is safe to clean up the session + * if {@link #abandon()} has been called previously. + * + * @return True if this session has been abandoned. + */ + private boolean dispatchPendingAbandonCallback() { + final Runnable callback; + synchronized (mLock) { + if (!mStageDirInUse) { + return false; + } + mStageDirInUse = false; + callback = mPendingAbandonCallback; + mPendingAbandonCallback = null; + } + if (callback != null) { + callback.run(); + return true; + } + return false; + } + + @Override + public void abandon() { + final Runnable r; + synchronized (mLock) { + assertNotChild("abandon"); + assertCallerIsOwnerOrRootOrSystem(); + if (isInTerminalState()) { + // Finalized sessions have been properly cleaned up. No need to abandon them. + return; + } + mDestroyed = true; + r = () -> { + assertNotLocked("abandonStaged"); + if (isStaged() && isCommitted()) { + mStagingManager.abortCommittedSession(mStagedSession); + } + destroy("Session was abandoned"); + dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned", null); + maybeFinishChildSessions(INSTALL_FAILED_ABORTED, + "Session was abandoned because the parent session is abandoned"); + }; + if (mStageDirInUse) { + // Verification is ongoing, not safe to clean up the session yet. + mPendingAbandonCallback = r; + mCallback.onSessionChanged(this); + return; + } + } + + final long token = Binder.clearCallingIdentity(); + try { + // This will call into StagingManager which might trigger external callbacks + r.run(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + public boolean isMultiPackage() { + return params.isMultiPackage; + } + + @Override + public boolean isStaged() { + return params.isStaged; + } + + @Override + public int getInstallFlags() { + return params.installFlags; + } + + @android.annotation.EnforcePermission(android.Manifest.permission.USE_INSTALLER_V2) + @Override + public DataLoaderParamsParcel getDataLoaderParams() { + getDataLoaderParams_enforcePermission(); + return params.dataLoaderParams != null ? params.dataLoaderParams.getData() : null; + } + + @android.annotation.EnforcePermission(android.Manifest.permission.USE_INSTALLER_V2) + @Override + public void addFile(int location, String name, long lengthBytes, byte[] metadata, + byte[] signature) { + addFile_enforcePermission(); + if (!isDataLoaderInstallation()) { + throw new IllegalStateException( + "Cannot add files to non-data loader installation session."); + } + if (isStreamingInstallation()) { + if (location != LOCATION_DATA_APP) { + throw new IllegalArgumentException( + "Non-incremental installation only supports /data/app placement: " + name); + } + } + if (metadata == null) { + throw new IllegalArgumentException( + "DataLoader installation requires valid metadata: " + name); + } + // Use installer provided name for now; we always rename later + if (!FileUtils.isValidExtFilename(name)) { + throw new IllegalArgumentException("Invalid name: " + name); + } + + synchronized (mLock) { + assertCallerIsOwnerOrRoot(); + assertPreparedAndNotSealedLocked("addFile"); + + if (!mFiles.add(new FileEntry(mFiles.size(), + new InstallationFile(location, name, lengthBytes, metadata, signature)))) { + throw new IllegalArgumentException("File already added: " + name); + } + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.USE_INSTALLER_V2) + @Override + public void removeFile(int location, String name) { + removeFile_enforcePermission(); + if (!isDataLoaderInstallation()) { + throw new IllegalStateException( + "Cannot add files to non-data loader installation session."); + } + if (TextUtils.isEmpty(params.appPackageName)) { + throw new IllegalStateException("Must specify package name to remove a split"); + } + + synchronized (mLock) { + assertCallerIsOwnerOrRoot(); + assertPreparedAndNotSealedLocked("removeFile"); + + if (!mFiles.add(new FileEntry(mFiles.size(), + new InstallationFile(location, getRemoveMarkerName(name), -1, null, null)))) { + throw new IllegalArgumentException("File already removed: " + name); + } + } + } + + /** + * Makes sure files are present in staging location. + * @return if the image is ready for installation + */ + @GuardedBy("mLock") + private boolean prepareDataLoaderLocked() + throws PackageManagerException { + if (!isDataLoaderInstallation()) { + return true; + } + if (mDataLoaderFinished) { + return true; + } + + final List addedFiles = new ArrayList<>(); + final List removedFiles = new ArrayList<>(); + + final InstallationFile[] files = getInstallationFilesLocked(); + for (InstallationFile file : files) { + if (sAddedFilter.accept(new File(this.stageDir, file.getName()))) { + addedFiles.add(file.getData()); + continue; + } + if (sRemovedFilter.accept(new File(this.stageDir, file.getName()))) { + String name = file.getName().substring( + 0, file.getName().length() - REMOVE_MARKER_EXTENSION.length()); + removedFiles.add(name); + } + } + + final DataLoaderParams params = this.params.dataLoaderParams; + final boolean manualStartAndDestroy = !isIncrementalInstallation(); + final boolean systemDataLoader = isSystemDataLoaderInstallation(); + final IDataLoaderStatusListener statusListener = new IDataLoaderStatusListener.Stub() { + @Override + public void onStatusChanged(int dataLoaderId, int status) { + switch (status) { + case IDataLoaderStatusListener.DATA_LOADER_BINDING: + case IDataLoaderStatusListener.DATA_LOADER_STOPPED: + case IDataLoaderStatusListener.DATA_LOADER_DESTROYED: + return; + } + + if (mDestroyed || mDataLoaderFinished) { + switch (status) { + case IDataLoaderStatusListener.DATA_LOADER_UNRECOVERABLE: + if (systemDataLoader) { + onSystemDataLoaderUnrecoverable(); + } + return; + } + return; + } + try { + switch (status) { + case IDataLoaderStatusListener.DATA_LOADER_BOUND: { + if (manualStartAndDestroy) { + FileSystemControlParcel control = new FileSystemControlParcel(); + control.callback = new FileSystemConnector(addedFiles); + getDataLoader(dataLoaderId).create(dataLoaderId, params.getData(), + control, this); + } + + break; + } + case IDataLoaderStatusListener.DATA_LOADER_CREATED: { + if (manualStartAndDestroy) { + // IncrementalFileStorages will call start after all files are + // created in IncFS. + getDataLoader(dataLoaderId).start(dataLoaderId); + } + break; + } + case IDataLoaderStatusListener.DATA_LOADER_STARTED: { + getDataLoader(dataLoaderId).prepareImage( + dataLoaderId, + addedFiles.toArray( + new InstallationFileParcel[addedFiles.size()]), + removedFiles.toArray(new String[removedFiles.size()])); + break; + } + case IDataLoaderStatusListener.DATA_LOADER_IMAGE_READY: { + mDataLoaderFinished = true; + if (hasParentSessionId()) { + mSessionProvider.getSession( + getParentSessionId()).dispatchSessionSealed(); + } else { + dispatchSessionSealed(); + } + if (manualStartAndDestroy) { + getDataLoader(dataLoaderId).destroy(dataLoaderId); + } + break; + } + case IDataLoaderStatusListener.DATA_LOADER_IMAGE_NOT_READY: { + mDataLoaderFinished = true; + dispatchSessionValidationFailure(INSTALL_FAILED_MEDIA_UNAVAILABLE, + "Failed to prepare image."); + if (manualStartAndDestroy) { + getDataLoader(dataLoaderId).destroy(dataLoaderId); + } + break; + } + case IDataLoaderStatusListener.DATA_LOADER_UNAVAILABLE: { + // Don't fail or commit the session. Allow caller to commit again. + sendPendingStreaming(mContext, getRemoteStatusReceiver(), sessionId, + "DataLoader unavailable"); + break; + } + case IDataLoaderStatusListener.DATA_LOADER_UNRECOVERABLE: + throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, + "DataLoader reported unrecoverable failure."); + } + } catch (PackageManagerException e) { + mDataLoaderFinished = true; + dispatchSessionValidationFailure(e.error, ExceptionUtils.getCompleteMessage(e)); + } catch (RemoteException e) { + // In case of streaming failure we don't want to fail or commit the session. + // Just return from this method and allow caller to commit again. + sendPendingStreaming(mContext, getRemoteStatusReceiver(), sessionId, + e.getMessage()); + } + } + }; + + if (!manualStartAndDestroy) { + final PerUidReadTimeouts[] perUidReadTimeouts = + mPm.getPerUidReadTimeouts(mPm.snapshotComputer()); + + final StorageHealthCheckParams healthCheckParams = new StorageHealthCheckParams(); + healthCheckParams.blockedTimeoutMs = INCREMENTAL_STORAGE_BLOCKED_TIMEOUT_MS; + healthCheckParams.unhealthyTimeoutMs = INCREMENTAL_STORAGE_UNHEALTHY_TIMEOUT_MS; + healthCheckParams.unhealthyMonitoringMs = INCREMENTAL_STORAGE_UNHEALTHY_MONITORING_MS; + + final IStorageHealthListener healthListener = new IStorageHealthListener.Stub() { + @Override + public void onHealthStatus(int storageId, int status) { + if (mDestroyed || mDataLoaderFinished) { + return; + } + + switch (status) { + case IStorageHealthListener.HEALTH_STATUS_OK: + break; + case IStorageHealthListener.HEALTH_STATUS_READS_PENDING: + case IStorageHealthListener.HEALTH_STATUS_BLOCKED: + if (systemDataLoader) { + // It's OK for ADB data loader to wait for pages. + break; + } + // fallthrough + case IStorageHealthListener.HEALTH_STATUS_UNHEALTHY: + // Even ADB installation can't wait for missing pages for too long. + mDataLoaderFinished = true; + dispatchSessionValidationFailure(INSTALL_FAILED_MEDIA_UNAVAILABLE, + "Image is missing pages required for installation."); + break; + } + } + }; + + try { + final PackageInfo pkgInfo = mPm.snapshotComputer() + .getPackageInfo(this.params.appPackageName, 0, userId); + final File inheritedDir = + (pkgInfo != null && pkgInfo.applicationInfo != null) ? new File( + pkgInfo.applicationInfo.getCodePath()).getParentFile() : null; + + if (mIncrementalFileStorages == null) { + mIncrementalFileStorages = IncrementalFileStorages.initialize(mContext, + stageDir, inheritedDir, params, statusListener, healthCheckParams, + healthListener, addedFiles, perUidReadTimeouts, + new IPackageLoadingProgressCallback.Stub() { + @Override + public void onPackageLoadingProgressChanged(float progress) { + synchronized (mProgressLock) { + mIncrementalProgress = progress; + computeProgressLocked(true); + } + } + }); + } else { + // Retrying commit. + mIncrementalFileStorages.startLoading(params, statusListener, healthCheckParams, + healthListener, perUidReadTimeouts); + } + return false; + } catch (IOException e) { + throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, e.getMessage(), + e.getCause()); + } + } + + final long bindDelayMs = 0; + if (!getDataLoaderManager().bindToDataLoader(sessionId, params.getData(), bindDelayMs, + statusListener)) { + throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, + "Failed to initialize data loader"); + } + + return false; + } + + private DataLoaderManager getDataLoaderManager() throws PackageManagerException { + DataLoaderManager dataLoaderManager = mContext.getSystemService(DataLoaderManager.class); + if (dataLoaderManager == null) { + throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, + "Failed to find data loader manager service"); + } + return dataLoaderManager; + } + + private IDataLoader getDataLoader(int dataLoaderId) throws PackageManagerException { + IDataLoader dataLoader = getDataLoaderManager().getDataLoader(dataLoaderId); + if (dataLoader == null) { + throw new PackageManagerException(INSTALL_FAILED_MEDIA_UNAVAILABLE, + "Failure to obtain data loader"); + } + return dataLoader; + } + + private void dispatchSessionValidationFailure(int error, String detailMessage) { + mHandler.obtainMessage(MSG_SESSION_VALIDATION_FAILURE, error, -1, + detailMessage).sendToTarget(); + } + + @GuardedBy("mLock") + private int[] getChildSessionIdsLocked() { + int size = mChildSessions.size(); + if (size == 0) { + return EMPTY_CHILD_SESSION_ARRAY; + } + final int[] childSessionIds = new int[size]; + for (int i = 0; i < size; ++i) { + childSessionIds[i] = mChildSessions.keyAt(i); + } + return childSessionIds; + } + + @Override + public int[] getChildSessionIds() { + synchronized (mLock) { + return getChildSessionIdsLocked(); + } + } + + private boolean canBeAddedAsChild(int parentCandidate) { + synchronized (mLock) { + return (!hasParentSessionId() || mParentSessionId == parentCandidate) + && !isCommitted() + && !mDestroyed; + } + } + + private void acquireTransactionLock() { + if (!mTransactionLock.compareAndSet(false, true)) { + throw new UnsupportedOperationException("Concurrent access not supported"); + } + } + + private void releaseTransactionLock() { + mTransactionLock.compareAndSet(true, false); + } + + @Override + public void addChildSessionId(int childSessionId) { + if (!params.isMultiPackage) { + throw new IllegalStateException("Single-session " + sessionId + " can't have child."); + } + + final PackageInstallerSession childSession = mSessionProvider.getSession(childSessionId); + if (childSession == null) { + throw new IllegalStateException("Unable to add child session " + childSessionId + + " as it does not exist."); + } + if (childSession.params.isMultiPackage) { + throw new IllegalStateException("Multi-session " + childSessionId + + " can't be a child."); + } + if (params.isStaged != childSession.params.isStaged) { + throw new IllegalStateException("Multipackage Inconsistency: session " + + childSession.sessionId + " and session " + sessionId + + " have inconsistent staged settings"); + } + if (params.getEnableRollback() != childSession.params.getEnableRollback()) { + throw new IllegalStateException("Multipackage Inconsistency: session " + + childSession.sessionId + " and session " + sessionId + + " have inconsistent rollback settings"); + } + boolean hasAPK = containsApkSession() || !childSession.isApexSession(); + boolean hasAPEX = sessionContains(s -> s.isApexSession()) || childSession.isApexSession(); + if (!params.isStaged && hasAPK && hasAPEX) { + throw new IllegalStateException("Mix of APK and APEX is not supported for " + + "non-staged multi-package session"); + } + + try { + acquireTransactionLock(); + childSession.acquireTransactionLock(); + + if (!childSession.canBeAddedAsChild(sessionId)) { + throw new IllegalStateException("Unable to add child session " + childSessionId + + " as it is in an invalid state."); + } + synchronized (mLock) { + assertCallerIsOwnerOrRoot(); + assertPreparedAndNotSealedLocked("addChildSessionId"); + + final int indexOfSession = mChildSessions.indexOfKey(childSessionId); + if (indexOfSession >= 0) { + return; + } + childSession.setParentSessionId(this.sessionId); + mChildSessions.put(childSessionId, childSession); + } + } finally { + releaseTransactionLock(); + childSession.releaseTransactionLock(); + } + } + + @Override + public void removeChildSessionId(int sessionId) { + synchronized (mLock) { + assertCallerIsOwnerOrRoot(); + assertPreparedAndNotSealedLocked("removeChildSessionId"); + + final int indexOfSession = mChildSessions.indexOfKey(sessionId); + if (indexOfSession < 0) { + // not added in the first place; no-op + return; + } + PackageInstallerSession session = mChildSessions.valueAt(indexOfSession); + try { + acquireTransactionLock(); + session.acquireTransactionLock(); + session.setParentSessionId(SessionInfo.INVALID_ID); + mChildSessions.removeAt(indexOfSession); + } finally { + releaseTransactionLock(); + session.releaseTransactionLock(); + } + } + } + + /** + * Sets the parent session ID if not already set. + * If {@link SessionInfo#INVALID_ID} is passed, it will be unset. + */ + void setParentSessionId(int parentSessionId) { + synchronized (mLock) { + if (parentSessionId != SessionInfo.INVALID_ID + && mParentSessionId != SessionInfo.INVALID_ID) { + throw new IllegalStateException("The parent of " + sessionId + " is" + " already" + + "set to " + mParentSessionId); + } + this.mParentSessionId = parentSessionId; + } + } + + boolean hasParentSessionId() { + synchronized (mLock) { + return mParentSessionId != SessionInfo.INVALID_ID; + } + } + + @Override + public int getParentSessionId() { + synchronized (mLock) { + return mParentSessionId; + } + } + + private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) { + // Session can be marked as finished due to user rejecting pre approval or commit request, + // any internal error or after successful completion. As such, check whether + // the session is in the preapproval stage or the commit stage. + sendUpdateToRemoteStatusReceiver(returnCode, msg, extras, + /* forPreapproval= */ isPreapprovalRequested() && !isCommitted()); + + synchronized (mLock) { + mFinalStatus = returnCode; + mFinalMessage = msg; + } + + final boolean success = (returnCode == INSTALL_SUCCEEDED); + + // Send broadcast to default launcher only if it's a new install + // TODO(b/144270665): Secure the usage of this broadcast. + final boolean isNewInstall = extras == null || !extras.getBoolean(Intent.EXTRA_REPLACING); + if (success && isNewInstall && mPm.mInstallerService.okToSendBroadcasts()) { + mPm.sendSessionCommitBroadcast(generateInfoScrubbed(true /*icon*/), userId); + } + + mCallback.onSessionFinished(this, success); + if (isDataLoaderInstallation()) { + logDataLoaderInstallationSession(returnCode); + } + } + + private void sendUpdateToRemoteStatusReceiver(int returnCode, String msg, Bundle extras, + boolean forPreapproval) { + final IntentSender statusReceiver = forPreapproval ? getPreapprovalRemoteStatusReceiver() + : getRemoteStatusReceiver(); + if (statusReceiver != null) { + // Execute observer.onPackageInstalled on different thread as we don't want callers + // inside the system server have to worry about catching the callbacks while they are + // calling into the session + final SomeArgs args = SomeArgs.obtain(); + args.arg1 = getPackageName(); + args.arg2 = msg; + args.arg3 = extras; + args.arg4 = statusReceiver; + args.argi1 = returnCode; + args.argi2 = isPreapprovalRequested() && !isCommitted() ? 1 : 0; + mHandler.obtainMessage(MSG_ON_PACKAGE_INSTALLED, args).sendToTarget(); + } + } + + private void dispatchSessionPreapproved() { + final IntentSender target = getPreapprovalRemoteStatusReceiver(); + final Intent intent = new Intent(); + intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); + intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_SUCCESS); + intent.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL, true); + try { + final BroadcastOptions options = BroadcastOptions.makeBasic(); + options.setPendingIntentBackgroundActivityLaunchAllowed(false); + target.sendIntent(mContext, 0 /* code */, intent, null /* onFinished */, + null /* handler */, null /* requiredPermission */, options.toBundle()); + } catch (IntentSender.SendIntentException ignored) { + } + } + + @Override + public void requestUserPreapproval(@NonNull PreapprovalDetails details, + @NonNull IntentSender statusReceiver) { + validatePreapprovalRequest(details, statusReceiver); + + if (!mPm.isPreapprovalRequestAvailable()) { + sendUpdateToRemoteStatusReceiver(INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE, + "Request user pre-approval is currently not available.", /* extras= */null, + /* preapproval= */true); + return; + } + + dispatchPreapprovalRequest(); + } + + /** + * Validates whether the necessary information (e.g., PreapprovalDetails) are provided. + */ + private void validatePreapprovalRequest(@NonNull PreapprovalDetails details, + @NonNull IntentSender statusReceiver) { + assertCallerIsOwnerOrRoot(); + if (isMultiPackage()) { + throw new IllegalStateException( + "Session " + sessionId + " is a parent of multi-package session and " + + "requestUserPreapproval on the parent session isn't supported."); + } + + synchronized (mLock) { + assertPreparedAndNotSealedLocked("request of session " + sessionId); + mPreapprovalDetails = details; + setPreapprovalRemoteStatusReceiver(statusReceiver); + } + } + + private void dispatchPreapprovalRequest() { + synchronized (mLock) { + assertPreparedAndNotPreapprovalRequestedLocked("dispatchPreapprovalRequest"); + } + + // Mark this session are pre-approval requested, and ready to progress to the next phase. + markAsPreapprovalRequested(); + + mHandler.obtainMessage(MSG_PRE_APPROVAL_REQUEST).sendToTarget(); + } + + /** + * Marks this session as pre-approval requested, and prevents further related modification. + */ + private void markAsPreapprovalRequested() { + mPreapprovalRequested.set(true); + } + + @Override + public boolean isApplicationEnabledSettingPersistent() { + return params.applicationEnabledSettingPersistent; + } + + @Override + public boolean isRequestUpdateOwnership() { + return (params.installFlags & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0; + } + + @Override + public void setPreVerifiedDomains(@NonNull DomainSet preVerifiedDomains) { + // First check permissions + final boolean exemptFromPermissionChecks = + (mInstallerUid == Process.ROOT_UID) || (mInstallerUid == Process.SHELL_UID); + if (!exemptFromPermissionChecks) { + final Computer snapshot = mPm.snapshotComputer(); + if (PackageManager.PERMISSION_GRANTED != snapshot.checkUidPermission( + Manifest.permission.ACCESS_INSTANT_APPS, mInstallerUid)) { + throw new SecurityException("You need android.permission.ACCESS_INSTANT_APPS " + + "permission to set pre-verified domains."); + } + ComponentName instantAppInstallerComponent = snapshot.getInstantAppInstallerComponent(); + if (instantAppInstallerComponent == null) { + // Shouldn't happen + throw new IllegalStateException("Instant app installer is not available. " + + "Only the instant app installer can call this API."); + } + if (!instantAppInstallerComponent.getPackageName().equals(getInstallerPackageName())) { + throw new SecurityException("Only the instant app installer can call this API."); + } + } + // Then check size limits + final long preVerifiedDomainsCountLimit = getPreVerifiedDomainsCountLimit(); + if (preVerifiedDomains.getDomains().size() > preVerifiedDomainsCountLimit) { + throw new IllegalArgumentException( + "The number of pre-verified domains have exceeded the maximum of " + + preVerifiedDomainsCountLimit); + } + final long preVerifiedDomainLengthLimit = getPreVerifiedDomainLengthLimit(); + for (String domain : preVerifiedDomains.getDomains()) { + if (domain.length() > preVerifiedDomainLengthLimit) { + throw new IllegalArgumentException( + "Pre-verified domain: [" + domain + " ] exceeds maximum length allowed: " + + preVerifiedDomainLengthLimit); + } + } + // Okay to proceed + synchronized (mLock) { + mPreVerifiedDomains = preVerifiedDomains; + } + } + + private static long getPreVerifiedDomainsCountLimit() { + final long token = Binder.clearCallingIdentity(); + try { + return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE, + PROPERTY_PRE_VERIFIED_DOMAINS_COUNT_LIMIT, + DEFAULT_PRE_VERIFIED_DOMAINS_COUNT_LIMIT); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private static long getPreVerifiedDomainLengthLimit() { + final long token = Binder.clearCallingIdentity(); + try { + return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE, + PROPERTY_PRE_VERIFIED_DOMAIN_LENGTH_LIMIT, + DEFAULT_PRE_VERIFIED_DOMAIN_LENGTH_LIMIT); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + @Override + @Nullable + public DomainSet getPreVerifiedDomains() { + assertCallerIsOwnerOrRoot(); + synchronized (mLock) { + assertPreparedAndNotCommittedOrDestroyedLocked("getPreVerifiedDomains"); + return mPreVerifiedDomains; + } + } + + + void setSessionReady() { + synchronized (mLock) { + // Do not allow destroyed/failed session to change state + if (mDestroyed || mSessionFailed) return; + mSessionReady = true; + mSessionApplied = false; + mSessionFailed = false; + mSessionErrorCode = PackageManager.INSTALL_UNKNOWN; + mSessionErrorMessage = ""; + } + mCallback.onSessionChanged(this); + } + + void setSessionFailed(int errorCode, String errorMessage) { + synchronized (mLock) { + // Do not allow destroyed/failed session to change state + if (mDestroyed || mSessionFailed) return; + mSessionReady = false; + mSessionApplied = false; + mSessionFailed = true; + mSessionErrorCode = errorCode; + mSessionErrorMessage = errorMessage; + Slog.d(TAG, "Marking session " + sessionId + " as failed: " + errorMessage); + } + destroy("Session marked as failed: " + errorMessage); + mCallback.onSessionChanged(this); + } + + private void setSessionApplied() { + synchronized (mLock) { + // Do not allow destroyed/failed session to change state + if (mDestroyed || mSessionFailed) return; + mSessionReady = false; + mSessionApplied = true; + mSessionFailed = false; + mSessionErrorCode = INSTALL_SUCCEEDED; + mSessionErrorMessage = ""; + Slog.d(TAG, "Marking session " + sessionId + " as applied"); + } + destroy(null); + mCallback.onSessionChanged(this); + } + + /** {@hide} */ + boolean isSessionReady() { + synchronized (mLock) { + return mSessionReady; + } + } + + /** {@hide} */ + boolean isSessionApplied() { + synchronized (mLock) { + return mSessionApplied; + } + } + + /** {@hide} */ + boolean isSessionFailed() { + synchronized (mLock) { + return mSessionFailed; + } + } + + /** {@hide} */ + int getSessionErrorCode() { + synchronized (mLock) { + return mSessionErrorCode; + } + } + + /** {@hide} */ + String getSessionErrorMessage() { + synchronized (mLock) { + return mSessionErrorMessage; + } + } + + void registerUnarchivalListener(IntentSender intentSender) { + synchronized (mLock) { + this.mUnarchivalListeners.add(intentSender); + } + } + + Set getUnarchivalListeners() { + synchronized (mLock) { + return new ArraySet<>(mUnarchivalListeners); + } + } + + void reportUnarchivalStatus(@UnarchivalStatus int status, int unarchiveId, + long requiredStorageBytes, PendingIntent userActionIntent) { + if (getUnarchivalStatus() != UNARCHIVAL_STATUS_UNSET) { + throw new IllegalStateException( + TextUtils.formatSimple( + "Unarchival status for ID %s has already been set or a session has " + + "been created for it already by the caller.", + unarchiveId)); + } + mUnarchivalStatus = status; + + // Execute expensive calls outside the sync block. + mPm.mHandler.post( + () -> mPm.mInstallerService.mPackageArchiver.notifyUnarchivalListener(status, + getInstallerPackageName(), params.appPackageName, requiredStorageBytes, + userActionIntent, getUnarchivalListeners(), userId)); + if (status != UNARCHIVAL_OK) { + Binder.withCleanCallingIdentity(this::abandon); + } + } + + @UnarchivalStatus + int getUnarchivalStatus() { + return this.mUnarchivalStatus; + } + + /** + * Free up storage used by this session and its children. + * Must not be called on a child session. + */ + private void destroy(String reason) { + // TODO(b/173194203): destroy() is called indirectly by + // PackageInstallerService#restoreAndApplyStagedSessionIfNeeded on an orphan child session. + // Enable this assertion when we figure out a better way to clean up orphan sessions. + // assertNotChild("destroy"); + + // TODO(b/173194203): destroyInternal() should be used by destroy() only. + // For the sake of consistency, a session should be destroyed as a whole. The caller + // should always call destroy() for cleanup without knowing it has child sessions or not. + destroyInternal(reason); + for (PackageInstallerSession child : getChildSessions()) { + child.destroyInternal(reason); + } + } + + /** + * Free up storage used by this session. + */ + private void destroyInternal(String reason) { + if (reason != null) { + Slog.i(TAG, + "Session [" + this.sessionId + "] was destroyed because of [" + reason + "]"); + } + final IncrementalFileStorages incrementalFileStorages; + synchronized (mLock) { + mSealed = true; + if (!params.isStaged) { + mDestroyed = true; + } + // Force shut down all bridges + for (RevocableFileDescriptor fd : mFds) { + fd.revoke(); + } + for (FileBridge bridge : mBridges) { + bridge.forceClose(); + } + incrementalFileStorages = mIncrementalFileStorages; + mIncrementalFileStorages = null; + } + try { + if (incrementalFileStorages != null) { + incrementalFileStorages.cleanUpAndMarkComplete(); + } + if (stageDir != null) { + final String tempPackageName = stageDir.getName(); + mInstaller.rmPackageDir(tempPackageName, stageDir.getAbsolutePath()); + } + } catch (InstallerException ignored) { + } + } + + void dump(IndentingPrintWriter pw) { + synchronized (mLock) { + dumpLocked(pw); + } + } + + @GuardedBy("mLock") + private void dumpLocked(IndentingPrintWriter pw) { + pw.println("Session " + sessionId + ":"); + pw.increaseIndent(); + + pw.printPair("userId", userId); + pw.printPair("mOriginalInstallerUid", mOriginalInstallerUid); + pw.printPair("mOriginalInstallerPackageName", mOriginalInstallerPackageName); + pw.printPair("installerPackageName", mInstallSource.mInstallerPackageName); + pw.printPair("installInitiatingPackageName", mInstallSource.mInitiatingPackageName); + pw.printPair("installOriginatingPackageName", mInstallSource.mOriginatingPackageName); + pw.printPair("mInstallerUid", mInstallerUid); + pw.printPair("createdMillis", createdMillis); + pw.printPair("updatedMillis", updatedMillis); + pw.printPair("committedMillis", committedMillis); + pw.printPair("stageDir", stageDir); + pw.printPair("stageCid", stageCid); + pw.println(); + + params.dump(pw); + + final float clientProgress; + final float progress; + synchronized (mProgressLock) { + clientProgress = mClientProgress; + progress = mProgress; + } + pw.printPair("mClientProgress", clientProgress); + pw.printPair("mProgress", progress); + pw.printPair("mCommitted", mCommitted); + pw.printPair("mPreapprovalRequested", mPreapprovalRequested); + pw.printPair("mSealed", mSealed); + pw.printPair("mPermissionsManuallyAccepted", mPermissionsManuallyAccepted); + pw.printPair("mStageDirInUse", mStageDirInUse); + pw.printPair("mDestroyed", mDestroyed); + pw.printPair("mFds", mFds.size()); + pw.printPair("mBridges", mBridges.size()); + pw.printPair("mFinalStatus", mFinalStatus); + pw.printPair("mFinalMessage", mFinalMessage); + pw.printPair("params.isMultiPackage", params.isMultiPackage); + pw.printPair("params.isStaged", params.isStaged); + pw.printPair("mParentSessionId", mParentSessionId); + pw.printPair("mChildSessionIds", getChildSessionIdsLocked()); + pw.printPair("mSessionApplied", mSessionApplied); + pw.printPair("mSessionFailed", mSessionFailed); + pw.printPair("mSessionReady", mSessionReady); + pw.printPair("mSessionErrorCode", mSessionErrorCode); + pw.printPair("mSessionErrorMessage", mSessionErrorMessage); + pw.printPair("mPreapprovalDetails", mPreapprovalDetails); + if (mPreVerifiedDomains != null) { + pw.printPair("mPreVerifiedDomains", mPreVerifiedDomains); + } + pw.println(); + + pw.decreaseIndent(); + } + + /** + * This method doesn't change internal states and is safe to call outside the lock. + */ + private static void sendOnUserActionRequired(Context context, IntentSender target, + int sessionId, Intent intent) { + final Intent fillIn = new Intent(); + fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); + fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_PENDING_USER_ACTION); + fillIn.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL, + PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction())); + fillIn.putExtra(Intent.EXTRA_INTENT, intent); + try { + final BroadcastOptions options = BroadcastOptions.makeBasic(); + options.setPendingIntentBackgroundActivityLaunchAllowed(false); + target.sendIntent(context, 0, fillIn, null /* onFinished */, + null /* handler */, null /* requiredPermission */, options.toBundle()); + } catch (IntentSender.SendIntentException ignored) { + } + } + + /** + * This method doesn't change internal states and is safe to call outside the lock. + */ + private static void sendOnPackageInstalled(Context context, IntentSender target, int sessionId, + boolean showNotification, int userId, String basePackageName, int returnCode, + boolean isPreapproval, String msg, Bundle extras) { + if (INSTALL_SUCCEEDED == returnCode && showNotification) { + boolean update = (extras != null) && extras.getBoolean(Intent.EXTRA_REPLACING); + Notification notification = PackageInstallerService.buildSuccessNotification(context, + getDeviceOwnerInstalledPackageMsg(context, update), + basePackageName, + userId); + if (notification != null) { + NotificationManager notificationManager = (NotificationManager) + context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(basePackageName, + SystemMessageProto.SystemMessage.NOTE_PACKAGE_STATE, + notification); + } + } + final Intent fillIn = new Intent(); + fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, basePackageName); + fillIn.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); + fillIn.putExtra(PackageInstaller.EXTRA_STATUS, + PackageManager.installStatusToPublicStatus(returnCode)); + fillIn.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, + PackageManager.installStatusToString(returnCode, msg)); + fillIn.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, returnCode); + fillIn.putExtra(PackageInstaller.EXTRA_PRE_APPROVAL, isPreapproval); + if (extras != null) { + final String existing = extras.getString( + PackageManager.EXTRA_FAILURE_EXISTING_PACKAGE); + if (!TextUtils.isEmpty(existing)) { + fillIn.putExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME, existing); + } + ArrayList warnings = extras.getStringArrayList(PackageInstaller.EXTRA_WARNINGS); + if (!ArrayUtils.isEmpty(warnings)) { + fillIn.putStringArrayListExtra(PackageInstaller.EXTRA_WARNINGS, warnings); + } + } + try { + final BroadcastOptions options = BroadcastOptions.makeBasic(); + options.setPendingIntentBackgroundActivityLaunchAllowed(false); + target.sendIntent(context, 0, fillIn, null /* onFinished */, + null /* handler */, null /* requiredPermission */, options.toBundle()); + } catch (IntentSender.SendIntentException ignored) { + } + } + + private static String getDeviceOwnerInstalledPackageMsg(Context context, boolean update) { + DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); + return update + ? dpm.getResources().getString(PACKAGE_UPDATED_BY_DO, + () -> context.getString(R.string.package_updated_device_owner)) + : dpm.getResources().getString(PACKAGE_INSTALLED_BY_DO, + () -> context.getString(R.string.package_installed_device_owner)); + } + + /** + * This method doesn't change internal states and is safe to call outside the lock. + */ + private static void sendPendingStreaming(Context context, IntentSender target, int sessionId, + @Nullable String cause) { + if (target == null) { + Slog.e(TAG, "Missing receiver for pending streaming status."); + return; + } + + final Intent intent = new Intent(); + intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); + intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_PENDING_STREAMING); + if (!TextUtils.isEmpty(cause)) { + intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, + "Staging Image Not Ready [" + cause + "]"); + } else { + intent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, "Staging Image Not Ready"); + } + try { + final BroadcastOptions options = BroadcastOptions.makeBasic(); + options.setPendingIntentBackgroundActivityLaunchAllowed(false); + target.sendIntent(context, 0, intent, null /* onFinished */, + null /* handler */, null /* requiredPermission */, options.toBundle()); + } catch (IntentSender.SendIntentException ignored) { + } + } + + private static void writePermissionsLocked(@NonNull TypedXmlSerializer out, + @NonNull SessionParams params) throws IOException { + var permissionStates = params.getPermissionStates(); + for (int index = 0; index < permissionStates.size(); index++) { + var permissionName = permissionStates.keyAt(index); + var state = permissionStates.valueAt(index); + String tag = state == SessionParams.PERMISSION_STATE_GRANTED ? TAG_GRANT_PERMISSION + : TAG_DENY_PERMISSION; + out.startTag(null, tag); + writeStringAttribute(out, ATTR_NAME, permissionName); + out.endTag(null, tag); + } + } + + private static void writeWhitelistedRestrictedPermissionsLocked(@NonNull TypedXmlSerializer out, + @Nullable List whitelistedRestrictedPermissions) throws IOException { + if (whitelistedRestrictedPermissions != null) { + final int permissionCount = whitelistedRestrictedPermissions.size(); + for (int i = 0; i < permissionCount; i++) { + out.startTag(null, TAG_WHITELISTED_RESTRICTED_PERMISSION); + writeStringAttribute(out, ATTR_NAME, whitelistedRestrictedPermissions.get(i)); + out.endTag(null, TAG_WHITELISTED_RESTRICTED_PERMISSION); + } + } + } + + private static void writeAutoRevokePermissionsMode(@NonNull TypedXmlSerializer out, int mode) + throws IOException { + out.startTag(null, TAG_AUTO_REVOKE_PERMISSIONS_MODE); + out.attributeInt(null, ATTR_MODE, mode); + out.endTag(null, TAG_AUTO_REVOKE_PERMISSIONS_MODE); + } + + + private static File buildAppIconFile(int sessionId, @NonNull File sessionsDir) { + return new File(sessionsDir, "app_icon." + sessionId + ".png"); + } + + /** + * Write this session to a {@link TypedXmlSerializer}. + * + * @param out Where to write the session to + * @param sessionsDir The directory containing the sessions + */ + void write(@NonNull TypedXmlSerializer out, @NonNull File sessionsDir) throws IOException { + synchronized (mLock) { + if (mDestroyed && !params.isStaged) { + return; + } + + out.startTag(null, TAG_SESSION); + + out.attributeInt(null, ATTR_SESSION_ID, sessionId); + out.attributeInt(null, ATTR_USER_ID, userId); + writeStringAttribute(out, ATTR_INSTALLER_PACKAGE_NAME, + mInstallSource.mInstallerPackageName); + out.attributeInt(null, ATTR_INSTALLER_PACKAGE_UID, mInstallSource.mInstallerPackageUid); + writeStringAttribute(out, ATTR_UPDATE_OWNER_PACKAGE_NAME, + mInstallSource.mUpdateOwnerPackageName); + writeStringAttribute(out, ATTR_INSTALLER_ATTRIBUTION_TAG, + mInstallSource.mInstallerAttributionTag); + out.attributeInt(null, ATTR_INSTALLER_UID, mInstallerUid); + writeStringAttribute(out, ATTR_INITIATING_PACKAGE_NAME, + mInstallSource.mInitiatingPackageName); + writeStringAttribute(out, ATTR_ORIGINATING_PACKAGE_NAME, + mInstallSource.mOriginatingPackageName); + out.attributeLong(null, ATTR_CREATED_MILLIS, createdMillis); + out.attributeLong(null, ATTR_UPDATED_MILLIS, updatedMillis); + out.attributeLong(null, ATTR_COMMITTED_MILLIS, committedMillis); + if (stageDir != null) { + writeStringAttribute(out, ATTR_SESSION_STAGE_DIR, + stageDir.getAbsolutePath()); + } + if (stageCid != null) { + writeStringAttribute(out, ATTR_SESSION_STAGE_CID, stageCid); + } + writeBooleanAttribute(out, ATTR_PREPARED, mPrepared); + writeBooleanAttribute(out, ATTR_COMMITTED, isCommitted()); + writeBooleanAttribute(out, ATTR_DESTROYED, mDestroyed); + writeBooleanAttribute(out, ATTR_SEALED, mSealed); + + writeBooleanAttribute(out, ATTR_MULTI_PACKAGE, params.isMultiPackage); + writeBooleanAttribute(out, ATTR_STAGED_SESSION, params.isStaged); + writeBooleanAttribute(out, ATTR_IS_READY, mSessionReady); + writeBooleanAttribute(out, ATTR_IS_FAILED, mSessionFailed); + writeBooleanAttribute(out, ATTR_IS_APPLIED, mSessionApplied); + out.attributeInt(null, ATTR_PACKAGE_SOURCE, params.packageSource); + out.attributeInt(null, ATTR_SESSION_ERROR_CODE, mSessionErrorCode); + writeStringAttribute(out, ATTR_SESSION_ERROR_MESSAGE, mSessionErrorMessage); + // TODO(patb,109941548): avoid writing to xml and instead infer / validate this after + // we've read all sessions. + out.attributeInt(null, ATTR_PARENT_SESSION_ID, mParentSessionId); + out.attributeInt(null, ATTR_MODE, params.mode); + out.attributeInt(null, ATTR_INSTALL_FLAGS, params.installFlags); + out.attributeInt(null, ATTR_INSTALL_LOCATION, params.installLocation); + out.attributeLong(null, ATTR_SIZE_BYTES, params.sizeBytes); + writeStringAttribute(out, ATTR_APP_PACKAGE_NAME, params.appPackageName); + writeStringAttribute(out, ATTR_APP_LABEL, params.appLabel); + writeUriAttribute(out, ATTR_ORIGINATING_URI, params.originatingUri); + out.attributeInt(null, ATTR_ORIGINATING_UID, params.originatingUid); + writeUriAttribute(out, ATTR_REFERRER_URI, params.referrerUri); + writeStringAttribute(out, ATTR_ABI_OVERRIDE, params.abiOverride); + writeStringAttribute(out, ATTR_VOLUME_UUID, params.volumeUuid); + out.attributeInt(null, ATTR_INSTALL_REASON, params.installReason); + writeBooleanAttribute(out, ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT, + params.applicationEnabledSettingPersistent); + + final boolean isDataLoader = params.dataLoaderParams != null; + writeBooleanAttribute(out, ATTR_IS_DATALOADER, isDataLoader); + if (isDataLoader) { + out.attributeInt(null, ATTR_DATALOADER_TYPE, params.dataLoaderParams.getType()); + writeStringAttribute(out, ATTR_DATALOADER_PACKAGE_NAME, + params.dataLoaderParams.getComponentName().getPackageName()); + writeStringAttribute(out, ATTR_DATALOADER_CLASS_NAME, + params.dataLoaderParams.getComponentName().getClassName()); + writeStringAttribute(out, ATTR_DATALOADER_ARGUMENTS, + params.dataLoaderParams.getArguments()); + } + + writePermissionsLocked(out, params); + writeWhitelistedRestrictedPermissionsLocked(out, + params.whitelistedRestrictedPermissions); + writeAutoRevokePermissionsMode(out, params.autoRevokePermissionsMode); + + // Persist app icon if changed since last written + File appIconFile = buildAppIconFile(sessionId, sessionsDir); + if (params.appIcon == null && appIconFile.exists()) { + appIconFile.delete(); + } else if (params.appIcon != null + && appIconFile.lastModified() != params.appIconLastModified) { + if (LOGD) Slog.w(TAG, "Writing changed icon " + appIconFile); + FileOutputStream os = null; + try { + os = new FileOutputStream(appIconFile); + params.appIcon.compress(Bitmap.CompressFormat.PNG, 90, os); + } catch (IOException e) { + Slog.w(TAG, "Failed to write icon " + appIconFile + ": " + e.getMessage()); + } finally { + IoUtils.closeQuietly(os); + } + + params.appIconLastModified = appIconFile.lastModified(); + } + final int[] childSessionIds = getChildSessionIdsLocked(); + for (int childSessionId : childSessionIds) { + out.startTag(null, TAG_CHILD_SESSION); + out.attributeInt(null, ATTR_SESSION_ID, childSessionId); + out.endTag(null, TAG_CHILD_SESSION); + } + + final InstallationFile[] files = getInstallationFilesLocked(); + for (InstallationFile file : files) { + out.startTag(null, TAG_SESSION_FILE); + out.attributeInt(null, ATTR_LOCATION, file.getLocation()); + writeStringAttribute(out, ATTR_NAME, file.getName()); + out.attributeLong(null, ATTR_LENGTH_BYTES, file.getLengthBytes()); + writeByteArrayAttribute(out, ATTR_METADATA, file.getMetadata()); + writeByteArrayAttribute(out, ATTR_SIGNATURE, file.getSignature()); + out.endTag(null, TAG_SESSION_FILE); + } + + for (int i = 0, isize = mChecksums.size(); i < isize; ++i) { + final String fileName = mChecksums.keyAt(i); + final PerFileChecksum perFileChecksum = mChecksums.valueAt(i); + final Checksum[] checksums = perFileChecksum.getChecksums(); + for (Checksum checksum : checksums) { + out.startTag(null, TAG_SESSION_CHECKSUM); + writeStringAttribute(out, ATTR_NAME, fileName); + out.attributeInt(null, ATTR_CHECKSUM_KIND, checksum.getType()); + writeByteArrayAttribute(out, ATTR_CHECKSUM_VALUE, checksum.getValue()); + out.endTag(null, TAG_SESSION_CHECKSUM); + } + } + for (int i = 0, isize = mChecksums.size(); i < isize; ++i) { + final String fileName = mChecksums.keyAt(i); + final PerFileChecksum perFileChecksum = mChecksums.valueAt(i); + final byte[] signature = perFileChecksum.getSignature(); + if (signature == null || signature.length == 0) { + continue; + } + out.startTag(null, TAG_SESSION_CHECKSUM_SIGNATURE); + writeStringAttribute(out, ATTR_NAME, fileName); + writeByteArrayAttribute(out, ATTR_SIGNATURE, signature); + out.endTag(null, TAG_SESSION_CHECKSUM_SIGNATURE); + } + if (mPreVerifiedDomains != null) { + for (String domain : mPreVerifiedDomains.getDomains()) { + out.startTag(null, TAG_PRE_VERIFIED_DOMAINS); + writeStringAttribute(out, ATTR_DOMAIN, domain); + out.endTag(null, TAG_PRE_VERIFIED_DOMAINS); + } + } + } + + out.endTag(null, TAG_SESSION); + } + + // Validity check to be performed when the session is restored from an external file. Only one + // of the session states should be true, or none of them. + private static boolean isStagedSessionStateValid(boolean isReady, boolean isApplied, + boolean isFailed) { + return (!isReady && !isApplied && !isFailed) + || (isReady && !isApplied && !isFailed) + || (!isReady && isApplied && !isFailed) + || (!isReady && !isApplied && isFailed); + } + + /** + * Read new session from a {@link TypedXmlPullParser xml description} and create it. + * + * @param in The source of the description + * @param callback Callback the session uses to notify about changes of it's state + * @param context Context to be used by the session + * @param pm PackageManager to use by the session + * @param installerThread Thread to be used for callbacks of this session + * @param sessionsDir The directory the sessions are stored in + * + * @param sessionProvider to get the other PackageInstallerSession instance by sessionId. + * @return The newly created session + */ + public static PackageInstallerSession readFromXml(@NonNull TypedXmlPullParser in, + @NonNull PackageInstallerService.InternalCallback callback, @NonNull Context context, + @NonNull PackageManagerService pm, Looper installerThread, + @NonNull StagingManager stagingManager, @NonNull File sessionsDir, + @NonNull PackageSessionProvider sessionProvider, + @NonNull SilentUpdatePolicy silentUpdatePolicy) + throws IOException, XmlPullParserException { + final int sessionId = in.getAttributeInt(null, ATTR_SESSION_ID); + final int userId = in.getAttributeInt(null, ATTR_USER_ID); + final String installerPackageName = readStringAttribute(in, ATTR_INSTALLER_PACKAGE_NAME); + final int installPackageUid = in.getAttributeInt(null, ATTR_INSTALLER_PACKAGE_UID, + INVALID_UID); + final String updateOwnerPackageName = readStringAttribute(in, + ATTR_UPDATE_OWNER_PACKAGE_NAME); + final String installerAttributionTag = readStringAttribute(in, + ATTR_INSTALLER_ATTRIBUTION_TAG); + final int installerUid = in.getAttributeInt(null, ATTR_INSTALLER_UID, pm.snapshotComputer() + .getPackageUid(installerPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, + userId)); + final String installInitiatingPackageName = + readStringAttribute(in, ATTR_INITIATING_PACKAGE_NAME); + final String installOriginatingPackageName = + readStringAttribute(in, ATTR_ORIGINATING_PACKAGE_NAME); + final long createdMillis = in.getAttributeLong(null, ATTR_CREATED_MILLIS); + long updatedMillis = in.getAttributeLong(null, ATTR_UPDATED_MILLIS); + final long committedMillis = in.getAttributeLong(null, ATTR_COMMITTED_MILLIS, 0L); + final String stageDirRaw = readStringAttribute(in, ATTR_SESSION_STAGE_DIR); + final File stageDir = (stageDirRaw != null) ? new File(stageDirRaw) : null; + final String stageCid = readStringAttribute(in, ATTR_SESSION_STAGE_CID); + final boolean prepared = in.getAttributeBoolean(null, ATTR_PREPARED, true); + final boolean committed = in.getAttributeBoolean(null, ATTR_COMMITTED, false); + final boolean destroyed = in.getAttributeBoolean(null, ATTR_DESTROYED, false); + final boolean sealed = in.getAttributeBoolean(null, ATTR_SEALED, false); + final int parentSessionId = in.getAttributeInt(null, ATTR_PARENT_SESSION_ID, + SessionInfo.INVALID_ID); + + final SessionParams params = new SessionParams( + SessionParams.MODE_INVALID); + params.isMultiPackage = in.getAttributeBoolean(null, ATTR_MULTI_PACKAGE, false); + params.isStaged = in.getAttributeBoolean(null, ATTR_STAGED_SESSION, false); + params.mode = in.getAttributeInt(null, ATTR_MODE); + params.installFlags = in.getAttributeInt(null, ATTR_INSTALL_FLAGS); + params.installLocation = in.getAttributeInt(null, ATTR_INSTALL_LOCATION); + params.sizeBytes = in.getAttributeLong(null, ATTR_SIZE_BYTES); + params.appPackageName = readStringAttribute(in, ATTR_APP_PACKAGE_NAME); + params.appIcon = readBitmapAttribute(in, ATTR_APP_ICON); + params.appLabel = readStringAttribute(in, ATTR_APP_LABEL); + params.originatingUri = readUriAttribute(in, ATTR_ORIGINATING_URI); + params.originatingUid = + in.getAttributeInt(null, ATTR_ORIGINATING_UID, SessionParams.UID_UNKNOWN); + params.referrerUri = readUriAttribute(in, ATTR_REFERRER_URI); + params.abiOverride = readStringAttribute(in, ATTR_ABI_OVERRIDE); + params.volumeUuid = readStringAttribute(in, ATTR_VOLUME_UUID); + params.installReason = in.getAttributeInt(null, ATTR_INSTALL_REASON); + params.packageSource = in.getAttributeInt(null, ATTR_PACKAGE_SOURCE); + params.applicationEnabledSettingPersistent = in.getAttributeBoolean(null, + ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT, false); + + if (in.getAttributeBoolean(null, ATTR_IS_DATALOADER, false)) { + params.dataLoaderParams = new DataLoaderParams( + in.getAttributeInt(null, ATTR_DATALOADER_TYPE), + new ComponentName( + readStringAttribute(in, ATTR_DATALOADER_PACKAGE_NAME), + readStringAttribute(in, ATTR_DATALOADER_CLASS_NAME)), + readStringAttribute(in, ATTR_DATALOADER_ARGUMENTS)); + } + + final File appIconFile = buildAppIconFile(sessionId, sessionsDir); + if (appIconFile.exists()) { + params.appIcon = BitmapFactory.decodeFile(appIconFile.getAbsolutePath()); + params.appIconLastModified = appIconFile.lastModified(); + } + final boolean isReady = in.getAttributeBoolean(null, ATTR_IS_READY, false); + final boolean isFailed = in.getAttributeBoolean(null, ATTR_IS_FAILED, false); + final boolean isApplied = in.getAttributeBoolean(null, ATTR_IS_APPLIED, false); + final int sessionErrorCode = in.getAttributeInt(null, ATTR_SESSION_ERROR_CODE, + PackageManager.INSTALL_UNKNOWN); + final String sessionErrorMessage = readStringAttribute(in, ATTR_SESSION_ERROR_MESSAGE); + + if (!isStagedSessionStateValid(isReady, isApplied, isFailed)) { + throw new IllegalArgumentException("Can't restore staged session with invalid state."); + } + + // Parse sub tags of this session, typically used for repeated values / arrays. + // Sub tags can come in any order, therefore we need to keep track of what we find while + // parsing and only set the right values at the end. + + // Store the current depth. We should stop parsing when we reach an end tag at the same + // depth. + List legacyGrantedRuntimePermissions = new ArrayList<>(); + ArraySet grantPermissions = new ArraySet<>(); + ArraySet denyPermissions = new ArraySet<>(); + List whitelistedRestrictedPermissions = new ArrayList<>(); + int autoRevokePermissionsMode = MODE_DEFAULT; + IntArray childSessionIds = new IntArray(); + List files = new ArrayList<>(); + ArrayMap> checksums = new ArrayMap<>(); + ArrayMap signatures = new ArrayMap<>(); + ArraySet preVerifiedDomainSet = new ArraySet<>(); + int outerDepth = in.getDepth(); + int type; + while ((type = in.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || in.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + switch (in.getName()) { + case TAG_GRANTED_RUNTIME_PERMISSION: + legacyGrantedRuntimePermissions.add(readStringAttribute(in, ATTR_NAME)); + break; + case TAG_GRANT_PERMISSION: + grantPermissions.add(readStringAttribute(in, ATTR_NAME)); + break; + case TAG_DENY_PERMISSION: + denyPermissions.add(readStringAttribute(in, ATTR_NAME)); + break; + case TAG_WHITELISTED_RESTRICTED_PERMISSION: + whitelistedRestrictedPermissions.add(readStringAttribute(in, ATTR_NAME)); + break; + case TAG_AUTO_REVOKE_PERMISSIONS_MODE: + autoRevokePermissionsMode = in.getAttributeInt(null, ATTR_MODE); + break; + case TAG_CHILD_SESSION: + childSessionIds.add(in.getAttributeInt(null, ATTR_SESSION_ID, + SessionInfo.INVALID_ID)); + break; + case TAG_SESSION_FILE: + files.add(new InstallationFile( + in.getAttributeInt(null, ATTR_LOCATION, 0), + readStringAttribute(in, ATTR_NAME), + in.getAttributeLong(null, ATTR_LENGTH_BYTES, -1), + readByteArrayAttribute(in, ATTR_METADATA), + readByteArrayAttribute(in, ATTR_SIGNATURE))); + break; + case TAG_SESSION_CHECKSUM: + final String fileName = readStringAttribute(in, ATTR_NAME); + final Checksum checksum = new Checksum( + in.getAttributeInt(null, ATTR_CHECKSUM_KIND, 0), + readByteArrayAttribute(in, ATTR_CHECKSUM_VALUE)); + + List fileChecksums = checksums.get(fileName); + if (fileChecksums == null) { + fileChecksums = new ArrayList<>(); + checksums.put(fileName, fileChecksums); + } + fileChecksums.add(checksum); + break; + case TAG_SESSION_CHECKSUM_SIGNATURE: + final String fileName1 = readStringAttribute(in, ATTR_NAME); + final byte[] signature = readByteArrayAttribute(in, ATTR_SIGNATURE); + signatures.put(fileName1, signature); + break; + case TAG_PRE_VERIFIED_DOMAINS: + preVerifiedDomainSet.add(readStringAttribute(in, ATTR_DOMAIN)); + break; + } + } + + if (legacyGrantedRuntimePermissions.size() > 0) { + params.setPermissionStates(legacyGrantedRuntimePermissions, Collections.emptyList()); + } else { + params.setPermissionStates(grantPermissions, denyPermissions); + } + + if (whitelistedRestrictedPermissions.size() > 0) { + params.whitelistedRestrictedPermissions = whitelistedRestrictedPermissions; + } + + params.autoRevokePermissionsMode = autoRevokePermissionsMode; + + int[] childSessionIdsArray; + if (childSessionIds.size() > 0) { + childSessionIdsArray = new int[childSessionIds.size()]; + for (int i = 0, size = childSessionIds.size(); i < size; ++i) { + childSessionIdsArray[i] = childSessionIds.get(i); + } + } else { + childSessionIdsArray = EMPTY_CHILD_SESSION_ARRAY; + } + + InstallationFile[] fileArray = null; + if (!files.isEmpty()) { + fileArray = files.toArray(EMPTY_INSTALLATION_FILE_ARRAY); + } + + ArrayMap checksumsMap = null; + if (!checksums.isEmpty()) { + checksumsMap = new ArrayMap<>(checksums.size()); + for (int i = 0, isize = checksums.size(); i < isize; ++i) { + final String fileName = checksums.keyAt(i); + final List perFileChecksum = checksums.valueAt(i); + final byte[] perFileSignature = signatures.get(fileName); + checksumsMap.put(fileName, new PerFileChecksum( + perFileChecksum.toArray(new Checksum[perFileChecksum.size()]), + perFileSignature)); + } + } + + DomainSet preVerifiedDomains = + preVerifiedDomainSet.isEmpty() ? null : new DomainSet(preVerifiedDomainSet); + + InstallSource installSource = InstallSource.create(installInitiatingPackageName, + installOriginatingPackageName, installerPackageName, installPackageUid, + updateOwnerPackageName, installerAttributionTag, params.packageSource); + return new PackageInstallerSession(callback, context, pm, sessionProvider, + silentUpdatePolicy, installerThread, stagingManager, sessionId, userId, + installerUid, installSource, params, createdMillis, committedMillis, stageDir, + stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed, + childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied, + sessionErrorCode, sessionErrorMessage, preVerifiedDomains); + } +} diff --git a/aosp/packages/apps/Settings/res/xml/top_level_settings.xml b/aosp/packages/apps/Settings/res/xml/top_level_settings.xml index 7815b5f731f9a85eb088d556d57a0ee31da3bfd2..8c4e5fedc86e4948dc280c16533700ca7e68bbaa 100644 --- a/aosp/packages/apps/Settings/res/xml/top_level_settings.xml +++ b/aosp/packages/apps/Settings/res/xml/top_level_settings.xml @@ -40,7 +40,7 @@ settings:highlightableMenuKey="@string/menu_key_communal" settings:controller="com.android.settings.communal.CommunalPreferenceController"/> - + settings:controller="com.android.settings.connecteddevice.TopLevelConnectedDevicesPreferenceController"/>--> mCategoryToPrefCategoryMap = + new ArrayMap<>(); + @VisibleForTesting + final Map mServicePreferenceToPreferenceCategoryMap = + new ArrayMap<>(); + private final Map mPreBundledServiceComponentToCategoryMap = + new ArrayMap<>(); + + private boolean mNeedPreferencesUpdate = false; + private boolean mIsForeground = true; + + public AccessibilitySettings() { + // Observe changes to anything that the shortcut can toggle, so we can reflect updates + final Collection features = + AccessibilityShortcutController.getFrameworkShortcutFeaturesMap().values(); + final List shortcutFeatureKeys = new ArrayList<>(features.size()); + for (AccessibilityShortcutController.FrameworkFeatureInfo feature : features) { + final String key = feature.getSettingKey(); + if (key != null) { + shortcutFeatureKeys.add(key); + } + } + + // Observe changes from accessibility selection menu + shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); + shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE); + shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_STICKY_KEYS); + shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SLOW_KEYS); + shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS); + mSettingsContentObserver = new AccessibilitySettingsContentObserver(mHandler); + mSettingsContentObserver.registerKeysToObserverCallback(shortcutFeatureKeys, + key -> onContentChanged()); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ACCESSIBILITY; + } + + @Override + public int getHelpResource() { + return R.string.help_uri_accessibility; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + /* TODO蓝牙服务未开发,当前关闭该服务,注释蓝牙相关流程(支持助听器)防止无障碍服务崩溃 + use(AccessibilityHearingAidPreferenceController.class) + .setFragmentManager(getFragmentManager()); + */ + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + initializeAllPreferences(); + updateAllPreferences(); + registerContentMonitors(); + registerInputDeviceListener(); + } + + @Override + public void onResume() { + super.onResume(); + updateAllPreferences(); + } + + @Override + public void onStart() { + if (mNeedPreferencesUpdate) { + updateAllPreferences(); + mNeedPreferencesUpdate = false; + } + mIsForeground = true; + super.onStart(); + } + + @Override + public void onStop() { + mIsForeground = false; + super.onStop(); + } + + @Override + public void onDestroy() { + unregisterContentMonitors(); + unRegisterInputDeviceListener(); + super.onDestroy(); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.accessibility_settings; + } + + @Override + protected String getLogTag() { + return TAG; + } + + /** + * Returns the summary for the current state of this accessibilityService. + * + * @param context A valid context + * @param info The accessibilityService's info + * @param serviceEnabled Whether the accessibility service is enabled. + * @return The service summary + */ + public static CharSequence getServiceSummary(Context context, AccessibilityServiceInfo info, + boolean serviceEnabled) { + if (serviceEnabled && info.crashed) { + return context.getText(R.string.accessibility_summary_state_stopped); + } + + final CharSequence serviceState; + final int fragmentType = AccessibilityUtil.getAccessibilityServiceFragmentType(info); + if (fragmentType == AccessibilityServiceFragmentType.INVISIBLE_TOGGLE) { + final ComponentName componentName = new ComponentName( + info.getResolveInfo().serviceInfo.packageName, + info.getResolveInfo().serviceInfo.name); + final boolean shortcutEnabled = AccessibilityUtil.getUserShortcutTypesFromSettings( + context, componentName) != AccessibilityUtil.UserShortcutType.EMPTY; + serviceState = shortcutEnabled + ? context.getText(R.string.accessibility_summary_shortcut_enabled) + : context.getText(R.string.generic_accessibility_feature_shortcut_off); + } else { + serviceState = serviceEnabled + ? context.getText(R.string.generic_accessibility_service_on) + : context.getText(R.string.generic_accessibility_service_off); + } + + final CharSequence serviceSummary = info.loadSummary(context.getPackageManager()); + final String stateSummaryCombo = context.getString( + R.string.preference_summary_default_combination, + serviceState, serviceSummary); + + return TextUtils.isEmpty(serviceSummary) ? serviceState : stateSummaryCombo; + } + + /** + * Returns the description for the current state of this accessibilityService. + * + * @param context A valid context + * @param info The accessibilityService's info + * @param serviceEnabled Whether the accessibility service is enabled. + * @return The service description + */ + public static CharSequence getServiceDescription(Context context, AccessibilityServiceInfo info, + boolean serviceEnabled) { + if (serviceEnabled && info.crashed) { + return context.getText(R.string.accessibility_description_state_stopped); + } + + return info.loadDescription(context.getPackageManager()); + } + + @VisibleForTesting + void onContentChanged() { + // If the fragment is visible then update preferences immediately, else set the flag then + // wait for the fragment to show up to update preferences. + if (mIsForeground) { + updateAllPreferences(); + } else { + mNeedPreferencesUpdate = true; + } + } + + private void initializeAllPreferences() { + for (int i = 0; i < CATEGORIES.length; i++) { + PreferenceCategory prefCategory = findPreference(CATEGORIES[i]); + mCategoryToPrefCategoryMap.put(CATEGORIES[i], prefCategory); + } + } + + @VisibleForTesting + void updateAllPreferences() { + updateServicePreferences(); + updatePreferencesState(); + updateSystemPreferences(); + } + + private void registerContentMonitors() { + final Context context = getActivity(); + + mSettingsPackageMonitor.register(context, context.getMainLooper(), /* externalStorage= */ + false); + mSettingsContentObserver.register(getContentResolver()); + } + + private void registerInputDeviceListener() { + InputManager mIm = getSystemService(InputManager.class); + if (mIm == null) { + return; + } + mIm.registerInputDeviceListener(this, null); + } + + private void unRegisterInputDeviceListener() { + InputManager mIm = getSystemService(InputManager.class); + if (mIm == null) { + return; + } + mIm.unregisterInputDeviceListener(this); + } + + private void unregisterContentMonitors() { + mSettingsPackageMonitor.unregister(); + mSettingsContentObserver.unregister(getContentResolver()); + } + + protected void updateServicePreferences() { + // Since services category is auto generated we have to do a pass + // to generate it since services can come and go and then based on + // the global accessibility state to decided whether it is enabled. + final ArrayList servicePreferences = + new ArrayList<>(mServicePreferenceToPreferenceCategoryMap.keySet()); + for (int i = 0; i < servicePreferences.size(); i++) { + Preference service = servicePreferences.get(i); + PreferenceCategory category = mServicePreferenceToPreferenceCategoryMap.get(service); + category.removePreference(service); + } + + initializePreBundledServicesMapFromArray(CATEGORY_SCREEN_READER, + R.array.config_preinstalled_screen_reader_services); + initializePreBundledServicesMapFromArray(CATEGORY_CAPTIONS, + R.array.config_preinstalled_captions_services); + initializePreBundledServicesMapFromArray(CATEGORY_AUDIO, + R.array.config_preinstalled_audio_services); + initializePreBundledServicesMapFromArray(CATEGORY_DISPLAY, + R.array.config_preinstalled_display_services); + initializePreBundledServicesMapFromArray(CATEGORY_SPEECH, + R.array.config_preinstalled_speech_services); + initializePreBundledServicesMapFromArray(CATEGORY_INTERACTION_CONTROL, + R.array.config_preinstalled_interaction_control_services); + + // ACCESSIBILITY_MENU_IN_SYSTEM is a default pre-bundled interaction control service. + // If the device opts out of including this service then this is a no-op. + mPreBundledServiceComponentToCategoryMap.put( + AccessibilityUtils.ACCESSIBILITY_MENU_IN_SYSTEM, + mCategoryToPrefCategoryMap.get(CATEGORY_INTERACTION_CONTROL)); + + final List preferenceList = getInstalledAccessibilityList( + getPrefContext()); + + final PreferenceCategory downloadedServicesCategory = + mCategoryToPrefCategoryMap.get(CATEGORY_DOWNLOADED_SERVICES); + + for (int i = 0, count = preferenceList.size(); i < count; ++i) { + final RestrictedPreference preference = preferenceList.get(i); + final ComponentName componentName = preference.getExtras().getParcelable( + EXTRA_COMPONENT_NAME); + PreferenceCategory prefCategory = downloadedServicesCategory; + // Set the appropriate category if the service comes pre-installed. + if (mPreBundledServiceComponentToCategoryMap.containsKey(componentName)) { + prefCategory = mPreBundledServiceComponentToCategoryMap.get(componentName); + } + prefCategory.addPreference(preference); + mServicePreferenceToPreferenceCategoryMap.put(preference, prefCategory); + } + + // Update the order of all the category according to the order defined in xml file. + updateCategoryOrderFromArray(CATEGORY_SCREEN_READER, + R.array.config_order_screen_reader_services); + updateCategoryOrderFromArray(CATEGORY_CAPTIONS, + R.array.config_order_captions_services); + updateCategoryOrderFromArray(CATEGORY_AUDIO, + R.array.config_order_audio_services); + updateCategoryOrderFromArray(CATEGORY_INTERACTION_CONTROL, + R.array.config_order_interaction_control_services); + updateCategoryOrderFromArray(CATEGORY_DISPLAY, + R.array.config_order_display_services); + updateCategoryOrderFromArray(CATEGORY_SPEECH, + R.array.config_order_speech_services); + + // Need to check each time when updateServicePreferences() called. + if (downloadedServicesCategory.getPreferenceCount() == 0) { + getPreferenceScreen().removePreference(downloadedServicesCategory); + } else { + getPreferenceScreen().addPreference(downloadedServicesCategory); + } + + // Hide category if it is empty. + updatePreferenceCategoryVisibility(CATEGORY_SCREEN_READER); + updatePreferenceCategoryVisibility(CATEGORY_SPEECH); + updatePreferenceCategoryVisibility(CATEGORY_KEYBOARD_OPTIONS); + } + + private List getInstalledAccessibilityList(Context context) { + final AccessibilityManager a11yManager = AccessibilityManager.getInstance(context); + final RestrictedPreferenceHelper preferenceHelper = new RestrictedPreferenceHelper(context); + + final List installedShortcutList = + a11yManager.getInstalledAccessibilityShortcutListAsUser(context, + UserHandle.myUserId()); + + // Remove duplicate item here, new a ArrayList to copy unmodifiable list result + // (getInstalledAccessibilityServiceList). + final List installedServiceList = new ArrayList<>( + a11yManager.getInstalledAccessibilityServiceList()); + installedServiceList.removeIf( + target -> containsTargetNameInList(installedShortcutList, target)); + + final List activityList = + preferenceHelper.createAccessibilityActivityPreferenceList(installedShortcutList); + + final List serviceList = + preferenceHelper.createAccessibilityServicePreferenceList(installedServiceList); + + final List preferenceList = new ArrayList<>(); + preferenceList.addAll(activityList); + preferenceList.addAll(serviceList); + + return preferenceList; + } + + private boolean containsTargetNameInList(List shortcutInfos, + AccessibilityServiceInfo targetServiceInfo) { + final ServiceInfo serviceInfo = targetServiceInfo.getResolveInfo().serviceInfo; + final String servicePackageName = serviceInfo.packageName; + final CharSequence serviceLabel = serviceInfo.loadLabel(getPackageManager()); + + for (int i = 0, count = shortcutInfos.size(); i < count; ++i) { + final ActivityInfo activityInfo = shortcutInfos.get(i).getActivityInfo(); + final String activityPackageName = activityInfo.packageName; + final CharSequence activityLabel = activityInfo.loadLabel(getPackageManager()); + if (servicePackageName.equals(activityPackageName) + && serviceLabel.equals(activityLabel)) { + return true; + } + } + return false; + } + + private void initializePreBundledServicesMapFromArray(String categoryKey, int key) { + String[] services = getResources().getStringArray(key); + PreferenceCategory category = mCategoryToPrefCategoryMap.get(categoryKey); + for (int i = 0; i < services.length; i++) { + ComponentName component = ComponentName.unflattenFromString(services[i]); + mPreBundledServiceComponentToCategoryMap.put(component, category); + } + } + + /** + * Update the order of preferences in the category by matching their preference + * key with the string array of preference order which is defined in the xml. + * + * @param categoryKey The key of the category need to update the order + * @param key The key of the string array which defines the order of category + */ + private void updateCategoryOrderFromArray(String categoryKey, int key) { + String[] services = getResources().getStringArray(key); + PreferenceCategory category = mCategoryToPrefCategoryMap.get(categoryKey); + int preferenceCount = category.getPreferenceCount(); + int serviceLength = services.length; + for (int preferenceIndex = 0; preferenceIndex < preferenceCount; preferenceIndex++) { + for (int serviceIndex = 0; serviceIndex < serviceLength; serviceIndex++) { + if (category.getPreference(preferenceIndex).getKey() + .equals(services[serviceIndex])) { + category.getPreference(preferenceIndex).setOrder(serviceIndex); + break; + } + } + } + } + + /** + * Updates the visibility of a category according to its child preference count. + * + * @param categoryKey The key of the category which needs to check + */ + private void updatePreferenceCategoryVisibility(String categoryKey) { + final PreferenceCategory category = mCategoryToPrefCategoryMap.get(categoryKey); + category.setVisible(category.getPreferenceCount() != 0); + } + + /** + * Updates preferences related to system configurations. + */ + protected void updateSystemPreferences() { + updateKeyboardPreferencesVisibility(); + } + + private void updatePreferencesState() { + final List controllers = new ArrayList<>(); + getPreferenceControllers().forEach(controllers::addAll); + controllers.forEach(controller -> controller.updateState( + findPreference(controller.getPreferenceKey()))); + } + + private void updateKeyboardPreferencesVisibility() { + if (!mCategoryToPrefCategoryMap.containsKey(CATEGORY_KEYBOARD_OPTIONS)) { + return; + } + boolean isVisible = isAnyHardKeyboardsExist() + && isAnyKeyboardPreferenceAvailable(); + mCategoryToPrefCategoryMap.get(CATEGORY_KEYBOARD_OPTIONS).setVisible( + isVisible); + if (isVisible) { + //set summary here. + findPreference(KeyboardBounceKeyPreferenceController.PREF_KEY).setSummary( + getContext().getString(R.string.bounce_keys_summary, + PhysicalKeyboardFragment.BOUNCE_KEYS_THRESHOLD)); + findPreference(KeyboardSlowKeyPreferenceController.PREF_KEY).setSummary( + getContext().getString(R.string.slow_keys_summary, + PhysicalKeyboardFragment.SLOW_KEYS_THRESHOLD)); + } + } + + private boolean isAnyHardKeyboardsExist() { + for (int deviceId : InputDevice.getDeviceIds()) { + final InputDevice device = InputDevice.getDevice(deviceId); + if (device != null && !device.isVirtual() && device.isFullKeyboard()) { + return true; + } + } + return false; + } + + private boolean isAnyKeyboardPreferenceAvailable() { + for (List controllerList : getPreferenceControllers()) { + for (AbstractPreferenceController controller : controllerList) { + if (controller.getPreferenceKey().equals( + KeyboardBounceKeyPreferenceController.PREF_KEY) + || controller.getPreferenceKey().equals( + KeyboardSlowKeyPreferenceController.PREF_KEY) + || controller.getPreferenceKey().equals( + KeyboardStickyKeyPreferenceController.PREF_KEY)) { + if (controller.isAvailable()) { + return true; + } + } + } + } + return false; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.accessibility_settings) { + @Override + public List getRawDataToIndex(Context context, + boolean enabled) { + return FeatureFactory.getFeatureFactory() + .getAccessibilitySearchFeatureProvider().getSearchIndexableRawData( + context); + } + }; + + @Override + public void onInputDeviceAdded(int deviceId) {} + + @Override + public void onInputDeviceRemoved(int deviceId) {} + + @Override + public void onInputDeviceChanged(int deviceId) { + mHandler.postDelayed(mUpdateRunnable, DELAY_UPDATE_SERVICES_MILLIS); + } +} diff --git a/aosp/packages/apps/Settings/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java b/aosp/packages/apps/Settings/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java new file mode 100644 index 0000000000000000000000000000000000000000..b488bbad5da5cf3c442aca0db308a347a49b1e8c --- /dev/null +++ b/aosp/packages/apps/Settings/src/com/android/settings/development/BluetoothMaxConnectedAudioDevicesPreferenceController.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 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 com.android.settings.development; + +import android.bluetooth.BluetoothManager; +import android.content.Context; +import android.os.SystemProperties; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +public class BluetoothMaxConnectedAudioDevicesPreferenceController extends + DeveloperOptionsPreferenceController implements Preference.OnPreferenceChangeListener, + PreferenceControllerMixin { + + private static final String MAX_CONNECTED_AUDIO_DEVICES_PREFERENCE_KEY = + "bluetooth_max_connected_audio_devices"; + + @VisibleForTesting + static final String MAX_CONNECTED_AUDIO_DEVICES_PROPERTY = + "persist.bluetooth.maxconnectedaudiodevices"; + + private int mDefaultMaxConnectedAudioDevices = 0; + + public BluetoothMaxConnectedAudioDevicesPreferenceController(Context context) { + super(context); + + final BluetoothManager bluetoothManager = context.getSystemService(BluetoothManager.class); + /* TODO蓝牙服务未开发,当前关闭该服务,注释蓝牙相关流程(USB调试)防止开发者选项崩溃 + mDefaultMaxConnectedAudioDevices = + bluetoothManager.getAdapter().getMaxConnectedAudioDevices(); + */ + } + + @Override + public String getPreferenceKey() { + return MAX_CONNECTED_AUDIO_DEVICES_PREFERENCE_KEY; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + final ListPreference listPreference = (ListPreference) mPreference; + final CharSequence[] entries = listPreference.getEntries(); + entries[0] = String.format(entries[0].toString(), mDefaultMaxConnectedAudioDevices); + listPreference.setEntries(entries); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String newValueString = newValue.toString(); + final ListPreference listPreference = (ListPreference) preference; + if (listPreference.findIndexOfValue(newValueString) <= 0) { + // Reset property value when default is chosen or when value is illegal + newValueString = ""; + } + SystemProperties.set(MAX_CONNECTED_AUDIO_DEVICES_PROPERTY, newValueString); + updateState(preference); + return true; + } + + @Override + public void updateState(Preference preference) { + final ListPreference listPreference = (ListPreference) preference; + final CharSequence[] entries = listPreference.getEntries(); + final String currentValue = SystemProperties.get(MAX_CONNECTED_AUDIO_DEVICES_PROPERTY); + int index = 0; + if (!currentValue.isEmpty()) { + index = listPreference.findIndexOfValue(currentValue); + if (index < 0) { + // Reset property value when value is illegal + SystemProperties.set(MAX_CONNECTED_AUDIO_DEVICES_PROPERTY, ""); + index = 0; + } + } + listPreference.setValueIndex(index); + listPreference.setSummary(entries[index]); + } + + @Override + protected void onDeveloperOptionsSwitchEnabled() { + super.onDeveloperOptionsSwitchEnabled(); + updateState(mPreference); + } + + @Override + protected void onDeveloperOptionsSwitchDisabled() { + super.onDeveloperOptionsSwitchDisabled(); + SystemProperties.set(MAX_CONNECTED_AUDIO_DEVICES_PROPERTY, ""); + updateState(mPreference); + } +} + diff --git a/aosp/packages/apps/Settings/src/com/android/settings/network/TetheredRepository.kt b/aosp/packages/apps/Settings/src/com/android/settings/network/TetheredRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..eb2660f9473447f7f7f20989d62b24309e50a886 --- /dev/null +++ b/aosp/packages/apps/Settings/src/com/android/settings/network/TetheredRepository.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 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 com.android.settings.network + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothPan +import android.bluetooth.BluetoothProfile +import android.content.Context +import android.content.IntentFilter +import android.net.TetheringInterface +import android.net.TetheringManager +import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch + +class TetheredRepository(private val context: Context) { + private val tetheringManager = context.getSystemService(TetheringManager::class.java)!! + + private val adapter = context.getSystemService(BluetoothManager::class.java)!!.adapter + + fun tetheredTypesFlow(): Flow> = + combine( + tetheredInterfacesFlow(), + isBluetoothTetheringOnFlow(), + ) { tetheringInterfaces, isBluetoothTetheringOn -> + val mutableSet = tetheringInterfaces.map { it.type }.toMutableSet() + if (isBluetoothTetheringOn) mutableSet += TetheringManager.TETHERING_BLUETOOTH + mutableSet + }.conflate().flowOn(Dispatchers.Default) + + private fun tetheredInterfacesFlow(): Flow> = callbackFlow { + val callback = object : TetheringManager.TetheringEventCallback { + override fun onTetheredInterfacesChanged(interfaces: Set) { + trySend(interfaces) + } + } + + tetheringManager.registerTetheringEventCallback(Dispatchers.Default.asExecutor(), callback) + + awaitClose { tetheringManager.unregisterTetheringEventCallback(callback) } + }.conflate().flowOn(Dispatchers.Default) + + @OptIn(ExperimentalCoroutinesApi::class) + private fun isBluetoothTetheringOnFlow(): Flow = + merge( + flowOf(null), // kick an initial value + context.broadcastReceiverFlow(IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)), + ).flatMapLatest { + if (adapter != null && adapter.getState() == BluetoothAdapter.STATE_ON) { // TODO蓝牙服务未开发,当前关闭该服务,增加空值判断防止设置中网络和互联网设置崩溃 + isBluetoothPanTetheringOnFlow() + } else { + flowOf(false) + } + }.conflate().flowOn(Dispatchers.Default) + + private fun isBluetoothPanTetheringOnFlow() = callbackFlow { + var connectedProxy: BluetoothProfile? = null + + val listener = object : BluetoothProfile.ServiceListener { + override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { + connectedProxy = proxy + launch(Dispatchers.Default) { + trySend((proxy as BluetoothPan).isTetheringOn) + } + } + + override fun onServiceDisconnected(profile: Int) {} + } + + adapter.getProfileProxy(context, listener, BluetoothProfile.PAN) + + awaitClose { + connectedProxy?.let { adapter.closeProfileProxy(BluetoothProfile.PAN, it) } + } + }.conflate().flowOn(Dispatchers.Default) +} diff --git a/aosp/system/memory/libmeminfo/sysmeminfo.cpp b/aosp/system/memory/libmeminfo/sysmeminfo.cpp new file mode 100644 index 0000000000000000000000000000000000000000..438e9c1f86d3ec00b44bd291b89f0f79fc2510e3 --- /dev/null +++ b/aosp/system/memory/libmeminfo/sysmeminfo.cpp @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2018 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#if defined(__ANDROID__) && !defined(__ANDROID_APEX__) && !defined(__ANDROID_VNDK__) +#include "bpf/BpfMap.h" +#endif +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "meminfo_private.h" + +namespace android { +namespace meminfo { + +bool SysMemInfo::ReadMemInfo(const char* path) { + return ReadMemInfo(path, SysMemInfo::kDefaultSysMemInfoTags.size(), + &*SysMemInfo::kDefaultSysMemInfoTags.begin(), + [&](std::string_view tag, uint64_t val) { + // Safe to store the string_view in the map + // because the tags from + // kDefaultSysMemInfoTags are all + // statically-allocated. + mem_in_kb_[tag] = val; + }); +} + +bool SysMemInfo::ReadMemInfo(std::vector* out, const char* path) { + out->clear(); + out->resize(SysMemInfo::kDefaultSysMemInfoTags.size()); + return ReadMemInfo(SysMemInfo::kDefaultSysMemInfoTags.size(), + &*SysMemInfo::kDefaultSysMemInfoTags.begin(), out->data(), path); +} + +bool SysMemInfo::ReadMemInfo(size_t ntags, const std::string_view* tags, uint64_t* out, + const char* path) { + return ReadMemInfo(path, ntags, tags, [&]([[maybe_unused]] std::string_view tag, uint64_t val) { + auto it = std::find(tags, tags + ntags, tag); + if (it == tags + ntags) { + LOG(ERROR) << "Tried to store invalid tag: " << tag; + return; + } + auto index = std::distance(tags, it); + // store the values in the same order as the tags + out[index] = val; + }); +} + +uint64_t SysMemInfo::ReadVmallocInfo() { + return ::android::meminfo::ReadVmallocInfo(); +} + +bool SysMemInfo::ReadMemInfo(const char* path, size_t ntags, const std::string_view* tags, + std::function store_val) { + char buffer[4096]; + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + PLOG(ERROR) << "Failed to open file :" << path; + return false; + } + + const int len = read(fd, buffer, sizeof(buffer) - 1); + close(fd); + if (len < 0) { + return false; + } + + buffer[len] = '\0'; + char* p = buffer; + uint32_t found = 0; + uint32_t lineno = 0; + bool zram_tag_found = false; + while (*p && found < ntags) { + for (size_t tagno = 0; tagno < ntags; ++tagno) { + const std::string_view& tag = tags[tagno]; + // Special case for "Zram:" tag that android_os_Debug and friends look + // up along with the rest of the numbers from /proc/meminfo + if (!zram_tag_found && tag == "Zram:") { + store_val(tag, mem_zram_kb()); + zram_tag_found = true; + found++; + continue; + } + + if (strncmp(p, tag.data(), tag.size()) == 0) { + p += tag.size(); + while (*p == ' ') p++; + char* endptr = nullptr; + uint64_t val = strtoull(p, &endptr, 10); + if (p == endptr) { + PLOG(ERROR) << "Failed to parse line:" << lineno + 1 << " in file: " << path; + return false; + } + store_val(tag, val); + p = endptr; + found++; + break; + } + } + + while (*p && *p != '\n') { + p++; + } + if (*p) p++; + lineno++; + } + + return true; +} + +uint64_t SysMemInfo::mem_zram_kb(const char* zram_dev_cstr) const { + uint64_t mem_zram_total = 0; + if (zram_dev_cstr) { + if (!MemZramDevice(zram_dev_cstr, &mem_zram_total)) { + return 0; + } + return mem_zram_total / 1024; + } + + constexpr uint32_t kMaxZramDevices = 256; + for (uint32_t i = 0; i < kMaxZramDevices; i++) { + std::string zram_dev_abspath = ::android::base::StringPrintf("/sys/block/zram%u/", i); + if (access(zram_dev_abspath.c_str(), F_OK)) { + // We assume zram devices appear in range 0-255 and appear always in sequence + // under /sys/block. So, stop looking for them once we find one is missing. + break; + } + + uint64_t mem_zram_dev; + if (!MemZramDevice(zram_dev_abspath.c_str(), &mem_zram_dev)) { + return 0; + } + + mem_zram_total += mem_zram_dev; + } + + return mem_zram_total / 1024; +} + +bool SysMemInfo::MemZramDevice(const char* zram_dev, uint64_t* mem_zram_dev) const { + std::string mmstat = ::android::base::StringPrintf("%s/%s", zram_dev, "mm_stat"); + auto mmstat_fp = std::unique_ptr{fopen(mmstat.c_str(), "re"), fclose}; + if (mmstat_fp != nullptr) { + // only if we do have mmstat, use it. Otherwise, fall through to trying out the old + // 'mem_used_total' + if (fscanf(mmstat_fp.get(), "%*" SCNu64 " %*" SCNu64 " %" SCNu64, mem_zram_dev) != 1) { + PLOG(ERROR) << "Malformed mm_stat file in: " << zram_dev; + return false; + } + return true; + } + + std::string content; + if (::android::base::ReadFileToString( + ::android::base::StringPrintf("%s/mem_used_total", zram_dev), &content)) { + *mem_zram_dev = strtoull(content.c_str(), NULL, 10); + if (*mem_zram_dev == ULLONG_MAX) { + PLOG(ERROR) << "Malformed mem_used_total file for zram dev: " << zram_dev + << " content: " << content; + return false; + } + + return true; + } + + LOG(ERROR) << "Can't find memory status under: " << zram_dev; + return false; +} + +uint64_t SysMemInfo::mem_compacted_kb(const char* zram_dev_cstr) { + uint64_t mem_compacted_total = 0; + if (zram_dev_cstr) { + // Fast-path, single device + if (!GetTotalMemCompacted(zram_dev_cstr, &mem_compacted_total)) { + return 0; + } + return mem_compacted_total / 1024; + } + + // Slow path - multiple devices + constexpr uint32_t kMaxZramDevices = 256; + for (uint32_t i = 0; i < kMaxZramDevices; i++) { + std::string zram_dev_abspath = ::android::base::StringPrintf("/sys/block/zram%u/", i); + if (access(zram_dev_abspath.c_str(), F_OK)) { + // We assume zram devices appear in range 0-255 and appear always in sequence + // under /sys/block. So, stop looking for them once we find one is missing. + break; + } + + uint64_t mem_compacted; + if (!GetTotalMemCompacted(zram_dev_abspath.c_str(), &mem_compacted)) { + return 0; + } + + mem_compacted_total += mem_compacted; + } + + return mem_compacted_total / 1024; // transform to KBs +} + +// Returns the total memory compacted in bytes which corresponds to the following formula +// compacted memory = uncompressed memory size - compressed memory size +bool SysMemInfo::GetTotalMemCompacted(const char* zram_dev, uint64_t* out_mem_compacted) { + std::string mmstat = ::android::base::StringPrintf("%s/%s", zram_dev, "mm_stat"); + auto mmstat_fp = std::unique_ptr{fopen(mmstat.c_str(), "re"), fclose}; + if (mmstat_fp != nullptr) { + uint64_t uncompressed_size_bytes; + uint64_t compressed_size_bytes; + + if (fscanf(mmstat_fp.get(), "%" SCNu64 "%" SCNu64, &uncompressed_size_bytes, + &compressed_size_bytes) != 2) { + PLOG(ERROR) << "Malformed mm_stat file in: " << zram_dev; + *out_mem_compacted = 0; + return false; + } + + *out_mem_compacted = uncompressed_size_bytes - compressed_size_bytes; + return true; + } + + *out_mem_compacted = 0; + return false; +} + +// Public methods +uint64_t ReadVmallocInfo(const char* path) { + uint64_t vmalloc_total = 0; + auto fp = std::unique_ptr{fopen(path, "re"), fclose}; + if (fp == nullptr) { + return vmalloc_total; + } + + char* line = nullptr; + size_t line_alloc = 0; + while (getline(&line, &line_alloc, fp.get()) > 0) { + // We are looking for lines like + // + // 0x0000000000000000-0x0000000000000000 12288 drm_property_create_blob+0x44/0xec pages=2 vmalloc + // 0x0000000000000000-0x0000000000000000 8192 wlan_logging_sock_init_svc+0xf8/0x4f0 [wlan] pages=1 vmalloc + // + // Notice that if the caller is coming from a module, the kernel prints and extra + // "[module_name]" after the address and the symbol of the call site. This means we can't + // use the old sscanf() method of getting the # of pages. + char* p_start = strstr(line, "pages="); + if (p_start == nullptr) { + // we didn't find anything + continue; + } + + uint64_t nr_pages; + if (sscanf(p_start, "pages=%" SCNu64 "", &nr_pages) == 1) { + vmalloc_total += (nr_pages * getpagesize()); + } + } + + free(line); + + return vmalloc_total; +} + +static bool ReadSysfsFile(const std::string& path, uint64_t* value) { + std::string content; + if (!::android::base::ReadFileToString(path, &content)) { + LOG(ERROR) << "Can't open file: " << path; + return false; + } + + *value = strtoull(content.c_str(), NULL, 10); + if (*value == ULLONG_MAX) { + PLOG(ERROR) << "Invalid file format: " << path; + return false; + } + + return true; +} + +bool ReadIonHeapsSizeKb(uint64_t* size, const std::string& path) { + return ReadSysfsFile(path, size); +} + +bool ReadIonPoolsSizeKb(uint64_t* size, const std::string& path) { + return ReadSysfsFile(path, size); +} + +bool ReadDmabufHeapPoolsSizeKb(uint64_t* size, const std::string& dma_heap_pool_size_path) { + static bool support_dmabuf_heap_pool_size = [dma_heap_pool_size_path]() -> bool { + bool ret = (access(dma_heap_pool_size_path.c_str(), R_OK) == 0); + if (!ret) + LOG(ERROR) << "Unable to read DMA-BUF heap total pool size, read ION total pool " + "size instead."; + return ret; + }(); + + if (!support_dmabuf_heap_pool_size) return ReadIonPoolsSizeKb(size); + + return ReadSysfsFile(dma_heap_pool_size_path, size); +} + +bool ReadDmabufHeapTotalExportedKb(uint64_t* size, const std::string& dma_heap_root_path, + const std::string& dmabuf_sysfs_stats_path) { + static bool support_dmabuf_heaps = [dma_heap_root_path]() -> bool { + bool ret = (access(dma_heap_root_path.c_str(), R_OK) == 0); + if (!ret) LOG(ERROR) << "DMA-BUF heaps not supported, read ION heap total instead."; + return ret; + }(); + + if (!support_dmabuf_heaps) return ReadIonHeapsSizeKb(size); + + std::unique_ptr dir(opendir(dma_heap_root_path.c_str()), closedir); + + if (!dir) { + return false; + } + + std::unordered_set heap_list; + struct dirent* dent; + while ((dent = readdir(dir.get()))) { + if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, "..")) continue; + + heap_list.insert(dent->d_name); + } + + if (heap_list.empty()) return false; + + android::dmabufinfo::DmabufSysfsStats stats; + if (!android::dmabufinfo::GetDmabufSysfsStats(&stats, dmabuf_sysfs_stats_path)) return false; + + auto exporter_info = stats.exporter_info(); + + *size = 0; + for (const auto& heap : heap_list) { + auto iter = exporter_info.find(heap); + if (iter != exporter_info.end()) *size += iter->second.size; + } + + *size = *size / 1024; + + return true; +} + +bool ReadPerProcessGpuMem([[maybe_unused]] std::unordered_map* out) { +#if defined(__ANDROID__) && !defined(__ANDROID_APEX__) && !defined(__ANDROID_VNDK__) + static constexpr const char kBpfGpuMemTotalMap[] = "/sys/fs/bpf/map_gpuMem_gpu_mem_total_map"; + + // Use the read-only wrapper BpfMapRO to properly retrieve the read-only map. + auto map = bpf::BpfMapRO(kBpfGpuMemTotalMap); + if (!map.isValid()) { + LOG(ERROR) << "Can't open file: " << kBpfGpuMemTotalMap; + return false; + } + + if (!out) { + LOG(ERROR) << "ReadPerProcessGpuMem: out param is null"; + return false; + } + out->clear(); + + auto map_key = map.getFirstKey(); + if (!map_key.ok()) { + return true; + } + + do { + uint64_t key = map_key.value(); + uint32_t pid = key; // BPF Key [32-bits GPU ID | 32-bits PID] + + auto gpu_mem = map.readValue(key); + if (!gpu_mem.ok()) { + LOG(ERROR) << "Invalid file format: " << kBpfGpuMemTotalMap; + return false; + } + + const auto& iter = out->find(pid); + if (iter == out->end()) { + out->insert({pid, gpu_mem.value() / 1024}); + } else { + iter->second += gpu_mem.value() / 1024; + } + + map_key = map.getNextKey(key); + } while (map_key.ok()); + + return true; +#else + return false; +#endif +} + +bool ReadProcessGpuUsageKb([[maybe_unused]] uint32_t pid, [[maybe_unused]] uint32_t gpu_id, + uint64_t* size) { + if (size) { + *size = 0; + } + return false; +#if defined(__ANDROID__) && !defined(__ANDROID_APEX__) && !defined(__ANDROID_VNDK__) + static constexpr const char kBpfGpuMemTotalMap[] = "/sys/fs/bpf/map_gpuMem_gpu_mem_total_map"; + + uint64_t gpu_mem; + + // BPF Key [32-bits GPU ID | 32-bits PID] + uint64_t kBpfKeyGpuUsage = ((uint64_t)gpu_id << 32) | pid; + + // Use the read-only wrapper BpfMapRO to properly retrieve the read-only map. + auto map = bpf::BpfMapRO(kBpfGpuMemTotalMap); + if (!map.isValid()) { + LOG(ERROR) << "Can't open file: " << kBpfGpuMemTotalMap; + return false; + } + + auto res = map.readValue(kBpfKeyGpuUsage); + + if (res.ok()) { + gpu_mem = res.value(); + } else if (res.error().code() == ENOENT) { + gpu_mem = 0; + } else { + LOG(ERROR) << "Invalid file format: " << kBpfGpuMemTotalMap; + return false; + } + + if (size) { + *size = gpu_mem / 1024; + } + return true; +#else + if (size) { + *size = 0; + } + return false; +#endif +} + +bool ReadGpuTotalUsageKb(uint64_t* size) { + // gpu_mem_total tracepoint defines PID 0 as global total + // GPU ID 0 suffices for current android devices. + // This will need to check all GPU IDs in future if more than + // one is GPU device is present on the device. + return ReadProcessGpuUsageKb(0, 0, size); +} + +} // namespace meminfo +} // namespace android