| /* |
| * 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.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.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_PARSE_FAILED_NO_CERTIFICATES; |
| import static android.content.pm.PackageManager.INSTALL_STAGED; |
| import static android.content.pm.PackageManager.INSTALL_SUCCEEDED; |
| 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 android.Manifest; |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.AppOpsManager; |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.admin.DevicePolicyEventLogger; |
| import android.app.admin.DevicePolicyManagerInternal; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.IIntentReceiver; |
| import android.content.IIntentSender; |
| 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.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.SessionInfo; |
| import android.content.pm.PackageInstaller.SessionInfo.StagedSessionErrorCode; |
| import android.content.pm.PackageInstaller.SessionParams; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManagerInternal; |
| import android.content.pm.PackageParser; |
| import android.content.pm.PackageParser.PackageParserException; |
| 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.ParsingPackageUtils; |
| import android.content.pm.parsing.result.ParseResult; |
| import android.content.pm.parsing.result.ParseTypeImpl; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.os.Binder; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.FileBridge; |
| import android.os.FileUtils; |
| import android.os.Handler; |
| import android.os.IBinder; |
| 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.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.storage.StorageManager; |
| import android.provider.Settings.Secure; |
| 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.ExceptionUtils; |
| import android.util.MathUtils; |
| import android.util.Slog; |
| import android.util.SparseArray; |
| import android.util.TypedXmlPullParser; |
| import android.util.TypedXmlSerializer; |
| 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.content.NativeLibraryHelper; |
| import com.android.internal.content.PackageHelper; |
| import com.android.internal.messages.nano.SystemMessageProto; |
| import com.android.internal.os.SomeArgs; |
| import com.android.internal.security.VerityUtils; |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.util.FrameworkStatsLog; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.internal.util.Preconditions; |
| import com.android.server.LocalServices; |
| import com.android.server.SystemConfig; |
| import com.android.server.pm.Installer.InstallerException; |
| import com.android.server.pm.dex.DexManager; |
| import com.android.server.pm.parsing.pkg.AndroidPackage; |
| |
| 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.FileOutputStream; |
| import java.io.IOException; |
| 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.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; |
| |
| /** 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_WHITELISTED_RESTRICTED_PERMISSION = |
| "whitelisted-restricted-permission"; |
| private static final String TAG_AUTO_REVOKE_PERMISSIONS_MODE = |
| "auto-revoke-permissions-mode"; |
| 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_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_STAGED_SESSION_ERROR_CODE = "errorCode"; |
| private static final String ATTR_STAGED_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 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; |
| |
| // 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 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; |
| |
| /** 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; |
| @GuardedBy("mLock") |
| private boolean mCommitted = false; |
| @GuardedBy("mLock") |
| private boolean mRelinquished = 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<RevocableFileDescriptor> mFds = new ArrayList<>(); |
| @GuardedBy("mLock") |
| private final ArrayList<FileBridge> mBridges = new ArrayList<>(); |
| |
| @GuardedBy("mLock") |
| private IntentSender mRemoteStatusReceiver; |
| |
| /** Fields derived from commit parsing */ |
| @GuardedBy("mLock") |
| private String mPackageName; |
| @GuardedBy("mLock") |
| private long mVersionCode; |
| @GuardedBy("mLock") |
| private PackageParser.SigningDetails mSigningDetails; |
| @GuardedBy("mLock") |
| private SparseArray<PackageInstallerSession> mChildSessions = new SparseArray<>(); |
| @GuardedBy("mLock") |
| private int mParentSessionId; |
| |
| 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 ArraySet<FileEntry> 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 ArrayMap<String, PerFileChecksum> mChecksums = new ArrayMap<>(); |
| |
| @Nullable |
| final StagedSession mStagedSession; |
| |
| @VisibleForTesting |
| public class StagedSession implements StagingManager.StagedSession { |
| @GuardedBy("mLock") |
| private boolean mSessionApplied; |
| @GuardedBy("mLock") |
| private boolean mSessionReady; |
| @GuardedBy("mLock") |
| private boolean mSessionFailed; |
| @GuardedBy("mLock") |
| private int mSessionErrorCode = SessionInfo.STAGED_SESSION_NO_ERROR; |
| @GuardedBy("mLock") |
| private String mSessionErrorMessage; |
| |
| /** |
| * 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; |
| /** |
| * {@code true} if pre-reboot verification is ongoing which means it is not safe for |
| * {@link #abandon()} to clean up staging directories. |
| */ |
| @GuardedBy("mLock") |
| private boolean mInPreRebootVerification; |
| |
| StagedSession(boolean isReady, boolean isApplied, boolean isFailed, int errorCode, |
| String errorMessage) { |
| mSessionReady = isReady; |
| mSessionApplied = isApplied; |
| mSessionFailed = isFailed; |
| mSessionErrorCode = errorCode; |
| mSessionErrorMessage = errorMessage != null ? errorMessage : ""; |
| } |
| |
| @Override |
| public List<StagingManager.StagedSession> getChildSessions() { |
| if (!params.isMultiPackage) { |
| return Collections.EMPTY_LIST; |
| } |
| synchronized (mLock) { |
| int size = mChildSessions.size(); |
| List<StagingManager.StagedSession> 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() { |
| synchronized (mLock) { |
| // Do not allow destroyed/failed staged session to change state |
| if (mDestroyed || mSessionFailed) return; |
| mSessionReady = true; |
| mSessionApplied = false; |
| mSessionFailed = false; |
| mSessionErrorCode = SessionInfo.STAGED_SESSION_NO_ERROR; |
| mSessionErrorMessage = ""; |
| } |
| mCallback.onStagedSessionChanged(PackageInstallerSession.this); |
| } |
| |
| @Override |
| public void setSessionFailed(int errorCode, String errorMessage) { |
| List<PackageInstallerSession> childSessions; |
| synchronized (mLock) { |
| // Do not allow destroyed/failed staged 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); |
| childSessions = getChildSessionsLocked(); |
| } |
| cleanStageDir(childSessions); |
| mCallback.onStagedSessionChanged(PackageInstallerSession.this); |
| } |
| |
| @Override |
| public void setSessionApplied() { |
| List<PackageInstallerSession> childSessions; |
| synchronized (mLock) { |
| // Do not allow destroyed/failed staged session to change state |
| if (mDestroyed || mSessionFailed) return; |
| mSessionReady = false; |
| mSessionApplied = true; |
| mSessionFailed = false; |
| mSessionErrorCode = SessionInfo.STAGED_SESSION_NO_ERROR; |
| mSessionErrorMessage = ""; |
| Slog.d(TAG, "Marking session " + sessionId + " as applied"); |
| childSessions = getChildSessionsLocked(); |
| } |
| cleanStageDir(childSessions); |
| mCallback.onStagedSessionChanged(PackageInstallerSession.this); |
| } |
| |
| @Override |
| public boolean containsApkSession() { |
| return PackageInstallerSession.this.containsApkSession(); |
| } |
| |
| /** |
| * Installs apks of staged session while skipping the verification process for a committed |
| * and ready session. |
| */ |
| @Override |
| public void installSession(IntentSender statusReceiver) { |
| assertCallerIsOwnerOrRootOrSystem(); |
| assertNotChildLocked("StagedSession#installSession"); |
| Preconditions.checkArgument(isCommitted() && isSessionReady()); |
| |
| // Since staged sessions are installed during boot, the original reference to status |
| // receiver from the owner has already been lost. We can safely replace it with a |
| // status receiver from the system without effecting the flow. |
| updateRemoteStatusReceiver(statusReceiver); |
| install(); |
| } |
| |
| private void updateRemoteStatusReceiver(IntentSender remoteStatusReceiver) { |
| synchronized (mLock) { |
| mRemoteStatusReceiver = remoteStatusReceiver; |
| if (isMultiPackage()) { |
| final IntentSender childIntentSender = new ChildStatusIntentReceiver( |
| mChildSessions.clone(), remoteStatusReceiver).getIntentSender(); |
| for (int i = mChildSessions.size() - 1; i >= 0; --i) { |
| mChildSessions.valueAt(i).mRemoteStatusReceiver = childIntentSender; |
| } |
| } |
| } |
| } |
| |
| @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() { |
| synchronized (mLock) { |
| return mSessionApplied || mSessionFailed; |
| } |
| } |
| |
| @Override |
| public boolean isDestroyed() { |
| return PackageInstallerSession.this.isDestroyed(); |
| } |
| |
| @Override |
| public long getCommittedMillis() { |
| return PackageInstallerSession.this.getCommittedMillis(); |
| } |
| |
| @Override |
| public boolean sessionContains(Predicate<StagingManager.StagedSession> filter) { |
| return PackageInstallerSession.this.sessionContains(s -> filter.test(s.mStagedSession)); |
| } |
| |
| @Override |
| public boolean isSessionReady() { |
| synchronized (mLock) { |
| return mSessionReady; |
| } |
| } |
| |
| @Override |
| public boolean isSessionApplied() { |
| synchronized (mLock) { |
| return mSessionApplied; |
| } |
| } |
| |
| @Override |
| public boolean isSessionFailed() { |
| synchronized (mLock) { |
| return mSessionFailed; |
| } |
| } |
| |
| @StagedSessionErrorCode int getSessionErrorCode() { |
| synchronized (mLock) { |
| return mSessionErrorCode; |
| } |
| } |
| |
| String getSessionErrorMessage() { |
| synchronized (mLock) { |
| return mSessionErrorMessage; |
| } |
| } |
| |
| @Override |
| public void abandon() { |
| final Runnable r; |
| synchronized (mLock) { |
| assertNotChildLocked("StagedSession#abandon"); |
| assertCallerIsOwnerOrRootOrSystem(); |
| if (isInTerminalState()) { |
| // We keep the session in the database if it's in a finalized state. It will be |
| // removed by PackageInstallerService when the last update time is old enough. |
| // Also, in such cases cleanStageDir() has already been executed so no need to |
| // do it now. |
| return; |
| } |
| mDestroyed = true; |
| boolean isCommitted = mCommitted; |
| List<PackageInstallerSession> childSessions = getChildSessionsLocked(); |
| r = () -> { |
| assertNotLocked("abandonStaged"); |
| if (isCommitted) { |
| mStagingManager.abortCommittedSession(this); |
| } |
| cleanStageDir(childSessions); |
| destroyInternal(); |
| dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned", null); |
| maybeCleanUpChildSessions(); |
| }; |
| if (mInPreRebootVerification) { |
| // Pre-reboot verification is ongoing, not safe to clean up the session yet. |
| mPendingAbandonCallback = r; |
| mCallback.onStagedSessionChanged(PackageInstallerSession.this); |
| return; |
| } |
| } |
| r.run(); |
| } |
| |
| /** |
| * Notified by the staging manager that pre-reboot verification is about to start. The |
| * return value should be checked to decide whether it is OK to start pre-reboot |
| * verification. In the case of a destroyed session, {@code false} is returned and there is |
| * no need to start pre-reboot verification. |
| */ |
| @Override |
| public boolean notifyStartPreRebootVerification() { |
| synchronized (mLock) { |
| if (mInPreRebootVerification) { |
| throw new IllegalStateException("Pre-reboot verification has started"); |
| } |
| if (mDestroyed) { |
| return false; |
| } |
| mInPreRebootVerification = true; |
| return true; |
| } |
| } |
| |
| /** |
| * Notified by the staging manager that pre-reboot verification has ended. Now it is safe to |
| * clean up the session if {@link #abandon()} has been called previously. |
| */ |
| @Override |
| public void notifyEndPreRebootVerification() { |
| synchronized (mLock) { |
| if (!mInPreRebootVerification) { |
| throw new IllegalStateException("Pre-reboot verification not started"); |
| } |
| mInPreRebootVerification = false; |
| } |
| dispatchPendingAbandonCallback(); |
| } |
| |
| /** |
| * 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(); |
| Preconditions.checkArgument(isCommitted()); |
| Preconditions.checkArgument(!mSessionApplied && !mSessionFailed); |
| verify(); |
| } |
| |
| private void dispatchPendingAbandonCallback() { |
| final Runnable callback; |
| synchronized (mLock) { |
| callback = mPendingAbandonCallback; |
| mPendingAbandonCallback = null; |
| } |
| if (callback != null) { |
| callback.run(); |
| } |
| } |
| } |
| |
| /** |
| * 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). |
| * <p> |
| * 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<File> mResolvedStagedFiles = new ArrayList<>(); |
| @GuardedBy("mLock") |
| private final List<File> mResolvedInheritedFiles = new ArrayList<>(); |
| @GuardedBy("mLock") |
| private final List<String> mResolvedInstructionSets = new ArrayList<>(); |
| @GuardedBy("mLock") |
| private final List<String> mResolvedNativeLibPaths = new ArrayList<>(); |
| @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; |
| |
| 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 (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; |
| } |
| }; |
| |
| 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; |
| args.recycle(); |
| |
| sendOnPackageInstalled(mContext, statusReceiver, sessionId, |
| isInstallerDeviceOwnerOrAffiliatedProfileOwner(), userId, |
| packageName, returnCode, message, extras); |
| |
| break; |
| case MSG_SESSION_VALIDATION_FAILURE: |
| final int error = msg.arg1; |
| final String detailMessage = (String) msg.obj; |
| onSessionValidationFailure(error, detailMessage); |
| break; |
| } |
| |
| return true; |
| } |
| }; |
| |
| private boolean isDataLoaderInstallation() { |
| return params.dataLoaderParams != null; |
| } |
| |
| private boolean isStreamingInstallation() { |
| return isDataLoaderInstallation() && params.dataLoaderParams.getType() == STREAMING; |
| } |
| |
| private boolean isIncrementalInstallation() { |
| return isDataLoaderInstallation() && params.dataLoaderParams.getType() == INCREMENTAL; |
| } |
| |
| private boolean isSystemDataLoaderInstallation() { |
| if (!isDataLoaderInstallation()) { |
| return false; |
| } |
| return SYSTEM_DATA_LOADER_PACKAGE.equals( |
| this.params.dataLoaderParams.getComponentName().getPackageName()); |
| } |
| |
| /** |
| * @return {@code true} iff the installing is app an device owner or affiliated profile owner. |
| */ |
| private boolean isInstallerDeviceOwnerOrAffiliatedProfileOwner() { |
| assertNotLocked("isInstallerDeviceOwnerOrAffiliatedProfileOwner"); |
| // It is safe to access mInstallerUid and mInstallSource without lock |
| // because they are immutable after sealing. |
| assertSealed("isInstallerDeviceOwnerOrAffiliatedProfileOwner"); |
| if (userId != UserHandle.getUserId(mInstallerUid)) { |
| return false; |
| } |
| DevicePolicyManagerInternal dpmi = |
| LocalServices.getService(DevicePolicyManagerInternal.class); |
| return dpmi != null && dpmi.canSilentlyInstallPackage( |
| mInstallSource.installerPackageName, mInstallerUid); |
| } |
| |
| 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; |
| |
| @IntDef({USER_ACTION_NOT_NEEDED, USER_ACTION_REQUIRED, USER_ACTION_PENDING_APK_PARSING}) |
| @interface |
| UserActionRequirement {} |
| |
| /** |
| * Checks if the permissions still need to be confirmed. |
| * |
| * <p>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; |
| synchronized (mLock) { |
| if (mPermissionsManuallyAccepted) { |
| return USER_ACTION_NOT_NEEDED; |
| } |
| packageName = mPackageName; |
| } |
| |
| final boolean forcePermissionPrompt = |
| (params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0 |
| || params.requireUserAction == SessionParams.USER_ACTION_REQUIRED; |
| if (forcePermissionPrompt) { |
| return USER_ACTION_REQUIRED; |
| } |
| // It is safe to access mInstallerUid and mInstallSource without lock |
| // because they are immutable after sealing. |
| final boolean isInstallPermissionGranted = |
| (mPm.checkUidPermission(android.Manifest.permission.INSTALL_PACKAGES, |
| mInstallerUid) == PackageManager.PERMISSION_GRANTED); |
| final boolean isSelfUpdatePermissionGranted = |
| (mPm.checkUidPermission(android.Manifest.permission.INSTALL_SELF_UPDATES, |
| mInstallerUid) == PackageManager.PERMISSION_GRANTED); |
| final boolean isUpdatePermissionGranted = |
| (mPm.checkUidPermission(android.Manifest.permission.INSTALL_PACKAGE_UPDATES, |
| mInstallerUid) == PackageManager.PERMISSION_GRANTED); |
| final boolean isUpdateWithoutUserActionPermissionGranted = (mPm.checkUidPermission( |
| android.Manifest.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION, mInstallerUid) |
| == PackageManager.PERMISSION_GRANTED); |
| final int targetPackageUid = mPm.getPackageUid(packageName, 0, userId); |
| final boolean isUpdate = targetPackageUid != -1 || isApexSession(); |
| final InstallSourceInfo existingInstallSourceInfo = isUpdate |
| ? mPm.getInstallSourceInfo(packageName) |
| : null; |
| final String existingInstallerPackageName = existingInstallSourceInfo != null |
| ? existingInstallSourceInfo.getInstallingPackageName() |
| : null; |
| final boolean isInstallerOfRecord = isUpdate |
| && Objects.equals(existingInstallerPackageName, getInstallerPackageName()); |
| final boolean isSelfUpdate = targetPackageUid == mInstallerUid; |
| final boolean isPermissionGranted = isInstallPermissionGranted |
| || (isUpdatePermissionGranted && isUpdate) |
| || (isSelfUpdatePermissionGranted && isSelfUpdate); |
| final boolean isInstallerRoot = (mInstallerUid == Process.ROOT_UID); |
| final boolean isInstallerSystem = (mInstallerUid == Process.SYSTEM_UID); |
| |
| // 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 = isPermissionGranted || isInstallerRoot |
| || isInstallerSystem || isInstallerDeviceOwnerOrAffiliatedProfileOwner(); |
| |
| if (noUserActionNecessary) { |
| return USER_ACTION_NOT_NEEDED; |
| } |
| |
| if (mPm.isInstallDisabledForPackage(getInstallerPackageName(), mInstallerUid, userId)) { |
| // show the installer to account for device poslicy or unknown sources use cases |
| return USER_ACTION_REQUIRED; |
| } |
| |
| if (params.requireUserAction == SessionParams.USER_ACTION_NOT_REQUIRED |
| && isUpdateWithoutUserActionPermissionGranted |
| && (isInstallerOfRecord || isSelfUpdate)) { |
| return USER_ACTION_PENDING_APK_PARSING; |
| } |
| |
| return USER_ACTION_REQUIRED; |
| } |
| |
| 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<String, PerFileChecksum> checksums, |
| boolean prepared, boolean committed, boolean destroyed, boolean sealed, |
| @Nullable int[] childSessionIds, int parentSessionId, boolean isReady, |
| boolean isFailed, boolean isApplied, int stagedSessionErrorCode, |
| String stagedSessionErrorMessage) { |
| mCallback = callback; |
| mContext = context; |
| mPm = pm; |
| 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.installerPackageName; |
| 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 = committed; |
| mDestroyed = destroyed; |
| mStagedSession = params.isStaged ? new StagedSession(isReady, isApplied, isFailed, |
| stagedSessionErrorCode, stagedSessionErrorMessage) : null; |
| |
| 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."); |
| } |
| } |
| |
| /** |
| * 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(); |
| synchronized (mLock) { |
| info.sessionId = sessionId; |
| info.userId = userId; |
| info.installerPackageName = mInstallSource.installerPackageName; |
| info.installerAttributionTag = mInstallSource.installerAttributionTag; |
| info.resolvedBaseCodePath = (mResolvedBaseFile != null) ? |
| mResolvedBaseFile.getAbsolutePath() : null; |
| info.progress = mProgress; |
| info.sealed = mSealed; |
| info.isCommitted = mCommitted; |
| info.active = mActiveCount.get() > 0; |
| |
| info.mode = params.mode; |
| info.installReason = params.installReason; |
| info.installScenario = params.installScenario; |
| info.sizeBytes = params.sizeBytes; |
| info.appPackageName = mPackageName != null ? mPackageName : params.appPackageName; |
| if (includeIcon) { |
| info.appIcon = params.appIcon; |
| } |
| info.appLabel = 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.grantedRuntimePermissions; |
| info.whitelistedRestrictedPermissions = params.whitelistedRestrictedPermissions; |
| info.autoRevokePermissionsMode = params.autoRevokePermissionsMode; |
| info.installFlags = params.installFlags; |
| info.isMultiPackage = params.isMultiPackage; |
| info.isStaged = params.isStaged; |
| info.rollbackDataPolicy = params.rollbackDataPolicy; |
| info.parentSessionId = mParentSessionId; |
| info.childSessionIds = getChildSessionIdsLocked(); |
| info.isStagedSessionApplied = isStagedSessionApplied(); |
| info.isStagedSessionReady = isStagedSessionReady(); |
| info.isStagedSessionFailed = isStagedSessionFailed(); |
| info.setStagedSessionErrorCode(getStagedSessionErrorCode(), |
| getStagedSessionErrorMessage()); |
| info.createdMillis = createdMillis; |
| info.updatedMillis = updatedMillis; |
| info.requireUserAction = params.requireUserAction; |
| } |
| return info; |
| } |
| |
| public boolean isPrepared() { |
| synchronized (mLock) { |
| return mPrepared; |
| } |
| } |
| |
| public boolean isSealed() { |
| synchronized (mLock) { |
| return mSealed; |
| } |
| } |
| |
| /** {@hide} */ |
| boolean isCommitted() { |
| synchronized (mLock) { |
| return mCommitted; |
| } |
| } |
| |
| /** {@hide} */ |
| boolean isDestroyed() { |
| synchronized (mLock) { |
| return mDestroyed; |
| } |
| } |
| |
| /** Returns true if a staged session has reached a final state and can be forgotten about */ |
| public boolean isStagedAndInTerminalState() { |
| return params.isStaged && mStagedSession.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 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 (mCommitted) { |
| 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() || !mCommitted) { |
| 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() { |
| assertCallerIsOwnerOrRoot(); |
| synchronized (mLock) { |
| assertPreparedAndNotCommittedOrDestroyedLocked("getNames"); |
| |
| return getNamesLocked(); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private String[] getNamesLocked() { |
| if (!isDataLoaderInstallation()) { |
| String[] result = stageDir.list(); |
| if (result == null) { |
| result = EmptyArray.STRING; |
| } |
| return result; |
| } |
| |
| 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<File> filterFiles(File parent, String[] names, FileFilter filter) { |
| ArrayList<File> 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<File> getAddedApksLocked() { |
| String[] names = getNamesLocked(); |
| return filterFiles(stageDir, names, sAddedApkFilter); |
| } |
| |
| @GuardedBy("mLock") |
| private List<File> 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 = mInstallSource.initiatingPackageName; |
| |
| final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class); |
| appOps.checkPackage(Binder.getCallingUid(), initiatingPackageName); |
| |
| final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); |
| final AndroidPackage callingInstaller = pmi.getPackage(initiatingPackageName); |
| 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); |
| } |
| } |
| |
| assertCallerIsOwnerOrRoot(); |
| synchronized (mLock) { |
| assertPreparedAndNotCommittedOrDestroyedLocked("addChecksums"); |
| |
| if (mChecksums.containsKey(name)) { |
| throw new IllegalStateException("Duplicate checksums."); |
| } |
| |
| mChecksums.put(name, new PerFileChecksum(checksums, signature)); |
| } |
| } |
| |
| @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"); |
| } |
| } |
| |
| @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"); |
| } |
| |
| try { |
| final File target = new File(path); |
| final File source = new File(stageDir, target.getName()); |
| try { |
| Os.link(path, source.getAbsolutePath()); |
| // Grant READ access for APK to be read successfully |
| Os.chmod(source.getAbsolutePath(), 0644); |
| } catch (ErrnoException e) { |
| e.rethrowAsIOException(); |
| } |
| if (!SELinux.restorecon(source)) { |
| throw new IOException("Can't relabel file: " + source); |
| } |
| } catch (IOException e) { |
| 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); |
| } |
| |
| ParcelFileDescriptor targetPfd = openTargetInternal(target.getAbsolutePath(), |
| O_CREAT | O_WRONLY, 0644); |
| Os.chmod(target.getAbsolutePath(), 0644); |
| |
| // 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, |
| PackageHelper.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 (mLock) { |
| 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. 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) { |
| if (hasParentSessionId()) { |
| throw new IllegalStateException( |
| "Session " + sessionId + " is a child of multi-package session " |
| + getParentSessionId() + " and may not be committed directly."); |
| } |
| |
| if (!markAsSealed(statusReceiver, forTransfer)) { |
| return; |
| } |
| if (isMultiPackage()) { |
| synchronized (mLock) { |
| final IntentSender childIntentSender = |
| new ChildStatusIntentReceiver(mChildSessions.clone(), statusReceiver) |
| .getIntentSender(); |
| 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(childIntentSender, forTransfer)) { |
| sealFailed = true; |
| } |
| } |
| if (sealFailed) { |
| return; |
| } |
| } |
| } |
| |
| dispatchSessionSealed(); |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| private void handleStreamValidateAndCommit() { |
| PackageManagerException unrecoverableFailure = null; |
| // This will track whether the session and any children were validated and are ready to |
| // progress to the next phase of install |
| boolean allSessionsReady = false; |
| try { |
| allSessionsReady = streamValidateAndCommit(); |
| } catch (PackageManagerException e) { |
| unrecoverableFailure = e; |
| } |
| |
| if (isMultiPackage()) { |
| final List<PackageInstallerSession> childSessions; |
| synchronized (mLock) { |
| childSessions = getChildSessionsLocked(); |
| } |
| int childCount = childSessions.size(); |
| |
| // This will contain all child sessions that do not encounter an unrecoverable failure |
| ArrayList<PackageInstallerSession> nonFailingSessions = new ArrayList<>(childCount); |
| |
| for (int i = childCount - 1; i >= 0; --i) { |
| // commit all children, regardless if any of them fail; we'll throw/return |
| // as appropriate once all children have been processed |
| try { |
| PackageInstallerSession session = childSessions.get(i); |
| allSessionsReady &= session.streamValidateAndCommit(); |
| nonFailingSessions.add(session); |
| } catch (PackageManagerException e) { |
| allSessionsReady = false; |
| if (unrecoverableFailure == null) { |
| unrecoverableFailure = e; |
| } |
| } |
| } |
| // If we encountered any unrecoverable failures, destroy all other sessions including |
| // the parent |
| if (unrecoverableFailure != null) { |
| // {@link #streamValidateAndCommit()} calls |
| // {@link #onSessionValidationFailure(PackageManagerException)}, but we don't |
| // expect it to ever do so for parent sessions. Call that on this parent to clean |
| // it up and notify listeners of the error. |
| onSessionValidationFailure(unrecoverableFailure); |
| // fail other child sessions that did not already fail |
| for (int i = nonFailingSessions.size() - 1; i >= 0; --i) { |
| PackageInstallerSession session = nonFailingSessions.get(i); |
| session.onSessionValidationFailure(unrecoverableFailure); |
| } |
| } |
| } |
| |
| if (!allSessionsReady) { |
| return; |
| } |
| |
| mHandler.obtainMessage(MSG_INSTALL).sendToTarget(); |
| } |
| |
| private final class FileSystemConnector extends |
| IPackageInstallerSessionFileSystemConnector.Stub { |
| final Set<String> mAddedFiles = new ArraySet<>(); |
| |
| FileSystemConnector(List<InstallationFileParcel> 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); |
| } |
| } |
| } |
| |
| private class ChildStatusIntentReceiver { |
| private final SparseArray<PackageInstallerSession> mChildSessionsRemaining; |
| private final IntentSender mStatusReceiver; |
| private final IIntentSender.Stub mLocalSender = new IIntentSender.Stub() { |
| @Override |
| public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, |
| IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { |
| statusUpdate(intent); |
| } |
| }; |
| |
| private ChildStatusIntentReceiver(SparseArray<PackageInstallerSession> remainingSessions, |
| IntentSender statusReceiver) { |
| this.mChildSessionsRemaining = remainingSessions; |
| this.mStatusReceiver = statusReceiver; |
| } |
| |
| public IntentSender getIntentSender() { |
| return new IntentSender((IIntentSender) mLocalSender); |
| } |
| |
| public void statusUpdate(Intent intent) { |
| mHandler.post(() -> { |
| if (mChildSessionsRemaining.size() == 0) { |
| // no children to deal with, ignore. |
| return; |
| } |
| final boolean destroyed; |
| synchronized (mLock) { |
| destroyed = mDestroyed; |
| } |
| if (destroyed) { |
| // the parent has already been terminated, ignore. |
| return; |
| } |
| final int sessionId = intent.getIntExtra( |
| PackageInstaller.EXTRA_SESSION_ID, 0); |
| final int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, |
| PackageInstaller.STATUS_FAILURE); |
| final int sessionIndex = mChildSessionsRemaining.indexOfKey(sessionId); |
| final String message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); |
| if (PackageInstaller.STATUS_SUCCESS == status) { |
| mChildSessionsRemaining.removeAt(sessionIndex); |
| if (mChildSessionsRemaining.size() == 0) { |
| destroyInternal(); |
| dispatchSessionFinished(INSTALL_SUCCEEDED, |
| "Session installed", null); |
| } |
| } else if (PackageInstaller.STATUS_PENDING_USER_ACTION == status) { |
| try { |
| mStatusReceiver.sendIntent(mContext, 0, intent, null, null); |
| } catch (IntentSender.SendIntentException ignore) { |
| } |
| } else { // failure, let's forward and clean up this session. |
| intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, |
| PackageInstallerSession.this.sessionId); |
| mChildSessionsRemaining.clear(); // we're done. Don't send any more. |
| destroyInternal(); |
| dispatchSessionFinished(INSTALL_FAILED_INTERNAL_ERROR, |
| "Child session " + sessionId + " failed: " + message, null); |
| } |
| }); |
| } |
| } |
| |
| /** {@hide} */ |
| private class StreamingException extends Exception { |
| StreamingException(Throwable cause) { |
| super(cause); |
| } |
| } |
| |
| /** |
| * Returns whether or not a package can be installed while Secure FRP is enabled. |
| * <p> |
| * 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( |
| PackageManagerInternal.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; |
| } |
| |
| /** |
| * Checks if the package can be installed on IncFs. |
| */ |
| private static boolean isIncrementalInstallationAllowed(String packageName) { |
| final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); |
| final PackageSetting existingPkgSetting = pmi.getPackageSetting(packageName); |
| if (existingPkgSetting == null || existingPkgSetting.pkg == null) { |
| return true; |
| } |
| |
| return !existingPkgSetting.pkg.isSystem() |
| && !existingPkgSetting.getPkgState().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(@NonNull IntentSender statusReceiver, boolean forTransfer) { |
| Objects.requireNonNull(statusReceiver); |
| assertCallerIsOwnerOrRoot(); |
| |
| synchronized (mLock) { |
| assertPreparedAndNotDestroyedLocked("commit of session " + sessionId); |
| assertNoWriteFileTransfersOpenLocked(); |
| |
| final boolean isSecureFrpEnabled = |
| (Secure.getInt(mContext.getContentResolver(), Secure.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"); |
| } |
| } |
| |
| mRemoteStatusReceiver = 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. |
| */ |
| 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 (mCommitted) { |
| 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(); |
| |
| mCommitted = true; |
| committedMillis = System.currentTimeMillis(); |
| } |
| return true; |
| } catch (PackageManagerException e) { |
| throw onSessionValidationFailure(e); |
| } catch (Throwable e) { |
| // Convert all exceptions into package manager exceptions as only those are handled |
| // in the code above. |
| throw onSessionValidationFailure(new PackageManagerException(e)); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private @NonNull List<PackageInstallerSession> getChildSessionsLocked() { |
| List<PackageInstallerSession> 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<PackageInstallerSession> getChildSessions() { |
| synchronized (mLock) { |
| return getChildSessionsLocked(); |
| } |
| } |
| |
| /** |
| * Seal the session to prevent further modification. |
| * |
| * <p>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(); |
| // Dispatch message to remove session from PackageInstallerService. |
| dispatchSessionFinished(error, detailMessage, null); |
| // TODO(b/173194203): clean up staged session in destroyInternal() call instead |
| if (isStaged() && stageDir != null) { |
| cleanStageDir(); |
| } |
| } |
| |
| private void onSessionVerificationFailure(int error, String msg) { |
| final String msgWithErrorCode = PackageManager.installStatusToString(error, msg); |
| Slog.e(TAG, "Failed to verify session " + sessionId + " [" + msgWithErrorCode + "]"); |
| // Session is sealed and committed but could not be verified, we need to destroy it. |
| destroyInternal(); |
| if (isStaged()) { |
| mStagedSession.setSessionFailed( |
| SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, msgWithErrorCode); |
| // TODO(b/136257624): Remove this once all verification logic has been transferred out |
| // of StagingManager. |
| mStagingManager.notifyVerificationComplete(mStagedSession); |
| } else { |
| // Dispatch message to remove session from PackageInstallerService. |
| dispatchSessionFinished(error, msg, null); |
| } |
| } |
| |
| private void onSessionInstallationFailure(int error, String detailedMessage) { |
| Slog.e(TAG, "Install of session " + sessionId + " failed: " + detailedMessage); |
| destroyInternal(); |
| dispatchSessionFinished(error, detailedMessage, null); |
| } |
| |
| private void onSystemDataLoaderUnrecoverable() { |
| final PackageManagerService packageManagerService = mPm; |
| final String packageName = mPackageName; |
| if (TextUtils.isEmpty(packageName)) { |
| // The package has not been installed. |
| return; |
| } |
| mHandler.post(() -> { |
| if (packageManagerService.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. |
| * |
| * <p> 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. |
| */ |
| void onAfterSessionRead(SparseArray<PackageInstallerSession> 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) { |
| 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)); |
| |
| ApplicationInfo newOwnerAppInfo = mPm.getApplicationInfo(packageName, 0, userId); |
| if (newOwnerAppInfo == null) { |
| throw new ParcelableException(new PackageManager.NameNotFoundException(packageName)); |
| } |
| |
| if (PackageManager.PERMISSION_GRANTED != mPm.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, packageName, null); |
| } |
| } |
| |
| private void handleInstall() { |
| if (isInstallerDeviceOwnerOrAffiliatedProfileOwner()) { |
| DevicePolicyEventLogger |
| .createEvent(DevicePolicyEnums.INSTALL_PACKAGE) |
| .setAdmin(mInstallSource.installerPackageName) |
| .write(); |
| } |
| |
| // Check if APEX update is allowed. We do this check in handleInstall, since this is one of |
| // the places that: |
| // * Shared between staged and non-staged APEX update flows. |
| // * Only is called after boot completes. |
| // The later is important, since isApexUpdateAllowed check depends on the |
| // ModuleInfoProvider, which is only populated after device has booted. |
| if (isApexSession()) { |
| boolean checkApexUpdateAllowed = |
| (params.installFlags & PackageManager.INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK) |
| == 0; |
| synchronized (mLock) { |
| if (checkApexUpdateAllowed && !isApexUpdateAllowed(mPackageName, |
| mInstallSource.installerPackageName)) { |
| onSessionValidationFailure(PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE, |
| "Update of APEX package " + mPackageName + " is not allowed for " |
| + mInstallSource.installerPackageName); |
| return; |
| } |
| } |
| |
| if (!params.isStaged) { |
| // For non-staged APEX installs also check if there is a staged session that |
| // contains the same APEX. If that's the case, we should fail this session. |
| synchronized (mLock) { |
| int sessionId = mStagingManager.getSessionIdByPackageName(mPackageName); |
| if (sessionId != -1) { |
| onSessionValidationFailure( |
| PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE, |
| "Staged session " + sessionId + " already contains " |
| + mPackageName); |
| return; |
| } |
| } |
| } |
| } |
| |
| if (params.isStaged) { |
| mStagingManager.commitSession(mStagedSession); |
| // TODO(b/136257624): CTS test fails if we don't send session finished broadcast, even |
| // though ideally, we just need to send session committed broadcast. |
| dispatchSessionFinished(INSTALL_SUCCEEDED, "Session staged", null); |
| return; |
| } |
| |
| verify(); |
| } |
| |
| private void verify() { |
| try { |
| verifyNonStaged(); |
| } catch (PackageManagerException e) { |
| final String completeMsg = ExceptionUtils.getCompleteMessage(e); |
| onSessionVerificationFailure(e.error, completeMsg); |
| } |
| } |
| |
| private void verifyNonStaged() |
| throws PackageManagerException { |
| final PackageManagerService.VerificationParams verifyingSession = |
| prepareForVerification(); |
| if (verifyingSession == null) { |
| return; |
| } |
| if (isMultiPackage()) { |
| final List<PackageInstallerSession> childSessions; |
| synchronized (mLock) { |
| childSessions = getChildSessionsLocked(); |
| } |
| // Spot check to reject a non-staged multi package install of APEXes and APKs. |
| if (!params.isStaged && containsApkSession() |
| && sessionContains(s -> s.isApexSession())) { |
| throw new PackageManagerException( |
| PackageManager.INSTALL_FAILED_SESSION_INVALID, |
| "Non-staged multi package install of APEX and APK packages is not supported"); |
| } |
| List<PackageManagerService.VerificationParams> verifyingChildSessions = |
| new ArrayList<>(childSessions.size()); |
| boolean success = true; |
| PackageManagerException failure = null; |
| for (int i = 0; i < childSessions.size(); ++i) { |
| final PackageInstallerSession session = childSessions.get(i); |
| try { |
| final PackageManagerService.VerificationParams verifyingChildSession = |
| session.prepareForVerification(); |
| if (verifyingChildSession != null) { |
| verifyingChildSessions.add(verifyingChildSession); |
| } |
| } catch (PackageManagerException e) { |
| failure = e; |
| success = false; |
| } |
| } |
| if (!success) { |
| final IntentSender statusReceiver; |
| synchronized (mLock) { |
| statusReceiver = mRemoteStatusReceiver; |
| } |
| sendOnPackageInstalled(mContext, statusReceiver, sessionId, |
| isInstallerDeviceOwnerOrAffiliatedProfileOwner(), userId, null, |
| failure.error, failure.getLocalizedMessage(), null); |
| return; |
| } |
| mPm.verifyStage(verifyingSession, verifyingChildSessions); |
| } else { |
| mPm.verifyStage(verifyingSession); |
| } |
| } |
| |
| private void install() { |
| try { |
| installNonStaged(); |
| } catch (PackageManagerException e) { |
| final String completeMsg = ExceptionUtils.getCompleteMessage(e); |
| onSessionInstallationFailure(e.error, completeMsg); |
| } |
| } |
| |
| private void installNonStaged() |
| throws PackageManagerException { |
| final PackageManagerService.InstallParams installingSession = makeInstallParams(); |
| if (installingSession == null) { |
| throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, |
| "Session should contain at least one apk session for installation"); |
| } |
| if (isMultiPackage()) { |
| final List<PackageInstallerSession> childSessions; |
| synchronized (mLock) { |
| childSessions = getChildSessionsLocked(); |
| } |
| List<PackageManagerService.InstallParams> installingChildSessions = |
| new ArrayList<>(childSessions.size()); |
| boolean success = true; |
| PackageManagerException failure = null; |
| for (int i = 0; i < childSessions.size(); ++i) { |
| final PackageInstallerSession session = childSessions.get(i); |
| try { |
| final PackageManagerService.InstallParams installingChildSession = |
| session.makeInstallParams(); |
| if (installingChildSession != null) { |
| installingChildSessions.add(installingChildSession); |
| } |
| } catch (PackageManagerException e) { |
| failure = e; |
| success = false; |
| } |
| } |
| if (!success) { |
| final IntentSender statusReceiver; |
| synchronized (mLock) { |
| statusReceiver = mRemoteStatusReceiver; |
| } |
| sendOnPackageInstalled(mContext, statusReceiver, sessionId, |
| isInstallerDeviceOwnerOrAffiliatedProfileOwner(), userId, null, |
| failure.error, failure.getLocalizedMessage(), null); |
| return; |
| } |
| mPm.installStage(installingSession, installingChildSessions); |
| } else { |
| mPm.installStage(installingSession); |
| } |
| } |
| |
| /** |
| * Stages this session for verification and returns a |
| * {@link PackageManagerService.VerificationParams} representing this new staged state or null |
| * in case permissions need to be requested before verification can proceed. |
| */ |
| @Nullable |
| private PackageManagerService.VerificationParams prepareForVerification() |
| throws PackageManagerException { |
| assertNotLocked("makeSessionActive"); |
| |
| @UserActionRequirement |
| int userActionRequirement = USER_ACTION_NOT_NEEDED; |
| // TODO(b/159331446): Move this to makeSessionActiveForInstall and update javadoc |
| if (!params.isMultiPackage) { |
| userActionRequirement = computeUserActionRequirement(); |
| if (userActionRequirement == USER_ACTION_REQUIRED) { |
| sendPendingUserActionIntent(); |
| return null; |
| } // else, we'll wait until we parse to determine if we need to |
| } |
| |
| boolean silentUpdatePolicyEnforceable = false; |
| synchronized (mLock) { |
| if (mRelinquished) { |
| throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, |
| "Session relinquished"); |
| } |
| if (mDestroyed) { |
| throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, |
| "Session destroyed"); |
| } |
| if (!mSealed) { |
| throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, |
| "Session not sealed"); |
| } |
| PackageLite result = parseApkLite(); |
| if (result != null) { |
| mPackageLite = result; |
| synchronized (mProgressLock) { |
| mInternalProgress = 0.5f; |
| computeProgressLocked(true); |
| } |
| |
| extractNativeLibraries( |
| mPackageLite, stageDir, params.abiOverride, mayInheritNativeLibs()); |
| |
| if (userActionRequirement == USER_ACTION_PENDING_APK_PARSING) { |
| if (result.getTargetSdk() < Build.VERSION_CODES.Q) { |
| sendPendingUserActionIntent(); |
| return null; |
| } |
| if (params.requireUserAction == SessionParams.USER_ACTION_NOT_REQUIRED) { |
| silentUpdatePolicyEnforceable = true; |
| } |
| } |
| } |
| } |
| if (silentUpdatePolicyEnforceable) { |
| if (!mSilentUpdatePolicy.isSilentUpdateAllowed( |
| getInstallerPackageName(), getPackageName())) { |
| // Fall back to the non-silent update if a repeated installation is invoked within |
| // the throttle time. |
| sendPendingUserActionIntent(); |
| return null; |
| } |
| mSilentUpdatePolicy.track(getInstallerPackageName(), getPackageName()); |
| } |
| synchronized (mLock) { |
| return makeVerificationParamsLocked(); |
| } |
| } |
| |
| private void sendPendingUserActionIntent() { |
| // User needs to confirm installation; |
| // give installer an intent they can use to involve |
| // user. |
| final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_INSTALL); |
| intent.setPackage(mPm.getPackageInstallerPackageName()); |
| intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); |
| |
| final IntentSender statusReceiver; |
| synchronized (mLock) { |
| statusReceiver = mRemoteStatusReceiver; |
| } |
| sendOnUserActionRequired(mContext, statusReceiver, sessionId, intent); |
| |
| // Commit was keeping session marked as active until now; release |
| // that extra refcount so session appears idle. |
| closeInternal(false); |
| } |
| |
| /** |
| * Prepares staged directory with any inherited APKs and returns the parsed package. |
| */ |
| @Nullable |
| private PackageLite parseApkLite() throws PackageManagerException { |
| |
| |
| // TODO(b/136257624): Some logic in this if block probably belongs in |
| // makeInstallParams(). |
| if (!isMultiPackage() && !isApexSession()) { |
| Objects.requireNonNull(mPackageName); |
| Objects.requireNonNull(mSigningDetails); |
| Objects.requireNonNull(mResolvedBaseFile); |
| |
| // If we haven't already parsed, inherit any packages and native libraries from existing |
| // install that haven't been overridden. |
| if (params.mode == SessionParams.MODE_INHERIT_EXISTING) { |
| try { |
| final List<File> fromFiles = mResolvedInheritedFiles; |
| final File toDir = stageDir; |
| |
| 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(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(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); |
| } |
| } |
| // For mode inherit existing, it would link/copy existing files to stage dir in the |
| // above block. 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. |
| return getOrParsePackageLiteLocked(stageDir, /* flags */ 0); |
| } |
| return null; |
| } |
| |
| @GuardedBy("mLock") |
| @Nullable |
| /** |
| * Returns a {@link com.android.server.pm.PackageManagerService.VerificationParams} |
| */ |
| private PackageManagerService.VerificationParams makeVerificationParamsLocked() { |
| final IPackageInstallObserver2 localObserver; |
| if (!hasParentSessionId()) { |
| // Avoid attaching this observer to child session since they won't use it. |
| 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) { |
| onVerificationComplete(); |
| } else { |
| onSessionVerificationFailure(returnCode, msg); |
| } |
| } |
| }; |
| } else { |
| localObserver = null; |
| } |
| |
| final UserHandle user; |
| if ((params.installFlags & PackageManager.INSTALL_ALL_USERS) != 0) { |
| user = UserHandle.ALL; |
| } else { |
| user = new UserHandle(userId); |
| } |
| |
| mRelinquished = true; |
| |
| // TODO(b/169375643): Remove this workaround once b/161121612 is fixed. |
| PackageInstaller.SessionParams copiedParams = params.copy(); |
| if (params.isStaged) { |
| // This is called by the pre-reboot verification. Don't enable rollback here since |
| // it has been enabled when pre-reboot verification starts. |
| copiedParams.installFlags &= ~PackageManager.INSTALL_ENABLE_ROLLBACK; |
| } |
| return mPm.new VerificationParams(user, stageDir, localObserver, copiedParams, |
| mInstallSource, mInstallerUid, mSigningDetails, sessionId, mPackageLite); |
| } |
| |
| private void onVerificationComplete() { |
| // Staged sessions will be installed later during boot |
| if (isStaged()) { |
| // TODO(b/136257624): Remove this once all verification logic has been transferred out |
| // of StagingManager. |
| mStagingManager.notifyPreRebootVerification_Apk_Complete(mStagedSession); |
| // TODO(b/136257624): We also need to destroy internals for verified staged session, |
| // otherwise file descriptors are never closed for verified staged session until reboot |
| return; |
| } |
| |
| install(); |
| } |
| |
| /** |
| * Stages this session for install and returns a |
| * {@link PackageManagerService.InstallParams} representing this new staged state. |
| */ |
| @Nullable |
| private PackageManagerService.InstallParams makeInstallParams() |
| throws PackageManagerException { |
| synchronized (mLock) { |
| if (mDestroyed) { |
| throw new PackageManagerException( |
| INSTALL_FAILED_INTERNAL_ERROR, "Session destroyed"); |
| } |
| if (!mSealed) { |
| throw new PackageManagerException( |
| INSTALL_FAILED_INTERNAL_ERROR, "Session not sealed"); |
| } |
| } |
| |
| // Do not try to install staged apex session. Parent session will have at least one apk |
| // session. |
| if (!isMultiPackage() && isApexSession() && params.isStaged) { |
| sendUpdateToRemoteStatusReceiver(INSTALL_SUCCEEDED, |
| "Apex package should have been installed by apexd", 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 (isStaged()) { |
| sendUpdateToRemoteStatusReceiver(returnCode, msg, extras); |
| } else { |
| // We've reached point of no return; call into PMS to install the stage. |
| // Regardless of success or failure we always destroy session. |
| destroyInternal(); |
| dispatchSessionFinished(returnCode, msg, extras); |
| } |
| } |
| }; |
| |
| 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 mPm.new InstallParams(stageDir, localObserver, params, mInstallSource, user, |
| mSigningDetails, mInstallerUid, mPackageLite); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private PackageLite getOrParsePackageLiteLocked(File packageFile, int flags) |
| throws PackageManagerException { |
| if (mPackageLite != null) { |
| return mPackageLite; |
| } |
| |
| final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); |
| final ParseResult<PackageLite> 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 = Process.INVALID_UID; |
| } else { |
| packageUid = mPm.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 PackageSetting ps = mPm.getPackageSetting(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<PackageInstallerSession> filter) { |
| if (!isMultiPackage()) { |
| return filter.test(this); |
| } |
| final List<PackageInstallerSession> childSessions; |
| synchronized (mLock) { |
| childSessions = getChildSessionsLocked(); |
| } |
| for (PackageInstallerSession child: childSessions) { |
| if (filter.test(child)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| boolean containsApkSession() { |
| return sessionContains((s) -> !s.isApexSession()); |
| } |
| |
| private boolean isApexUpdateAllowed(String apexPackageName, String installerPackageName) { |
| if (mPm.getModuleInfo(apexPackageName, 0) != null) { |
| final String modulesInstaller = |
| SystemConfig.getInstance().getModulesInstallerPackageName(); |
| if (modulesInstaller == null) { |
| Slog.w(TAG, "No modules installer defined"); |
| return false; |
| } |
| return modulesInstaller.equals(installerPackageName); |
| } |
| final String vendorApexInstaller = |
| SystemConfig.getInstance().getAllowedVendorApexes().get(apexPackageName); |
| if (vendorApexInstaller == null) { |
| Slog.w(TAG, apexPackageName + " is not allowed to be updated"); |
| return false; |
| } |
| return vendorApexInstaller.equals(installerPackageName); |
| } |
| |
| /** |
| * Validate apex install. |
| * <p> |
| * Sets {@link #mResolvedBaseFile} for RollbackManager to use. Sets {@link #mPackageName} for |
| * StagingManager to use. |
| */ |
| @GuardedBy("mLock") |
| private void validateApexInstallLocked() |
| throws PackageManagerException { |
| final List<File> addedFiles = getAddedApksLocked(); |
| if (addedFiles.isEmpty()) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| String.format("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<ApkLite> 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(); |
| } |
| } |
| |
| /** |
| * Validate install by confirming that all application packages are have |
| * consistent package name, version code, and signing certificates. |
| * <p> |
| * Clears and populates {@link #mResolvedBaseFile}, |
| * {@link #mResolvedStagedFiles}, and {@link #mResolvedInheritedFiles}. |
| * <p> |
| * Renames package files in stage to match split names defined inside. |
| * <p> |
| * 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; |
| PackageLite packageLite = null; |
| mPackageLite = null; |
| mPackageName = null; |
| mVersionCode = -1; |
| mSigningDetails = PackageParser.SigningDetails.UNKNOWN; |
| |
| mResolvedBaseFile = null; |
| mResolvedStagedFiles.clear(); |
| mResolvedInheritedFiles.clear(); |
| |
| final PackageInfo pkgInfo = mPm.getPackageInfo( |
| params.appPackageName, PackageManager.GET_SIGNATURES |
| | PackageManager.MATCH_STATIC_SHARED_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. |
| mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled() |
| && params.mode == SessionParams.MODE_INHERIT_EXISTING |
| && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath()); |
| |
| final List<File> removedFiles = getRemovedFilesLocked(); |
| final List<String> 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); |
| } |
| } |
| |
| final List<File> addedFiles = getAddedApksLocked(); |
| if (addedFiles.isEmpty() && removeSplitList.size() == 0) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| String.format("Session: %d. No packages staged in %s", sessionId, |
| stageDir.getAbsolutePath())); |
| } |
| |
| // Verify that all staged packages are internally consistent |
| final ArraySet<String> stagedSplits = new ArraySet<>(); |
| final ArrayMap<String, ApkLite> splitApks = new ArrayMap<>(); |
| final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); |
| for (File addedFile : addedFiles) { |
| final ParseResult<ApkLite> result = ApkLiteParseUtils.parseApkLite(input.reset(), |
| addedFile, ParsingPackageUtils.PARSE_COLLECT_CERTIFICATES); |
| if (result.isError()) { |
| throw new PackageManagerException(result.getErrorCode(), |
| result.getErrorMessage(), result.getException()); |
| } |
| |
| final ApkLite apk = result.getResult(); |
| if (!stagedSplits.add(apk.getSplitName())) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| "Split " + apk.getSplitName() + " was defined multiple times"); |
| } |
| |
| // Use first package to define unknown values |
| if (mPackageName == null) { |
| mPackageName = apk.getPackageName(); |
| mVersionCode = apk.getLongVersionCode(); |
| } |
| if (mSigningDetails == PackageParser.SigningDetails.UNKNOWN) { |
| mSigningDetails = apk.getSigningDetails(); |
| } |
| |
| assertApkConsistentLocked(String.valueOf(addedFile), 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); |
| resolveAndStageFileLocked(addedFile, targetFile, apk.getSplitName()); |
| |
| // Base is coming from session |
| if (apk.getSplitName() == null) { |
| mResolvedBaseFile = targetFile; |
| baseApk = apk; |
| } else { |
| splitApks.put(apk.getSplitName(), apk); |
| } |
| } |
| |
| 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 == PackageParser.SigningDetails.UNKNOWN) { |
| mSigningDetails = unsafeGetCertsWithoutVerification( |
| pkgInfo.applicationInfo.sourceDir); |
| } |
| } |
| |
| if (isIncrementalInstallation() && !isIncrementalInstallationAllowed(mPackageName)) { |
| throw new PackageManagerException( |
| PackageManager.INSTALL_FAILED_SESSION_INVALID, |
| "Incremental installation of this package is not allowed."); |
| } |
| |
| 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"); |
| } |
| if (baseApk.isSplitRequired() && stagedSplits.size() <= 1) { |
| 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<PackageLite> 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<PackageLite> 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 PackageParser.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); |
| } |
| |
| // 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); |
| if (!stagedSplits.contains(splitName) && !splitRemoved) { |
| inheritFileLocked(splitFile); |
| } |
| } |
| } |
| |
| // 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<String> libDirsToInherit = new ArrayList<>(); |
| final List<File> 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)) { |
| throw new PackageManagerException(INSTALL_FAILED_MISSING_SPLIT, |
| "Missing split for " + mPackageName); |
| } |
| } |
| } |
| 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(); |
| } |
| } |
| 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 { |
| 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 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 void storeBytesToInstallationFile(final String localPath, final String absolutePath, |
| final byte[] bytes) throws IOException { |
| if (!isIncrementalInstallation() || mIncrementalFileStorages == null) { |
| FileUtils.bytesToFile(absolutePath, bytes); |
| } else { |
| mIncrementalFileStorages.makeFile(localPath, bytes); |
| } |
| } |
| |
| @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 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 inheritFileLocked(File origFile) { |
| mResolvedInheritedFiles.add(origFile); |
| |
| maybeInheritFsveritySignatureLocked(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); |
| } |
| } |
| |
| private PackageParser.SigningDetails unsafeGetCertsWithoutVerification(String path) |
| throws PackageManagerException { |
| try { |
| return ApkSignatureVerifier.unsafeGetCertsWithoutVerification(path, |
| PackageParser.SigningDetails.SignatureSchemeVersion.JAR); |
| } catch (PackageParserException e) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| "Couldn't obtain signatures from APK : " + path); |
| } |
| } |
| |
| /** |
| * 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<File> 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().installerPackageName; |
| } |
| |
| String getInstallerAttributionTag() { |
| return getInstallSource().installerAttributionTag; |
| } |
| |
| InstallSource getInstallSource() { |
| synchronized (mLock) { |
| return mInstallSource; |
| } |
| } |
| |
| 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(List<String> instructionSets, File fromDir) |
| throws PackageManagerException { |
| for (String instructionSet : instructionSets) { |
| try { |
| mPm.mInstaller.createOatDir(fromDir.getAbsolutePath(), instructionSet); |
| } catch (InstallerException e) { |
| throw PackageManagerException.from(e); |
| } |
| } |
| } |
| |
| private void linkFile(String relativePath, String fromBase, String toBase) throws IOException { |
| try { |
| // Try |
| if (mIncrementalFileStorages != null && mIncrementalFileStorages.makeLink(relativePath, |
| fromBase, toBase)) { |
| return; |
| } |
| mPm.mInstaller.linkFile(relativePath, fromBase, toBase); |
| } catch (InstallerException | IOException e) { |
| throw new IOException("failed linkOrCreateDir(" + relativePath + ", " |
| + fromBase + ", " + toBase + ")", e); |
| } |
| } |
| |
| private void linkFiles(List<File> 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(relativePath, fromBase, toBase); |
| } |
| |
| Slog.d(TAG, "Linked " + fromFiles.size() + " files into " + toDir); |
| } |
| |
| private static void copyFiles(List<File> 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(), 0644); |
| } 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); |
| } |
| |
| 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()) { |
| throw new SecurityException("Must be sealed to accept permissions"); |
| } |
| |
| if (accepted) { |
| // Mark and kick off another install pass |
| synchronized (mLock) { |
| mPermissionsManuallyAccepted = true; |
| mHandler.obtainMessage(MSG_INSTALL).sendToTarget(); |
| } |
| } else { |
| destroyInternal(); |
| dispatchSessionFinished(INSTALL_FAILED_ABORTED, "User rejected permissions", null); |
| } |
| } |
| |
| public void open() throws IOException { |
| if (mActiveCount.getAndIncrement() == 0) { |
| mCallback.onSessionActiveChanged(this, true); |
| } |
| |
| 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); |
| } |
| } |
| |
| @Override |
| public void close() { |
| closeInternal(true); |
| } |
| |
| private void closeInternal(boolean checkCaller) { |
| int activeCount; |
| synchronized (mLock) { |
| if (checkCaller) { |
| assertCallerIsOwnerOrRoot(); |
| } |
| |
| activeCount = mActiveCount.decrementAndGet(); |
| } |
| |
| if (activeCount == 0) { |
| mCallback.onSessionActiveChanged(this, false); |
| } |
| } |
| |
| /** |
| * Cleans up the relevant stored files and information of all child sessions. |
| * <p>Cleaning up the stored files and session information is necessary for |
| * preventing the orphan children sessions. |
| * <ol> |
| * <li>To call {@link #destroyInternal()} cleans up the stored files.</li> |
| * <li>To call {@link #dispatchSessionFinished(int, String, Bundle)} to trigger the |
| * procedure to clean up the information in PackageInstallerService.</li> |
| * </ol></p> |
| */ |
| private void maybeCleanUpChildSessions() { |
| if (!isMultiPackage()) { |
| return; |
| } |
| |
| final List<PackageInstallerSession> childSessions = getChildSessions(); |
| final int size = childSessions.size(); |
| for (int i = 0; i < size; ++i) { |
| final PackageInstallerSession session = childSessions.get(i); |
| session.destroyInternal(); |
| session.dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned" |
| + " because the parent session is abandoned", null); |
| } |
| } |
| |
| private void abandonNonStaged() { |
| synchronized (mLock) { |
| assertNotChildLocked("abandonNonStaged"); |
| assertCallerIsOwnerOrRootOrSystem(); |
| if (mRelinquished) { |
| if (LOGD) Slog.d(TAG, "Ignoring abandon after commit relinquished control"); |
| return; |
| } |
| destroyInternal(); |
| } |
| dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned", null); |
| maybeCleanUpChildSessions(); |
| } |
| |
| @GuardedBy("mLock") |
| private void assertNotChildLocked(String cookie) { |
| if (hasParentSessionId()) { |
| throw new IllegalStateException(cookie + " can't be called on a child session, id=" |
| + sessionId + " parentId=" + getParentSessionId()); |
| } |
| } |
| |
| @Override |
| public void abandon() { |
| if (params.isStaged) { |
| mStagedSession.abandon(); |
| } else { |
| abandonNonStaged(); |
| } |
| } |
| |
| @Override |
| public boolean isMultiPackage() { |
| return params.isMultiPackage; |
| } |
| |
| @Override |
| public boolean isStaged() { |
| return params.isStaged; |
| } |
| |
| @Override |
| public DataLoaderParamsParcel getDataLoaderParams() { |
| mContext.enforceCallingOrSelfPermission(Manifest.permission.USE_INSTALLER_V2, null); |
| return params.dataLoaderParams != null ? params.dataLoaderParams.getData() : null; |
| } |
| |
| @Override |
| public void addFile(int location, String name, long lengthBytes, byte[] metadata, |
| byte[] signature) { |
| mContext.enforceCallingOrSelfPermission(Manifest.permission.USE_INSTALLER_V2, null); |
| 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); |
| } |
| } |
| } |
| |
| @Override |
| public void removeFile(int location, String name) { |
| mContext.enforceCallingOrSelfPermission(Manifest.permission.USE_INSTALLER_V2, null); |
| 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<InstallationFileParcel> addedFiles = new ArrayList<>(); |
| final List<String> 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. |
| final IntentSender statusReceiver; |
| synchronized (mLock) { |
| statusReceiver = mRemoteStatusReceiver; |
| } |
| sendPendingStreaming(mContext, statusReceiver, 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. |
| final IntentSender statusReceiver; |
| synchronized (mLock) { |
| statusReceiver = mRemoteStatusReceiver; |
| } |
| sendPendingStreaming(mContext, statusReceiver, sessionId, e.getMessage()); |
| } |
| } |
| }; |
| |
| if (!manualStartAndDestroy) { |
| final PerUidReadTimeouts[] perUidReadTimeouts = mPm.getPerUidReadTimeouts(); |
| |
| 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.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) |
| && !mCommitted && !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"); |
| } |
| |
| 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) { |
| sendUpdateToRemoteStatusReceiver(returnCode, msg, extras); |
| |
| 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) { |
| final IntentSender statusReceiver; |
| final String packageName; |
| synchronized (mLock) { |
| statusReceiver = mRemoteStatusReceiver; |
| packageName = mPackageName; |
| } |
| 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 = packageName; |
| args.arg2 = msg; |
| args.arg3 = extras; |
| args.arg4 = statusReceiver; |
| args.argi1 = returnCode; |
| mHandler.obtainMessage(MSG_ON_PACKAGE_INSTALLED, args).sendToTarget(); |
| } |
| } |
| |
| /** {@hide} */ |
| boolean isStagedSessionReady() { |
| return params.isStaged && mStagedSession.isSessionReady(); |
| } |
| |
| /** {@hide} */ |
| boolean isStagedSessionApplied() { |
| return params.isStaged && mStagedSession.isSessionApplied(); |
| } |
| |
| /** {@hide} */ |
| boolean isStagedSessionFailed() { |
| return params.isStaged && mStagedSession.isSessionFailed(); |
| } |
| |
| /** {@hide} */ |
| @StagedSessionErrorCode int getStagedSessionErrorCode() { |
| return params.isStaged ? mStagedSession.getSessionErrorCode() |
| : SessionInfo.STAGED_SESSION_NO_ERROR; |
| } |
| |
| /** {@hide} */ |
| String getStagedSessionErrorMessage() { |
| return params.isStaged ? mStagedSession.getSessionErrorMessage() : ""; |
| } |
| |
| private void destroyInternal() { |
| 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; |
| } |
| // For staged sessions, we don't delete the directory where the packages have been copied, |
| // since these packages are supposed to be read on reboot. |
| // Those dirs are deleted when the staged session has reached a final state. |
| if (stageDir != null && !params.isStaged) { |
| try { |
| if (incrementalFileStorages != null) { |
| incrementalFileStorages.cleanUpAndMarkComplete(); |
| } |
| mPm.mInstaller.rmPackageDir(stageDir.getAbsolutePath()); |
| } catch (InstallerException ignored) { |
| } |
| } |
| } |
| |
| private void cleanStageDir(List<PackageInstallerSession> childSessions) { |
| if (isMultiPackage()) { |
| for (PackageInstallerSession childSession : childSessions) { |
| childSession.cleanStageDir(); |
| } |
| } else { |
| cleanStageDir(); |
| } |
| } |
| |
| private void cleanStageDir() { |
| final IncrementalFileStorages incrementalFileStorages; |
| synchronized (mLock) { |
| incrementalFileStorages = mIncrementalFileStorages; |
| mIncrementalFileStorages = null; |
| } |
| try { |
| if (incrementalFileStorages != null) { |
| incrementalFileStorages.cleanUpAndMarkComplete(); |
| } |
| mPm.mInstaller.rmPackageDir(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.installerPackageName); |
| pw.printPair("installInitiatingPackageName", mInstallSource.initiatingPackageName); |
| pw.printPair("installOriginatingPackageName", mInstallSource.originatingPackageName); |
| 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); |
| |
| pw.printPair("mClientProgress", mClientProgress); |
| pw.printPair("mProgress", mProgress); |
| pw.printPair("mCommitted", mCommitted); |
| pw.printPair("mSealed", mSealed); |
| pw.printPair("mPermissionsManuallyAccepted", mPermissionsManuallyAccepted); |
| pw.printPair("mRelinquished", mRelinquished); |
| 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("mStagedSessionApplied", isStagedSessionApplied()); |
| pw.printPair("mStagedSessionFailed", isStagedSessionFailed()); |
| pw.printPair("mStagedSessionReady", isStagedSessionReady()); |
| pw.printPair("mStagedSessionErrorCode", getStagedSessionErrorCode()); |
| pw.printPair("mStagedSessionErrorMessage", getStagedSessionErrorMessage()); |
| 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(Intent.EXTRA_INTENT, intent); |
| try { |
| target.sendIntent(context, 0, fillIn, null, null); |
| } 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, |
| String msg, Bundle extras) { |
| if (INSTALL_SUCCEEDED == returnCode && showNotification) { |
| boolean update = (extras != null) && extras.getBoolean(Intent.EXTRA_REPLACING); |
| Notification notification = PackageInstallerService.buildSuccessNotification(context, |
| context.getResources() |
| .getString(update ? R.string.package_updated_device_owner : |
| R.string.package_installed_device_owner), |
| 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); |
| 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); |
| } |
| } |
| try { |
| target.sendIntent(context, 0, fillIn, null, null); |
| } catch (IntentSender.SendIntentException ignored) { |
| } |
| } |
| |
| /** |
| * 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 { |
| target.sendIntent(context, 0, intent, null, null); |
| } catch (IntentSender.SendIntentException ignored) { |
| } |
| } |
| |
| private static void writeGrantedRuntimePermissionsLocked(TypedXmlSerializer out, |
| String[] grantedRuntimePermissions) throws IOException { |
| if (grantedRuntimePermissions != null) { |
| for (String permission : grantedRuntimePermissions) { |
| out.startTag(null, TAG_GRANTED_RUNTIME_PERMISSION); |
| writeStringAttribute(out, ATTR_NAME, permission); |
| out.endTag(null, TAG_GRANTED_RUNTIME_PERMISSION); |
| } |
| } |
| } |
| |
| private static void writeWhitelistedRestrictedPermissionsLocked(@NonNull TypedXmlSerializer out, |
| @Nullable List<String> 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.installerPackageName); |
| writeStringAttribute(out, ATTR_INSTALLER_ATTRIBUTION_TAG, |
| mInstallSource.installerAttributionTag); |
| out.attributeInt(null, ATTR_INSTALLER_UID, mInstallerUid); |
| writeStringAttribute(out, ATTR_INITIATING_PACKAGE_NAME, |
| mInstallSource.initiatingPackageName); |
| writeStringAttribute(out, ATTR_ORIGINATING_PACKAGE_NAME, |
| mInstallSource.originatingPackageName); |
| 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, mCommitted); |
| 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, isStagedSessionReady()); |
| writeBooleanAttribute(out, ATTR_IS_FAILED, isStagedSessionFailed()); |
| writeBooleanAttribute(out, ATTR_IS_APPLIED, isStagedSessionApplied()); |
| out.attributeInt(null, ATTR_STAGED_SESSION_ERROR_CODE, getStagedSessionErrorCode()); |
| writeStringAttribute(out, ATTR_STAGED_SESSION_ERROR_MESSAGE, |
| getStagedSessionErrorMessage()); |
| // 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); |
| |
| 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()); |
| } |
| |
| writeGrantedRuntimePermissionsLocked(out, params.grantedRuntimePermissions); |
| 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); |
| } |
| |
| } |
| |
| 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 |
| * @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 String installerAttributionTag = readStringAttribute(in, |
| ATTR_INSTALLER_ATTRIBUTION_TAG); |
| final int installerUid = in.getAttributeInt(null, ATTR_INSTALLER_UID, pm.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); |
| |
| 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 stagedSessionErrorCode = in.getAttributeInt(null, ATTR_STAGED_SESSION_ERROR_CODE, |
| SessionInfo.STAGED_SESSION_NO_ERROR); |
| final String stagedSessionErrorMessage = readStringAttribute(in, |
| ATTR_STAGED_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<String> grantedRuntimePermissions = new ArrayList<>(); |
| List<String> whitelistedRestrictedPermissions = new ArrayList<>(); |
| int autoRevokePermissionsMode = MODE_DEFAULT; |
| List<Integer> childSessionIds = new ArrayList<>(); |
| List<InstallationFile> files = new ArrayList<>(); |
| ArrayMap<String, List<Checksum>> checksums = new ArrayMap<>(); |
| ArrayMap<String, byte[]> signatures = new ArrayMap<>(); |
| 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; |
| } |
| if (TAG_GRANTED_RUNTIME_PERMISSION.equals(in.getName())) { |
| grantedRuntimePermissions.add(readStringAttribute(in, ATTR_NAME)); |
| } |
| if (TAG_WHITELISTED_RESTRICTED_PERMISSION.equals(in.getName())) { |
| whitelistedRestrictedPermissions.add(readStringAttribute(in, ATTR_NAME)); |
| |
| } |
| if (TAG_AUTO_REVOKE_PERMISSIONS_MODE.equals(in.getName())) { |
| autoRevokePermissionsMode = in.getAttributeInt(null, ATTR_MODE); |
| } |
| if (TAG_CHILD_SESSION.equals(in.getName())) { |
| childSessionIds.add(in.getAttributeInt(null, ATTR_SESSION_ID, |
| SessionInfo.INVALID_ID)); |
| } |
| if (TAG_SESSION_FILE.equals(in.getName())) { |
| 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))); |
| } |
| if (TAG_SESSION_CHECKSUM.equals(in.getName())) { |
| 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<Checksum> fileChecksums = checksums.get(fileName); |
| if (fileChecksums == null) { |
| fileChecksums = new ArrayList<>(); |
| checksums.put(fileName, fileChecksums); |
| } |
| fileChecksums.add(checksum); |
| } |
| if (TAG_SESSION_CHECKSUM_SIGNATURE.equals(in.getName())) { |
| final String fileName = readStringAttribute(in, ATTR_NAME); |
| final byte[] signature = readByteArrayAttribute(in, ATTR_SIGNATURE); |
| signatures.put(fileName, signature); |
| } |
| } |
| |
| if (grantedRuntimePermissions.size() > 0) { |
| params.grantedRuntimePermissions = |
| grantedRuntimePermissions.toArray(EmptyArray.STRING); |
| } |
| |
| 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<String, PerFileChecksum> 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<Checksum> perFileChecksum = checksums.valueAt(i); |
| final byte[] perFileSignature = signatures.get(fileName); |
| checksumsMap.put(fileName, new PerFileChecksum( |
| perFileChecksum.toArray(new Checksum[perFileChecksum.size()]), |
| perFileSignature)); |
| } |
| } |
| |
| InstallSource installSource = InstallSource.create(installInitiatingPackageName, |
| installOriginatingPackageName, installerPackageName, installerAttributionTag); |
| 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, |
| stagedSessionErrorCode, stagedSessionErrorMessage); |
| } |
| } |