/*
|
* Copyright (C) 2018 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package com.android.server.pm;
|
|
import android.annotation.NonNull;
|
import android.annotation.Nullable;
|
import android.apex.ApexInfo;
|
import android.apex.ApexInfoList;
|
import android.apex.ApexSessionInfo;
|
import android.content.Context;
|
import android.content.IIntentReceiver;
|
import android.content.IIntentSender;
|
import android.content.Intent;
|
import android.content.IntentSender;
|
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInstaller;
|
import android.content.pm.PackageInstaller.SessionInfo;
|
import android.content.pm.PackageManager;
|
import android.content.pm.PackageParser.PackageParserException;
|
import android.content.pm.PackageParser.SigningDetails;
|
import android.content.pm.PackageParser.SigningDetails.SignatureSchemeVersion;
|
import android.content.pm.ParceledListSlice;
|
import android.content.pm.Signature;
|
import android.content.rollback.IRollbackManager;
|
import android.os.Bundle;
|
import android.os.Handler;
|
import android.os.IBinder;
|
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelableException;
|
import android.os.PowerManager;
|
import android.os.RemoteException;
|
import android.os.ServiceManager;
|
import android.util.Slog;
|
import android.util.SparseArray;
|
import android.util.apk.ApkSignatureVerifier;
|
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.os.BackgroundThread;
|
|
import java.io.File;
|
import java.io.IOException;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.List;
|
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.TimeUnit;
|
import java.util.function.Predicate;
|
import java.util.stream.Collectors;
|
|
/**
|
* This class handles staged install sessions, i.e. install sessions that require packages to
|
* be installed only after a reboot.
|
*/
|
public class StagingManager {
|
|
private static final String TAG = "StagingManager";
|
|
private final PackageInstallerService mPi;
|
private final ApexManager mApexManager;
|
private final PowerManager mPowerManager;
|
private final Handler mBgHandler;
|
|
@GuardedBy("mStagedSessions")
|
private final SparseArray<PackageInstallerSession> mStagedSessions = new SparseArray<>();
|
|
StagingManager(PackageInstallerService pi, ApexManager am, Context context) {
|
mPi = pi;
|
mApexManager = am;
|
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
mBgHandler = BackgroundThread.getHandler();
|
}
|
|
private void updateStoredSession(@NonNull PackageInstallerSession sessionInfo) {
|
synchronized (mStagedSessions) {
|
PackageInstallerSession storedSession = mStagedSessions.get(sessionInfo.sessionId);
|
// storedSession might be null if a call to abortSession was made before the session
|
// is updated.
|
if (storedSession != null) {
|
mStagedSessions.put(sessionInfo.sessionId, sessionInfo);
|
}
|
}
|
}
|
|
ParceledListSlice<PackageInstaller.SessionInfo> getSessions(int callingUid) {
|
final List<PackageInstaller.SessionInfo> result = new ArrayList<>();
|
synchronized (mStagedSessions) {
|
for (int i = 0; i < mStagedSessions.size(); i++) {
|
final PackageInstallerSession stagedSession = mStagedSessions.valueAt(i);
|
result.add(stagedSession.generateInfoForCaller(false /*icon*/, callingUid));
|
}
|
}
|
return new ParceledListSlice<>(result);
|
}
|
|
private boolean validateApexSignature(String apexPath, String packageName) {
|
final SigningDetails signingDetails;
|
try {
|
signingDetails = ApkSignatureVerifier.verify(apexPath, SignatureSchemeVersion.JAR);
|
} catch (PackageParserException e) {
|
Slog.e(TAG, "Unable to parse APEX package: " + apexPath, e);
|
return false;
|
}
|
|
final PackageInfo packageInfo = mApexManager.getPackageInfoForApexName(packageName);
|
|
if (packageInfo == null) {
|
// Don't allow installation of new APEX.
|
Slog.e(TAG, "Attempted to install a new apex " + packageName + ". Rejecting");
|
return false;
|
}
|
|
final SigningDetails existingSigningDetails;
|
try {
|
existingSigningDetails = ApkSignatureVerifier.verify(
|
packageInfo.applicationInfo.sourceDir, SignatureSchemeVersion.JAR);
|
} catch (PackageParserException e) {
|
Slog.e(TAG, "Unable to parse APEX package: "
|
+ packageInfo.applicationInfo.sourceDir, e);
|
return false;
|
}
|
|
// Now that we have both sets of signatures, demand that they're an exact match.
|
if (Signature.areExactMatch(existingSigningDetails.signatures, signingDetails.signatures)) {
|
return true;
|
}
|
|
return false;
|
}
|
|
private boolean submitSessionToApexService(@NonNull PackageInstallerSession session,
|
List<PackageInstallerSession> childSessions,
|
ApexInfoList apexInfoList) {
|
boolean submittedToApexd = mApexManager.submitStagedSession(
|
session.sessionId,
|
childSessions != null
|
? childSessions.stream().mapToInt(s -> s.sessionId).toArray() :
|
new int[]{},
|
apexInfoList);
|
if (!submittedToApexd) {
|
session.setStagedSessionFailed(
|
SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
|
"APEX staging failed, check logcat messages from apexd for more details.");
|
return false;
|
}
|
for (ApexInfo newPackage : apexInfoList.apexInfos) {
|
PackageInfo activePackage = mApexManager.getPackageInfoForApexName(
|
newPackage.packageName);
|
if (activePackage == null) {
|
continue;
|
}
|
long activeVersion = activePackage.applicationInfo.longVersionCode;
|
if (session.params.requiredInstalledVersionCode
|
!= PackageManager.VERSION_CODE_HIGHEST) {
|
if (activeVersion != session.params.requiredInstalledVersionCode) {
|
session.setStagedSessionFailed(
|
SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
|
"Installed version of APEX package " + newPackage.packageName
|
+ " does not match required. Active version: " + activeVersion
|
+ " required: " + session.params.requiredInstalledVersionCode);
|
|
if (!mApexManager.abortActiveSession()) {
|
Slog.e(TAG, "Failed to abort apex session " + session.sessionId);
|
}
|
return false;
|
}
|
}
|
|
boolean allowsDowngrade = PackageManagerServiceUtils.isDowngradePermitted(
|
session.params.installFlags, activePackage.applicationInfo.flags);
|
if (activeVersion > newPackage.versionCode && !allowsDowngrade) {
|
session.setStagedSessionFailed(
|
SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
|
"Downgrade of APEX package " + newPackage.packageName
|
+ " is not allowed. Active version: " + activeVersion
|
+ " attempted: " + newPackage.versionCode);
|
|
if (!mApexManager.abortActiveSession()) {
|
Slog.e(TAG, "Failed to abort apex session " + session.sessionId);
|
}
|
return false;
|
}
|
}
|
return true;
|
}
|
|
private static boolean isApexSession(@NonNull PackageInstallerSession session) {
|
return (session.params.installFlags & PackageManager.INSTALL_APEX) != 0;
|
}
|
|
private void preRebootVerification(@NonNull PackageInstallerSession session) {
|
boolean success = true;
|
|
final ApexInfoList apexInfoList = new ApexInfoList();
|
// APEX checks. For single-package sessions, check if they contain an APEX. For
|
// multi-package sessions, find all the child sessions that contain an APEX.
|
if (!session.isMultiPackage()
|
&& isApexSession(session)) {
|
success = submitSessionToApexService(session, null, apexInfoList);
|
|
} else if (session.isMultiPackage()) {
|
List<PackageInstallerSession> childSessions =
|
Arrays.stream(session.getChildSessionIds())
|
// Retrieve cached sessions matching ids.
|
.mapToObj(i -> mStagedSessions.get(i))
|
// Filter only the ones containing APEX.
|
.filter(childSession -> isApexSession(childSession))
|
.collect(Collectors.toList());
|
if (!childSessions.isEmpty()) {
|
success = submitSessionToApexService(session, childSessions, apexInfoList);
|
} // else this is a staged multi-package session with no APEX files.
|
}
|
|
if (!success) {
|
// submitSessionToApexService will populate error.
|
return;
|
}
|
|
if (sessionContainsApk(session)) {
|
if (!installApksInSession(session, /* preReboot */ true)) {
|
session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
|
"APK verification failed. Check logcat messages for "
|
+ "more information.");
|
// TODO(b/118865310): abort the session on apexd.
|
return;
|
}
|
}
|
|
if (apexInfoList.apexInfos != null && apexInfoList.apexInfos.length > 0) {
|
// For APEXes, we validate the signature here before we mark the session as ready,
|
// so we fail the session early if there is a signature mismatch. For APKs, the
|
// signature verification will be done by the package manager at the point at which
|
// it applies the staged install.
|
for (ApexInfo apexPackage : apexInfoList.apexInfos) {
|
if (!validateApexSignature(apexPackage.packagePath,
|
apexPackage.packageName)) {
|
session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
|
"APK-container signature verification failed for package "
|
+ apexPackage.packageName + ". Signature of file "
|
+ apexPackage.packagePath + " does not match the signature of "
|
+ " the package already installed.");
|
// TODO(b/118865310): abort the session on apexd.
|
return;
|
}
|
}
|
}
|
|
if ((session.params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
|
// If rollback is enabled for this session, we call through to the RollbackManager
|
// with the list of sessions it must enable rollback for. Note that notifyStagedSession
|
// is a synchronous operation.
|
final IRollbackManager rm = IRollbackManager.Stub.asInterface(
|
ServiceManager.getService(Context.ROLLBACK_SERVICE));
|
try {
|
// NOTE: To stay consistent with the non-staged install flow, we don't fail the
|
// entire install if rollbacks can't be enabled.
|
if (!rm.notifyStagedSession(session.sessionId)) {
|
Slog.e(TAG, "Unable to enable rollback for session: " + session.sessionId);
|
}
|
} catch (RemoteException re) {
|
// Cannot happen, the rollback manager is in the same process.
|
}
|
}
|
|
session.setStagedSessionReady();
|
if (sessionContainsApex(session)
|
&& !mApexManager.markStagedSessionReady(session.sessionId)) {
|
session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED,
|
"APEX staging failed, check logcat messages from apexd for more "
|
+ "details.");
|
}
|
}
|
|
|
private boolean sessionContains(@NonNull PackageInstallerSession session,
|
Predicate<PackageInstallerSession> filter) {
|
if (!session.isMultiPackage()) {
|
return filter.test(session);
|
}
|
synchronized (mStagedSessions) {
|
return !(Arrays.stream(session.getChildSessionIds())
|
// Retrieve cached sessions matching ids.
|
.mapToObj(i -> mStagedSessions.get(i))
|
// Filter only the ones containing APEX.
|
.filter(childSession -> filter.test(childSession))
|
.collect(Collectors.toList())
|
.isEmpty());
|
}
|
}
|
|
private boolean sessionContainsApex(@NonNull PackageInstallerSession session) {
|
return sessionContains(session, (s) -> isApexSession(s));
|
}
|
|
private boolean sessionContainsApk(@NonNull PackageInstallerSession session) {
|
return sessionContains(session, (s) -> !isApexSession(s));
|
}
|
|
private void resumeSession(@NonNull PackageInstallerSession session) {
|
boolean hasApex = sessionContainsApex(session);
|
if (hasApex) {
|
// Check with apexservice whether the apex packages have been activated.
|
ApexSessionInfo apexSessionInfo = mApexManager.getStagedSessionInfo(session.sessionId);
|
if (apexSessionInfo == null) {
|
session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED,
|
"apexd did not know anything about a staged session supposed to be"
|
+ "activated");
|
return;
|
}
|
if (isApexSessionFailed(apexSessionInfo)) {
|
session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED,
|
"APEX activation failed. Check logcat messages from apexd for "
|
+ "more information.");
|
return;
|
}
|
if (apexSessionInfo.isVerified) {
|
// Session has been previously submitted to apexd, but didn't complete all the
|
// pre-reboot verification, perhaps because the device rebooted in the meantime.
|
// Greedily re-trigger the pre-reboot verification.
|
Slog.d(TAG, "Found pending staged session " + session.sessionId + " still to be "
|
+ "verified, resuming pre-reboot verification");
|
mBgHandler.post(() -> preRebootVerification(session));
|
return;
|
}
|
if (!apexSessionInfo.isActivated && !apexSessionInfo.isSuccess) {
|
// In all the remaining cases apexd will try to apply the session again at next
|
// boot. Nothing to do here for now.
|
Slog.w(TAG, "Staged session " + session.sessionId + " scheduled to be applied "
|
+ "at boot didn't activate nor fail. This usually means that apexd will "
|
+ "retry at next reboot.");
|
return;
|
}
|
}
|
// The APEX part of the session is activated, proceed with the installation of APKs.
|
if (!installApksInSession(session, /* preReboot */ false)) {
|
session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_ACTIVATION_FAILED,
|
"Staged installation of APKs failed. Check logcat messages for"
|
+ "more information.");
|
|
if (!hasApex) {
|
return;
|
}
|
|
if (!mApexManager.abortActiveSession()) {
|
Slog.e(TAG, "Failed to abort APEXd session");
|
} else {
|
Slog.e(TAG,
|
"Successfully aborted apexd session. Rebooting device in order to revert "
|
+ "to the previous state of APEXd.");
|
mPowerManager.reboot(null);
|
}
|
return;
|
}
|
|
session.setStagedSessionApplied();
|
if (hasApex) {
|
mApexManager.markStagedSessionSuccessful(session.sessionId);
|
}
|
}
|
|
private List<String> findAPKsInDir(File stageDir) {
|
List<String> ret = new ArrayList<>();
|
if (stageDir != null && stageDir.exists()) {
|
for (File file : stageDir.listFiles()) {
|
if (file.getAbsolutePath().toLowerCase().endsWith(".apk")) {
|
ret.add(file.getAbsolutePath());
|
}
|
}
|
}
|
return ret;
|
}
|
|
private PackageInstallerSession createAndWriteApkSession(
|
@NonNull PackageInstallerSession originalSession, boolean preReboot) {
|
if (originalSession.stageDir == null) {
|
Slog.wtf(TAG, "Attempting to install a staged APK session with no staging dir");
|
return null;
|
}
|
List<String> apkFilePaths = findAPKsInDir(originalSession.stageDir);
|
if (apkFilePaths.isEmpty()) {
|
Slog.w(TAG, "Can't find staged APK in " + originalSession.stageDir.getAbsolutePath());
|
return null;
|
}
|
|
PackageInstaller.SessionParams params = originalSession.params.copy();
|
params.isStaged = false;
|
params.installFlags |= PackageManager.INSTALL_STAGED;
|
// TODO(b/129744602): use the userid from the original session.
|
if (preReboot) {
|
params.installFlags &= ~PackageManager.INSTALL_ENABLE_ROLLBACK;
|
params.installFlags |= PackageManager.INSTALL_DRY_RUN;
|
} else {
|
params.installFlags |= PackageManager.INSTALL_DISABLE_VERIFICATION;
|
}
|
try {
|
int apkSessionId = mPi.createSession(
|
params, originalSession.getInstallerPackageName(),
|
0 /* UserHandle.SYSTEM */);
|
PackageInstallerSession apkSession = mPi.getSession(apkSessionId);
|
apkSession.open();
|
for (String apkFilePath : apkFilePaths) {
|
File apkFile = new File(apkFilePath);
|
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(apkFile,
|
ParcelFileDescriptor.MODE_READ_ONLY);
|
long sizeBytes = (pfd == null) ? -1 : pfd.getStatSize();
|
if (sizeBytes < 0) {
|
Slog.e(TAG, "Unable to get size of: " + apkFilePath);
|
return null;
|
}
|
apkSession.write(apkFile.getName(), 0, sizeBytes, pfd);
|
}
|
return apkSession;
|
} catch (IOException | ParcelableException e) {
|
Slog.e(TAG, "Failure to install APK staged session " + originalSession.sessionId, e);
|
return null;
|
}
|
}
|
|
private boolean commitApkSession(@NonNull PackageInstallerSession apkSession,
|
int originalSessionId, boolean preReboot) {
|
|
if (!preReboot) {
|
if ((apkSession.params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
|
// If rollback is available for this session, notify the rollback
|
// manager of the apk session so it can properly enable rollback.
|
final IRollbackManager rm = IRollbackManager.Stub.asInterface(
|
ServiceManager.getService(Context.ROLLBACK_SERVICE));
|
try {
|
rm.notifyStagedApkSession(originalSessionId, apkSession.sessionId);
|
} catch (RemoteException re) {
|
// Cannot happen, the rollback manager is in the same process.
|
}
|
}
|
}
|
|
final LocalIntentReceiver receiver = new LocalIntentReceiver();
|
apkSession.commit(receiver.getIntentSender(), false);
|
final Intent result = receiver.getResult();
|
final int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
|
PackageInstaller.STATUS_FAILURE);
|
if (status == PackageInstaller.STATUS_SUCCESS) {
|
return true;
|
}
|
Slog.e(TAG, "Failure to install APK staged session " + originalSessionId + " ["
|
+ result.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + "]");
|
return false;
|
}
|
|
private boolean installApksInSession(@NonNull PackageInstallerSession session,
|
boolean preReboot) {
|
if (!session.isMultiPackage() && !isApexSession(session)) {
|
// APK single-packaged staged session. Do a regular install.
|
PackageInstallerSession apkSession = createAndWriteApkSession(session, preReboot);
|
if (apkSession == null) {
|
return false;
|
}
|
return commitApkSession(apkSession, session.sessionId, preReboot);
|
} else if (session.isMultiPackage()) {
|
// For multi-package staged sessions containing APKs, we identify which child sessions
|
// contain an APK, and with those then create a new multi-package group of sessions,
|
// carrying over all the session parameters and unmarking them as staged. On commit the
|
// sessions will be installed atomically.
|
List<PackageInstallerSession> childSessions;
|
synchronized (mStagedSessions) {
|
childSessions =
|
Arrays.stream(session.getChildSessionIds())
|
// Retrieve cached sessions matching ids.
|
.mapToObj(i -> mStagedSessions.get(i))
|
// Filter only the ones containing APKs.s
|
.filter(childSession -> !isApexSession(childSession))
|
.collect(Collectors.toList());
|
}
|
if (childSessions.isEmpty()) {
|
// APEX-only multi-package staged session, nothing to do.
|
return true;
|
}
|
PackageInstaller.SessionParams params = session.params.copy();
|
params.isStaged = false;
|
if (preReboot) {
|
params.installFlags &= ~PackageManager.INSTALL_ENABLE_ROLLBACK;
|
}
|
// TODO(b/129744602): use the userid from the original session.
|
int apkParentSessionId = mPi.createSession(
|
params, session.getInstallerPackageName(),
|
0 /* UserHandle.SYSTEM */);
|
PackageInstallerSession apkParentSession = mPi.getSession(apkParentSessionId);
|
try {
|
apkParentSession.open();
|
} catch (IOException e) {
|
Slog.e(TAG, "Unable to prepare multi-package session for staged session "
|
+ session.sessionId);
|
return false;
|
}
|
|
for (PackageInstallerSession sessionToClone : childSessions) {
|
PackageInstallerSession apkChildSession =
|
createAndWriteApkSession(sessionToClone, preReboot);
|
if (apkChildSession == null) {
|
return false;
|
}
|
try {
|
apkParentSession.addChildSessionId(apkChildSession.sessionId);
|
} catch (IllegalStateException e) {
|
Slog.e(TAG, "Failed to add a child session for installing the APK files", e);
|
return false;
|
}
|
}
|
return commitApkSession(apkParentSession, session.sessionId, preReboot);
|
}
|
// APEX single-package staged session, nothing to do.
|
return true;
|
}
|
|
void commitSession(@NonNull PackageInstallerSession session) {
|
updateStoredSession(session);
|
mBgHandler.post(() -> preRebootVerification(session));
|
}
|
|
@Nullable
|
PackageInstallerSession getActiveSession() {
|
synchronized (mStagedSessions) {
|
for (int i = 0; i < mStagedSessions.size(); i++) {
|
final PackageInstallerSession session = mStagedSessions.valueAt(i);
|
if (!session.isCommitted()) {
|
continue;
|
}
|
if (session.hasParentSessionId()) {
|
// Staging manager will finalize only parent session. Ignore child sessions
|
// picking the active.
|
continue;
|
}
|
if (!session.isStagedSessionApplied() && !session.isStagedSessionFailed()) {
|
return session;
|
}
|
}
|
}
|
return null;
|
}
|
|
void createSession(@NonNull PackageInstallerSession sessionInfo) {
|
synchronized (mStagedSessions) {
|
mStagedSessions.append(sessionInfo.sessionId, sessionInfo);
|
}
|
}
|
|
void abortSession(@NonNull PackageInstallerSession session) {
|
synchronized (mStagedSessions) {
|
mStagedSessions.remove(session.sessionId);
|
}
|
}
|
|
void abortCommittedSession(@NonNull PackageInstallerSession session) {
|
if (session.isStagedSessionApplied()) {
|
Slog.w(TAG, "Cannot abort applied session : " + session.sessionId);
|
return;
|
}
|
abortSession(session);
|
|
boolean hasApex = sessionContainsApex(session);
|
if (hasApex) {
|
ApexSessionInfo apexSession = mApexManager.getStagedSessionInfo(session.sessionId);
|
if (apexSession == null || isApexSessionFinalized(apexSession)) {
|
Slog.w(TAG,
|
"Cannot abort session because it is not active or APEXD is not reachable");
|
return;
|
}
|
mApexManager.abortActiveSession();
|
}
|
}
|
|
private boolean isApexSessionFinalized(ApexSessionInfo session) {
|
/* checking if the session is in a final state, i.e., not active anymore */
|
return session.isUnknown || session.isActivationFailed || session.isSuccess
|
|| session.isRolledBack;
|
}
|
|
private static boolean isApexSessionFailed(ApexSessionInfo apexSessionInfo) {
|
// isRollbackInProgress is included to cover the scenario, when a device is rebooted in
|
// during the rollback, and apexd fails to resume the rollback after reboot.
|
return apexSessionInfo.isActivationFailed || apexSessionInfo.isUnknown
|
|| apexSessionInfo.isRolledBack || apexSessionInfo.isRollbackInProgress
|
|| apexSessionInfo.isRollbackFailed;
|
}
|
|
@GuardedBy("mStagedSessions")
|
private boolean isMultiPackageSessionComplete(@NonNull PackageInstallerSession session) {
|
// This method assumes that the argument is either a parent session of a multi-package
|
// i.e. isMultiPackage() returns true, or that it is a child session, i.e.
|
// hasParentSessionId() returns true.
|
if (session.isMultiPackage()) {
|
// Parent session of a multi-package group. Check that we restored all the children.
|
for (int childSession : session.getChildSessionIds()) {
|
if (mStagedSessions.get(childSession) == null) {
|
return false;
|
}
|
}
|
return true;
|
}
|
if (session.hasParentSessionId()) {
|
PackageInstallerSession parent = mStagedSessions.get(session.getParentSessionId());
|
if (parent == null) {
|
return false;
|
}
|
return isMultiPackageSessionComplete(parent);
|
}
|
Slog.wtf(TAG, "Attempting to restore an invalid multi-package session.");
|
return false;
|
}
|
|
void restoreSession(@NonNull PackageInstallerSession session) {
|
PackageInstallerSession sessionToResume = session;
|
synchronized (mStagedSessions) {
|
mStagedSessions.append(session.sessionId, session);
|
// For multi-package sessions, we don't know in which order they will be restored. We
|
// need to wait until we have restored all the session in a group before restoring them.
|
if (session.isMultiPackage() || session.hasParentSessionId()) {
|
if (!isMultiPackageSessionComplete(session)) {
|
// Still haven't recovered all sessions of the group, return.
|
return;
|
}
|
// Group recovered, find the parent if necessary and resume the installation.
|
if (session.hasParentSessionId()) {
|
sessionToResume = mStagedSessions.get(session.getParentSessionId());
|
}
|
}
|
}
|
checkStateAndResume(sessionToResume);
|
}
|
|
private void checkStateAndResume(@NonNull PackageInstallerSession session) {
|
if (!session.isCommitted()) {
|
// Session hasn't been committed yet, ignore.
|
return;
|
}
|
// Check the state of the session and decide what to do next.
|
if (session.isStagedSessionFailed() || session.isStagedSessionApplied()) {
|
// Final states, nothing to do.
|
return;
|
}
|
if (!session.isStagedSessionReady()) {
|
// The framework got restarted before the pre-reboot verification could complete,
|
// restart the verification.
|
mBgHandler.post(() -> preRebootVerification(session));
|
} else {
|
// Session had already being marked ready. Start the checks to verify if there is any
|
// follow-up work.
|
resumeSession(session);
|
}
|
}
|
|
private static class LocalIntentReceiver {
|
private final LinkedBlockingQueue<Intent> mResult = new LinkedBlockingQueue<>();
|
|
private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
|
@Override
|
public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
|
IIntentReceiver finishedReceiver, String requiredPermission,
|
Bundle options) {
|
try {
|
mResult.offer(intent, 5, TimeUnit.SECONDS);
|
} catch (InterruptedException e) {
|
throw new RuntimeException(e);
|
}
|
}
|
};
|
|
public IntentSender getIntentSender() {
|
return new IntentSender((IIntentSender) mLocalSender);
|
}
|
|
public Intent getResult() {
|
try {
|
return mResult.take();
|
} catch (InterruptedException e) {
|
throw new RuntimeException(e);
|
}
|
}
|
}
|
}
|