Merge "[2/N] implementation of verifier controller and status tracker" into main
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index b1b1637..34d939b 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -126,6 +126,7 @@
import com.android.server.SystemServiceManager;
import com.android.server.pm.pkg.PackageStateInternal;
import com.android.server.pm.utils.RequestThrottle;
+import com.android.server.pm.verify.pkg.VerifierController;
import libcore.io.IoUtils;
@@ -213,6 +214,7 @@
private final StagingManager mStagingManager;
private AppOpsManager mAppOps;
+ private final VerifierController mVerifierController;
private final HandlerThread mInstallThread;
private final Handler mInstallHandler;
@@ -325,6 +327,7 @@
mGentleUpdateHelper = new GentleUpdateHelper(
context, mInstallThread.getLooper(), new AppStateHelper(context));
mPackageArchiver = new PackageArchiver(mContext, mPm);
+ mVerifierController = new VerifierController(mContext, mInstallHandler);
LocalServices.getService(SystemServiceManager.class).startService(
new Lifecycle(context, this));
@@ -521,7 +524,8 @@
try {
session = PackageInstallerSession.readFromXml(in, mInternalCallback,
mContext, mPm, mInstallThread.getLooper(), mStagingManager,
- mSessionsDir, this, mSilentUpdatePolicy);
+ mSessionsDir, this, mSilentUpdatePolicy,
+ mVerifierController);
} catch (Exception e) {
Slog.e(TAG, "Could not read session", e);
continue;
@@ -1037,7 +1041,8 @@
mSilentUpdatePolicy, mInstallThread.getLooper(), mStagingManager, sessionId,
userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid,
null, null, false, false, false, false, null, SessionInfo.INVALID_ID,
- false, false, false, PackageManager.INSTALL_UNKNOWN, "", null);
+ false, false, false, PackageManager.INSTALL_UNKNOWN, "", null,
+ mVerifierController);
synchronized (mSessions) {
mSessions.put(sessionId, session);
@@ -1047,6 +1052,7 @@
mCallbacks.notifySessionCreated(session.sessionId, session.userId);
mSettingsWriteRequest.schedule();
+
if (LOGD) {
Slog.d(TAG, "Created session id=" + sessionId + " staged=" + params.isStaged);
}
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index ff8a69d..c581622 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -38,6 +38,7 @@
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.content.pm.verify.pkg.VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN;
import static android.os.Process.INVALID_UID;
import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE;
import static android.system.OsConstants.O_CREAT;
@@ -87,6 +88,7 @@
import android.content.pm.DataLoaderParams;
import android.content.pm.DataLoaderParamsParcel;
import android.content.pm.FileSystemControlParcel;
+import android.content.pm.Flags;
import android.content.pm.IDataLoader;
import android.content.pm.IDataLoaderStatusListener;
import android.content.pm.IOnChecksumsReadyListener;
@@ -108,6 +110,7 @@
import android.content.pm.PackageManager.PackageInfoFlags;
import android.content.pm.PackageManagerInternal;
import android.content.pm.SigningDetails;
+import android.content.pm.SigningInfo;
import android.content.pm.dex.DexMetadataHelper;
import android.content.pm.parsing.ApkLite;
import android.content.pm.parsing.ApkLiteParseUtils;
@@ -115,6 +118,7 @@
import android.content.pm.parsing.result.ParseResult;
import android.content.pm.parsing.result.ParseTypeImpl;
import android.content.pm.verify.domain.DomainSet;
+import android.content.pm.verify.pkg.VerificationStatus;
import android.content.res.ApkAssets;
import android.content.res.AssetManager;
import android.content.res.Configuration;
@@ -122,6 +126,7 @@
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.icu.util.ULocale;
+import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -133,6 +138,7 @@
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.ParcelableException;
+import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteException;
import android.os.RevocableFileDescriptor;
@@ -190,6 +196,7 @@
import com.android.server.pm.dex.DexManager;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.verify.pkg.VerifierController;
import libcore.io.IoUtils;
import libcore.util.EmptyArray;
@@ -218,6 +225,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
+import java.util.function.Supplier;
public class PackageInstallerSession extends IPackageInstallerSession.Stub {
private static final String TAG = "PackageInstallerSession";
@@ -404,6 +412,7 @@
* Note all calls must be done outside {@link #mLock} to prevent lock inversion.
*/
private final StagingManager mStagingManager;
+ @NonNull private final VerifierController mVerifierController;
final int sessionId;
final int userId;
@@ -1156,7 +1165,8 @@
boolean prepared, boolean committed, boolean destroyed, boolean sealed,
@Nullable int[] childSessionIds, int parentSessionId, boolean isReady,
boolean isFailed, boolean isApplied, int sessionErrorCode,
- String sessionErrorMessage, DomainSet preVerifiedDomains) {
+ String sessionErrorMessage, DomainSet preVerifiedDomains,
+ @NonNull VerifierController verifierController) {
mCallback = callback;
mContext = context;
mPm = pm;
@@ -1165,6 +1175,7 @@
mSilentUpdatePolicy = silentUpdatePolicy;
mHandler = new Handler(looper, mHandlerCallback);
mStagingManager = stagingManager;
+ mVerifierController = verifierController;
this.sessionId = sessionId;
this.userId = userId;
@@ -1249,6 +1260,14 @@
"Archived installation can only use Streaming System DataLoader.");
}
}
+
+ if (Flags.verificationService()) {
+ // Start binding to the verification service, if not bound already.
+ mVerifierController.bindToVerifierServiceIfNeeded(() -> pm.snapshotComputer(), userId);
+ if (!TextUtils.isEmpty(params.appPackageName)) {
+ mVerifierController.notifyPackageNameAvailable(params.appPackageName);
+ }
+ }
}
PackageInstallerHistoricalSession createHistoricalSession() {
@@ -2821,7 +2840,35 @@
// since installation is in progress.
activate();
}
+ if (Flags.verificationService()) {
+ final Supplier<Computer> snapshotSupplier = mPm::snapshotComputer;
+ if (mVerifierController.isVerifierInstalled(snapshotSupplier, userId)) {
+ // TODO: extract shared library declarations
+ final SigningInfo signingInfo;
+ synchronized (mLock) {
+ signingInfo = new SigningInfo(mSigningDetails);
+ }
+ // Send the request to the verifier and wait for its response before the rest of
+ // the installation can proceed.
+ if (!mVerifierController.startVerificationSession(snapshotSupplier, userId,
+ sessionId, params.appPackageName, Uri.fromFile(stageDir), signingInfo,
+ /* declaredLibraries= */null, /* extensionParams= */ null,
+ new VerifierCallback(), /* retry= */ false)) {
+ // A verifier is installed but cannot be connected. Installation disallowed.
+ onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR,
+ "A verifier agent is available on device but cannot be connected.");
+ }
+ } else {
+ // Verifier is not installed. Let the installation pass for now.
+ resumeVerify();
+ }
+ } else {
+ // New verification feature is not enabled. Proceed to the rest of the verification.
+ resumeVerify();
+ }
+ }
+ private void resumeVerify() {
if (mVerificationInProgress) {
Slog.w(TAG, "Verification is already in progress for session " + sessionId);
return;
@@ -2856,6 +2903,66 @@
}
}
+ /**
+ * Used for the VerifierController to report status back.
+ */
+ public class VerifierCallback {
+ /**
+ * Called by the VerifierController when the connection has failed.
+ */
+ public void onConnectionFailed() {
+ mHandler.post(() -> {
+ onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
+ "A verifier agent is available on device but cannot be connected.");
+ });
+ }
+ /**
+ * Called by the VerifierController when the verification request has timed out.
+ */
+ public void onTimeout() {
+ mHandler.post(() -> {
+ mVerifierController.notifyVerificationTimeout(sessionId);
+ onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
+ "Verification timed out; missing a response from the verifier within the"
+ + " time limit");
+ });
+ }
+ /**
+ * Called by the VerifierController when the verification request has received a complete
+ * response.
+ */
+ public void onVerificationCompleteReceived(@NonNull VerificationStatus statusReceived,
+ @Nullable PersistableBundle extensionResponse) {
+ // TODO: handle extension response
+ mHandler.post(() -> {
+ if (statusReceived.isVerified()) {
+ // Continue with the rest of the verification and installation.
+ resumeVerify();
+ } else {
+ StringBuilder sb = new StringBuilder("Verifier rejected the installation");
+ if (!TextUtils.isEmpty(statusReceived.getFailureMessage())) {
+ sb.append(" with message: ").append(statusReceived.getFailureMessage());
+ }
+ onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE,
+ sb.toString());
+ }
+ });
+ }
+ /**
+ * Called by the VerifierController when the verification request has received an incomplete
+ * response.
+ */
+ public void onVerificationIncompleteReceived(int incompleteReason) {
+ mHandler.post(() -> {
+ if (incompleteReason == VERIFICATION_INCOMPLETE_UNKNOWN) {
+ // TODO: change this to a user confirmation and handle other incomplete reasons
+ onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR,
+ "Verification cannot be completed for unknown reasons.");
+ }
+ });
+ }
+ }
+
private IntentSender getRemoteStatusReceiver() {
synchronized (mLock) {
return mRemoteStatusReceiver;
@@ -5369,6 +5476,14 @@
}
} catch (InstallerException ignored) {
}
+ if (Flags.verificationService()
+ && !TextUtils.isEmpty(params.appPackageName)
+ && !isCommitted()) {
+ // Only notify for the cancellation if the verification request has not
+ // been sent out, which happens right after commit() is called.
+ mVerifierController.notifyVerificationCancelled(
+ params.appPackageName);
+ }
}
void dump(IndentingPrintWriter pw) {
@@ -5768,7 +5883,8 @@
@NonNull PackageManagerService pm, Looper installerThread,
@NonNull StagingManager stagingManager, @NonNull File sessionsDir,
@NonNull PackageSessionProvider sessionProvider,
- @NonNull SilentUpdatePolicy silentUpdatePolicy)
+ @NonNull SilentUpdatePolicy silentUpdatePolicy,
+ @NonNull VerifierController verifierController)
throws IOException, XmlPullParserException {
final int sessionId = in.getAttributeInt(null, ATTR_SESSION_ID);
final int userId = in.getAttributeInt(null, ATTR_USER_ID);
@@ -5972,6 +6088,6 @@
installerUid, installSource, params, createdMillis, committedMillis, stageDir,
stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed,
childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied,
- sessionErrorCode, sessionErrorMessage, preVerifiedDomains);
+ sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController);
}
}
diff --git a/services/core/java/com/android/server/pm/verify/pkg/VerificationStatusTracker.java b/services/core/java/com/android/server/pm/verify/pkg/VerificationStatusTracker.java
new file mode 100644
index 0000000..db747f9
--- /dev/null
+++ b/services/core/java/com/android/server/pm/verify/pkg/VerificationStatusTracker.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 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.verify.pkg;
+
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * This class keeps record of the current timeout status of a verification request.
+ */
+public final class VerificationStatusTracker {
+ private final @CurrentTimeMillisLong long mStartTime;
+ private @CurrentTimeMillisLong long mTimeoutTime;
+ private final @CurrentTimeMillisLong long mMaxTimeoutTime;
+ @NonNull
+ private final VerifierController.Injector mInjector;
+ // Record the package name associated with the verification result
+ @NonNull
+ private final String mPackageName;
+
+ /**
+ * By default, the timeout time is the default timeout duration plus the current time (when
+ * the timer starts for a verification request). Both the default timeout time and the max
+ * timeout time cannot be changed after the timer has started, but the actual timeout time
+ * can be extended via {@link #extendTimeRemaining} to the maximum allowed.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
+ public VerificationStatusTracker(@NonNull String packageName,
+ long defaultTimeoutMillis, long maxExtendedTimeoutMillis,
+ @NonNull VerifierController.Injector injector) {
+ mPackageName = packageName;
+ mStartTime = injector.getCurrentTimeMillis();
+ mTimeoutTime = mStartTime + defaultTimeoutMillis;
+ mMaxTimeoutTime = mStartTime + maxExtendedTimeoutMillis;
+ mInjector = injector;
+ }
+
+ /**
+ * Used by the controller to inform the verifier agent about the timestamp when the verification
+ * request will timeout.
+ */
+ public @CurrentTimeMillisLong long getTimeoutTime() {
+ return mTimeoutTime;
+ }
+
+ /**
+ * Used by the controller to decide when to check for timeout again.
+ * @return 0 if the timeout time has been reached, otherwise the remaining time in milliseconds
+ * before the timeout is reached.
+ */
+ public @CurrentTimeMillisLong long getRemainingTime() {
+ final long remainingTime = mTimeoutTime - mInjector.getCurrentTimeMillis();
+ if (remainingTime < 0) {
+ return 0;
+ }
+ return remainingTime;
+ }
+
+ /**
+ * Used by the controller to extend the timeout duration of the verification request, upon
+ * receiving the callback from the verifier agent.
+ * @return the amount of time in millis that the timeout has been extended, subject to the max
+ * amount allowed.
+ */
+ public long extendTimeRemaining(@CurrentTimeMillisLong long additionalMs) {
+ if (mTimeoutTime + additionalMs > mMaxTimeoutTime) {
+ additionalMs = mMaxTimeoutTime - mTimeoutTime;
+ }
+ mTimeoutTime += additionalMs;
+ return additionalMs;
+ }
+
+ /**
+ * Used by the controller to get the timeout status of the request.
+ * @return False if the request still has some time left before timeout, otherwise return True.
+ */
+ public boolean isTimeout() {
+ return mInjector.getCurrentTimeMillis() >= mTimeoutTime;
+ }
+
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+}
diff --git a/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java b/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java
new file mode 100644
index 0000000..7eac940
--- /dev/null
+++ b/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2024 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.verify.pkg;
+
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+import static android.os.Process.SYSTEM_UID;
+import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.SigningInfo;
+import android.content.pm.verify.pkg.IVerificationSessionCallback;
+import android.content.pm.verify.pkg.IVerificationSessionInterface;
+import android.content.pm.verify.pkg.IVerifierService;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
+import com.android.server.pm.Computer;
+import com.android.server.pm.PackageInstallerSession;
+import com.android.server.pm.pkg.PackageStateInternal;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+/**
+ * This class manages the bind to the verifier agent installed on the device that implements
+ * {@link android.content.pm.verify.pkg.VerifierService} and handles all its interactions.
+ */
+public class VerifierController {
+ private static final String TAG = "VerifierController";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Configurable maximum amount of time in milliseconds to wait for a verifier to respond to
+ * a verification request.
+ * Flag type: {@code long}
+ * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+ */
+ private static final String PROPERTY_VERIFICATION_REQUEST_TIMEOUT_MILLIS =
+ "verification_request_timeout_millis";
+ // Default duration to wait for a verifier to respond to a verification request.
+ private static final long DEFAULT_VERIFICATION_REQUEST_TIMEOUT_MILLIS =
+ TimeUnit.MINUTES.toMillis(1);
+ /**
+ * Configurable maximum amount of time in milliseconds that the verifier can request to extend
+ * the verification request timeout duration to. This is the maximum amount of time the system
+ * can wait for a request before it times out.
+ * Flag type: {@code long}
+ * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE
+ */
+ private static final String PROPERTY_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS =
+ "max_verification_request_extended_timeout_millis";
+ // Max duration allowed to wait for a verifier to respond to a verification request.
+ private static final long DEFAULT_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS =
+ TimeUnit.MINUTES.toMillis(10);
+ // The maximum amount of time to wait from the moment when the session requires a verification,
+ // till when the request is delivered to the verifier, pending the connection to be established.
+ private static final long CONNECTION_TIMEOUT_SECONDS = 10;
+ // The maximum amount of time to wait before the system unbinds from the verifier.
+ private static final long UNBIND_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(6);
+
+ private final Context mContext;
+ private final Handler mHandler;
+ @Nullable
+ private ServiceConnector<IVerifierService> mRemoteService;
+ @Nullable
+ private ComponentName mRemoteServiceComponentName;
+ @NonNull
+ private Injector mInjector;
+
+ // Repository of active verification sessions and their status, mapping from id to status.
+ @NonNull
+ @GuardedBy("mVerificationStatus")
+ private final SparseArray<VerificationStatusTracker> mVerificationStatus = new SparseArray<>();
+
+ public VerifierController(@NonNull Context context, @NonNull Handler handler) {
+ this(context, handler, new Injector());
+ }
+
+ @VisibleForTesting
+ public VerifierController(@NonNull Context context, @NonNull Handler handler,
+ @NonNull Injector injector) {
+ mContext = context;
+ mHandler = handler;
+ mInjector = injector;
+ }
+
+ /**
+ * Used by the installation session to check if a verifier is installed.
+ */
+ public boolean isVerifierInstalled(Supplier<Computer> snapshotSupplier, int userId) {
+ if (isVerifierConnected()) {
+ // Verifier is connected or is being connected, so it must be installed.
+ return true;
+ }
+ // Verifier has been disconnected, or it hasn't been connected. Check if it's installed.
+ return mInjector.isVerifierInstalled(snapshotSupplier.get(), userId);
+ }
+
+ /**
+ * Called to start querying and binding to a qualified verifier agent.
+ *
+ * @return False if a qualified verifier agent doesn't exist on device, so that the system can
+ * handle this situation immediately after the call.
+ * <p>
+ * Notice that since this is an async call, even if this method returns true, it doesn't
+ * necessarily mean that the binding connection was successful. However, the system will only
+ * try to bind once per installation session, so that it doesn't waste resource by repeatedly
+ * trying to bind if the verifier agent isn't available during a short amount of time.
+ * <p>
+ * If the verifier agent exists but cannot be started for some reason, all the notify* methods
+ * in this class will fail asynchronously and quietly. The system will learn about the failure
+ * after receiving the failure from
+ * {@link PackageInstallerSession.VerifierCallback#onConnectionFailed}.
+ */
+ public boolean bindToVerifierServiceIfNeeded(Supplier<Computer> snapshotSupplier, int userId) {
+ if (DEBUG) {
+ Slog.i(TAG, "Requesting to bind to the verifier service.");
+ }
+ if (mRemoteService != null) {
+ // Already connected
+ if (DEBUG) {
+ Slog.i(TAG, "Verifier service is already connected.");
+ }
+ return true;
+ }
+ Pair<ServiceConnector<IVerifierService>, ComponentName> result =
+ mInjector.getRemoteService(snapshotSupplier.get(), mContext, userId, mHandler);
+ if (result == null || result.first == null) {
+ if (DEBUG) {
+ Slog.i(TAG, "Unable to find a qualified verifier.");
+ }
+ return false;
+ }
+ mRemoteService = result.first;
+ mRemoteServiceComponentName = result.second;
+ if (DEBUG) {
+ Slog.i(TAG, "Connecting to a qualified verifier: " + mRemoteServiceComponentName);
+ }
+ mRemoteService.setServiceLifecycleCallbacks(
+ new ServiceConnector.ServiceLifecycleCallbacks<>() {
+ @Override
+ public void onConnected(@NonNull IVerifierService service) {
+ Slog.i(TAG, "Verifier " + mRemoteServiceComponentName + " is connected");
+ }
+
+ @Override
+ public void onDisconnected(@NonNull IVerifierService service) {
+ Slog.w(TAG,
+ "Verifier " + mRemoteServiceComponentName + " is disconnected");
+ destroy();
+ }
+
+ @Override
+ public void onBinderDied() {
+ Slog.w(TAG, "Verifier " + mRemoteServiceComponentName + " has died");
+ destroy();
+ }
+
+ private void destroy() {
+ if (isVerifierConnected()) {
+ mRemoteService.unbind();
+ mRemoteService = null;
+ mRemoteServiceComponentName = null;
+ }
+ }
+ });
+ AndroidFuture<IVerifierService> unusedFuture = mRemoteService.connect();
+ return true;
+ }
+
+ private boolean isVerifierConnected() {
+ return mRemoteService != null && mRemoteServiceComponentName != null;
+ }
+
+ /**
+ * Called to notify the bound verifier agent that a package name is available and will soon be
+ * requested for verification.
+ */
+ public void notifyPackageNameAvailable(@NonNull String packageName) {
+ if (!isVerifierConnected()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Verifier is not connected. Not notifying package name available");
+ }
+ return;
+ }
+ // Best effort. We don't check for the result.
+ mRemoteService.run(service -> {
+ if (DEBUG) {
+ Slog.i(TAG, "Notifying package name available for " + packageName);
+ }
+ service.onPackageNameAvailable(packageName);
+ });
+ }
+
+ /**
+ * Called to notify the bound verifier agent that a package previously notified via
+ * {@link android.content.pm.verify.pkg.VerifierService#onPackageNameAvailable(String)}
+ * will no longer be requested for verification, possibly because the installation is canceled.
+ */
+ public void notifyVerificationCancelled(@NonNull String packageName) {
+ if (!isVerifierConnected()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Verifier is not connected. Not notifying verification cancelled");
+ }
+ return;
+ }
+ // Best effort. We don't check for the result.
+ mRemoteService.run(service -> {
+ if (DEBUG) {
+ Slog.i(TAG, "Notifying verification cancelled for " + packageName);
+ }
+ service.onVerificationCancelled(packageName);
+ });
+ }
+
+ /**
+ * Called to notify the bound verifier agent that a package that's pending installation needs
+ * to be verified right now.
+ * <p>The verification request must be sent to the verifier as soon as the verifier is
+ * connected. If the connection cannot be made within {@link #CONNECTION_TIMEOUT_SECONDS}</p>
+ * of when the request is sent out, we consider the verification to be failed and notify the
+ * installation session.</p>
+ * <p>If a response is not returned from the verifier agent within a timeout duration from the
+ * time the request is sent to the verifier, the verification will be considered a failure.</p>
+ *
+ * @param retry whether this request is for retrying a previously incomplete verification.
+ */
+ public boolean startVerificationSession(Supplier<Computer> snapshotSupplier, int userId,
+ int installationSessionId, String packageName,
+ Uri stagedPackageUri, SigningInfo signingInfo,
+ List<SharedLibraryInfo> declaredLibraries,
+ PersistableBundle extensionParams, PackageInstallerSession.VerifierCallback callback,
+ boolean retry) {
+ // Try connecting to the verifier if not already connected
+ if (!bindToVerifierServiceIfNeeded(snapshotSupplier, userId)) {
+ return false;
+ }
+ if (!isVerifierConnected()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Verifier is not connected. Not notifying verification required");
+ }
+ // Normally this should not happen because we just tried to bind. But if the verifier
+ // just crashed or just became unavailable, we should notify the installation session so
+ // it can finish with a verification failure.
+ return false;
+ }
+ // For now, the verification id is the same as the installation session id.
+ final int verificationId = installationSessionId;
+ final VerificationSession session = new VerificationSession(
+ /* id= */ verificationId,
+ /* installSessionId= */ installationSessionId,
+ packageName, stagedPackageUri, signingInfo, declaredLibraries, extensionParams,
+ new VerificationSessionInterface(),
+ new VerificationSessionCallback(callback));
+ AndroidFuture<Void> unusedFuture = mRemoteService.post(service -> {
+ if (!retry) {
+ if (DEBUG) {
+ Slog.i(TAG, "Notifying verification required for session " + verificationId);
+ }
+ service.onVerificationRequired(session);
+ } else {
+ if (DEBUG) {
+ Slog.i(TAG, "Notifying verification retry for session " + verificationId);
+ }
+ service.onVerificationRetry(session);
+ }
+ }).orTimeout(CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS).whenComplete((res, err) -> {
+ if (err != null) {
+ Slog.e(TAG, "Error notifying verification request for session " + verificationId,
+ err);
+ // Notify the installation session so it can finish with verification failure.
+ callback.onConnectionFailed();
+ }
+ });
+ // Keep track of the session status with the ID. Start counting down the session timeout.
+ final long defaultTimeoutMillis = mInjector.getVerificationRequestTimeoutMillis();
+ final long maxExtendedTimeoutMillis = mInjector.getMaxVerificationExtendedTimeoutMillis();
+ final VerificationStatusTracker tracker = new VerificationStatusTracker(
+ packageName, defaultTimeoutMillis, maxExtendedTimeoutMillis, mInjector);
+ synchronized (mVerificationStatus) {
+ mVerificationStatus.put(verificationId, tracker);
+ }
+ startTimeoutCountdown(verificationId, tracker, callback, defaultTimeoutMillis);
+ return true;
+ }
+
+ private void startTimeoutCountdown(int verificationId, VerificationStatusTracker tracker,
+ PackageInstallerSession.VerifierCallback callback, long delayMillis) {
+ mHandler.postDelayed(() -> {
+ if (DEBUG) {
+ Slog.i(TAG, "Checking request timeout for " + verificationId);
+ }
+ if (!tracker.isTimeout()) {
+ if (DEBUG) {
+ Slog.i(TAG, "Timeout is not met for " + verificationId + "; check later.");
+ }
+ // If the current session is not timed out yet, check again later.
+ startTimeoutCountdown(verificationId, tracker, callback,
+ /* delayMillis= */ tracker.getRemainingTime());
+ } else {
+ if (DEBUG) {
+ Slog.i(TAG, "Request " + verificationId + " has timed out.");
+ }
+ // The request has timed out. Notify the installation session.
+ callback.onTimeout();
+ // Remove status tracking and stop the timeout countdown
+ removeStatusTracker(verificationId);
+ }
+ }, /* token= */ tracker, delayMillis);
+ }
+
+ /**
+ * Called to notify the bound verifier agent that a verification request has timed out.
+ */
+ public void notifyVerificationTimeout(int verificationId) {
+ if (!isVerifierConnected()) {
+ if (DEBUG) {
+ Slog.i(TAG,
+ "Verifier is not connected. Not notifying timeout for " + verificationId);
+ }
+ return;
+ }
+ AndroidFuture<Void> unusedFuture = mRemoteService.post(service -> {
+ if (DEBUG) {
+ Slog.i(TAG, "Notifying timeout for " + verificationId);
+ }
+ service.onVerificationTimeout(verificationId);
+ }).whenComplete((res, err) -> {
+ if (err != null) {
+ Slog.e(TAG, "Error notifying VerificationTimeout for session "
+ + verificationId, (Throwable) err);
+ }
+ });
+ }
+
+ /**
+ * Remove a status tracker after it's no longer needed.
+ */
+ private void removeStatusTracker(int verificationId) {
+ if (DEBUG) {
+ Slog.i(TAG, "Removing status tracking for verification " + verificationId);
+ }
+ synchronized (mVerificationStatus) {
+ VerificationStatusTracker tracker = mVerificationStatus.removeReturnOld(verificationId);
+ // Cancel the timeout counters if there's any
+ if (tracker != null) {
+ mInjector.stopTimeoutCountdown(mHandler, tracker);
+ }
+ }
+ }
+
+ @RequiresPermission(Manifest.permission.VERIFICATION_AGENT)
+ private void checkCallerPermission() {
+ // TODO: think of a better way to test it on non-eng builds
+ if (Build.IS_ENG) {
+ return;
+ }
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.VERIFICATION_AGENT)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("You need the"
+ + " com.android.permission.VERIFICATION_AGENT permission"
+ + " to use VerificationSession APIs.");
+ }
+ }
+
+ // This class handles requests from the remote verifier
+ private class VerificationSessionInterface extends IVerificationSessionInterface.Stub {
+ @Override
+ public long getTimeoutTime(int verificationId) {
+ checkCallerPermission();
+ synchronized (mVerificationStatus) {
+ final VerificationStatusTracker tracker = mVerificationStatus.get(verificationId);
+ if (tracker == null) {
+ throw new IllegalStateException("Verification session " + verificationId
+ + " doesn't exist or has finished");
+ }
+ return tracker.getTimeoutTime();
+ }
+ }
+
+ @Override
+ public long extendTimeRemaining(int verificationId, long additionalMs) {
+ checkCallerPermission();
+ synchronized (mVerificationStatus) {
+ final VerificationStatusTracker tracker = mVerificationStatus.get(verificationId);
+ if (tracker == null) {
+ throw new IllegalStateException("Verification session " + verificationId
+ + " doesn't exist or has finished");
+ }
+ return tracker.extendTimeRemaining(additionalMs);
+ }
+ }
+ }
+
+ private class VerificationSessionCallback extends IVerificationSessionCallback.Stub {
+ private final PackageInstallerSession.VerifierCallback mCallback;
+
+ VerificationSessionCallback(PackageInstallerSession.VerifierCallback callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void reportVerificationIncomplete(int id, int reason) throws RemoteException {
+ checkCallerPermission();
+ final VerificationStatusTracker tracker;
+ synchronized (mVerificationStatus) {
+ tracker = mVerificationStatus.get(id);
+ if (tracker == null) {
+ throw new IllegalStateException("Verification session " + id
+ + " doesn't exist or has finished");
+ }
+ mCallback.onVerificationIncompleteReceived(reason);
+ }
+ // Remove status tracking and stop the timeout countdown
+ removeStatusTracker(id);
+ }
+
+ @Override
+ public void reportVerificationComplete(int id, VerificationStatus verificationStatus)
+ throws RemoteException {
+ reportVerificationCompleteWithExtensionResponse(id, verificationStatus,
+ /* extensionResponse= */ null);
+ }
+
+ @Override
+ public void reportVerificationCompleteWithExtensionResponse(int id,
+ VerificationStatus verificationStatus, PersistableBundle extensionResponse)
+ throws RemoteException {
+ checkCallerPermission();
+ final VerificationStatusTracker tracker;
+ synchronized (mVerificationStatus) {
+ tracker = mVerificationStatus.get(id);
+ if (tracker == null) {
+ throw new IllegalStateException("Verification session " + id
+ + " doesn't exist or has finished");
+ }
+ }
+ mCallback.onVerificationCompleteReceived(verificationStatus, extensionResponse);
+ // Remove status tracking and stop the timeout countdown
+ removeStatusTracker(id);
+ }
+ }
+
+ @VisibleForTesting
+ public static class Injector {
+ /**
+ * Mock this method to inject the remote service to enable unit testing.
+ */
+ @Nullable
+ public Pair<ServiceConnector<IVerifierService>, ComponentName> getRemoteService(
+ @NonNull Computer snapshot, @NonNull Context context, int userId,
+ @NonNull Handler handler) {
+ final ComponentName verifierComponent = resolveVerifierComponentName(snapshot, userId);
+ if (verifierComponent == null) {
+ return null;
+ }
+ final Intent intent = new Intent(PackageManager.ACTION_VERIFY_PACKAGE);
+ intent.setComponent(verifierComponent);
+ return new Pair<>(new ServiceConnector.Impl<IVerifierService>(
+ context, intent, Context.BIND_AUTO_CREATE, userId,
+ IVerifierService.Stub::asInterface) {
+ @Override
+ protected Handler getJobHandler() {
+ return handler;
+ }
+
+ @Override
+ protected long getRequestTimeoutMs() {
+ return getVerificationRequestTimeoutMillis();
+ }
+
+ @Override
+ protected long getAutoDisconnectTimeoutMs() {
+ return UNBIND_TIMEOUT_MILLIS;
+ }
+ }, verifierComponent);
+ }
+
+ /**
+ * Check if a verifier is installed on this device.
+ */
+ public boolean isVerifierInstalled(Computer snapshot, int userId) {
+ return resolveVerifierComponentName(snapshot, userId) != null;
+ }
+
+ /**
+ * Find the ComponentName of the verifier service agent, using the intent action.
+ * If multiple qualified verifier services are present, the one with the highest intent
+ * filter priority will be chosen.
+ */
+ private static @Nullable ComponentName resolveVerifierComponentName(Computer snapshot,
+ int userId) {
+ final Intent intent = new Intent(PackageManager.ACTION_VERIFY_PACKAGE);
+ final int resolveFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE;
+ final List<ResolveInfo> matchedServices = snapshot.queryIntentServicesInternal(
+ intent, null,
+ resolveFlags, userId, SYSTEM_UID, Process.INVALID_PID,
+ /*includeInstantApps*/ false, /*resolveForStart*/ false);
+ if (matchedServices.isEmpty()) {
+ Slog.w(TAG,
+ "Failed to find any matching verifier service agent");
+ return null;
+ }
+ ResolveInfo best = null;
+ int numMatchedServices = matchedServices.size();
+ for (int i = 0; i < numMatchedServices; i++) {
+ ResolveInfo cur = matchedServices.get(i);
+ if (!isQualifiedVerifier(snapshot, cur, userId)) {
+ continue;
+ }
+ if (best == null || cur.priority > best.priority) {
+ best = cur;
+ }
+ }
+ if (best != null) {
+ Slog.i(TAG, "Found verifier service agent: "
+ + best.getComponentInfo().getComponentName().toShortString());
+ return best.getComponentInfo().getComponentName();
+ }
+ Slog.w(TAG, "Didn't find any qualified verifier service agent.");
+ return null;
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private static boolean isQualifiedVerifier(Computer snapshot, ResolveInfo ri, int userId) {
+ // Basic null checks
+ if (ri.getComponentInfo() == null) {
+ return false;
+ }
+ final ApplicationInfo applicationInfo = ri.getComponentInfo().applicationInfo;
+ if (applicationInfo == null) {
+ return false;
+ }
+ // Check for installed state
+ PackageStateInternal ps = snapshot.getPackageStateInternal(
+ ri.getComponentInfo().packageName, SYSTEM_UID);
+ if (ps == null || !ps.getUserStateOrDefault(userId).isInstalled()) {
+ return false;
+ }
+ // Check for enabled state
+ if (!snapshot.isComponentEffectivelyEnabled(ri.getComponentInfo(),
+ UserHandle.of(userId))) {
+ return false;
+ }
+ // Allow binding to a non-privileged app on an ENG build
+ // TODO: think of a better way to test it on non-eng builds
+ if (Build.IS_ENG) {
+ return true;
+ }
+ // Check if the app is platform-signed or is privileged
+ if (!applicationInfo.isSignedWithPlatformKey() && !applicationInfo.isPrivilegedApp()) {
+ return false;
+ }
+ // Check for permission
+ return (snapshot.checkUidPermission(
+ android.Manifest.permission.VERIFICATION_AGENT, applicationInfo.uid)
+ != PackageManager.PERMISSION_GRANTED);
+ }
+
+ /**
+ * This is added so we can mock timeouts in the unit tests.
+ */
+ public long getCurrentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * This is added so that we don't need to mock Handler.removeCallbacksAndEqualMessages
+ * which is final.
+ */
+ public void stopTimeoutCountdown(Handler handler, Object token) {
+ handler.removeCallbacksAndEqualMessages(token);
+ }
+
+ /**
+ * This is added so that we can mock the verification request timeout duration without
+ * calling into DeviceConfig.
+ */
+ public long getVerificationRequestTimeoutMillis() {
+ return getVerificationRequestTimeoutMillisFromDeviceConfig();
+ }
+
+ /**
+ * This is added so that we can mock the maximum request timeout duration without
+ * calling into DeviceConfig.
+ */
+ public long getMaxVerificationExtendedTimeoutMillis() {
+ return getMaxVerificationExtendedTimeoutMillisFromDeviceConfig();
+ }
+
+ private static long getVerificationRequestTimeoutMillisFromDeviceConfig() {
+ return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+ PROPERTY_VERIFICATION_REQUEST_TIMEOUT_MILLIS,
+ DEFAULT_VERIFICATION_REQUEST_TIMEOUT_MILLIS);
+ }
+
+ private static long getMaxVerificationExtendedTimeoutMillisFromDeviceConfig() {
+ return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE,
+ PROPERTY_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS,
+ DEFAULT_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS);
+ }
+ }
+}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
index 7aa2ff5..cbca434 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt
@@ -30,6 +30,7 @@
import android.util.Slog
import android.util.Xml
import com.android.internal.os.BackgroundThread
+import com.android.server.pm.verify.pkg.VerifierController
import com.android.server.testutils.whenever
import com.google.common.truth.Truth.assertThat
import libcore.io.IoUtils
@@ -195,7 +196,8 @@
/* isApplied */ false,
/* stagedSessionErrorCode */ PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
/* stagedSessionErrorMessage */ "some error",
- /* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar"))
+ /* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar")),
+ /* VerifierController */ mock(VerifierController::class.java)
)
}
@@ -249,7 +251,8 @@
mock(StagingManager::class.java),
mTmpDir,
mock(PackageSessionProvider::class.java),
- mock(SilentUpdatePolicy::class.java)
+ mock(SilentUpdatePolicy::class.java),
+ mock(VerifierController::class.java)
)
ret.add(session)
} catch (e: Exception) {
@@ -343,4 +346,4 @@
assertThat(expected.mInitiatingPackageName).isEqualTo(actual.mInitiatingPackageName)
assertThat(expected.mOriginatingPackageName).isEqualTo(actual.mOriginatingPackageName)
}
-}
\ No newline at end of file
+}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerificationStatusTrackerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerificationStatusTrackerTest.java
new file mode 100644
index 0000000..fa076db
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerificationStatusTrackerTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 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.verify.pkg;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.TimeUnit;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerificationStatusTrackerTest {
+ private static final String TEST_PACKAGE_NAME = "com.foo";
+ private static final long TEST_REQUEST_START_TIME = 100L;
+ private static final long TEST_TIMEOUT_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(1);
+ private static final long TEST_TIMEOUT_EXTENDED_MILLIS = TimeUnit.MINUTES.toMillis(2);
+ private static final long TEST_MAX_TIMEOUT_DURATION_MILLIS =
+ TimeUnit.MINUTES.toMillis(10);
+
+ @Mock
+ VerifierController.Injector mInjector;
+ private VerificationStatusTracker mTracker;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mInjector.getVerificationRequestTimeoutMillis()).thenReturn(
+ TEST_TIMEOUT_DURATION_MILLIS);
+ when(mInjector.getMaxVerificationExtendedTimeoutMillis()).thenReturn(
+ TEST_MAX_TIMEOUT_DURATION_MILLIS);
+ // Mock time forward as the code continues to check for the current time
+ when(mInjector.getCurrentTimeMillis())
+ .thenReturn(TEST_REQUEST_START_TIME)
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS - 1)
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS)
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_MAX_TIMEOUT_DURATION_MILLIS - 100)
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_MAX_TIMEOUT_DURATION_MILLIS);
+ mTracker = new VerificationStatusTracker(TEST_PACKAGE_NAME, TEST_TIMEOUT_DURATION_MILLIS,
+ TEST_MAX_TIMEOUT_DURATION_MILLIS, mInjector);
+ }
+
+ @Test
+ public void testTimeout() {
+ assertThat(mTracker.getTimeoutTime()).isEqualTo(
+ TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS);
+ // It takes two calls to set the timeout, because the timeout time hasn't been reached for
+ // the first calls
+ assertThat(mTracker.isTimeout()).isFalse();
+ assertThat(mTracker.isTimeout()).isTrue();
+ }
+
+ @Test
+ public void testTimeoutExtended() {
+ assertThat(mTracker.getTimeoutTime()).isEqualTo(
+ TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS);
+ assertThat(mTracker.extendTimeRemaining(TEST_TIMEOUT_EXTENDED_MILLIS))
+ .isEqualTo(TEST_TIMEOUT_EXTENDED_MILLIS);
+ assertThat(mTracker.getTimeoutTime()).isEqualTo(
+ TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS
+ + TEST_TIMEOUT_EXTENDED_MILLIS);
+
+ // It would take 3 calls to set the timeout, because the timeout time hasn't been reached
+ // for the first 2 time checks, but querying the remaining time also does a time check.
+ assertThat(mTracker.isTimeout()).isFalse();
+ assertThat(mTracker.getRemainingTime()).isGreaterThan(0);
+ assertThat(mTracker.isTimeout()).isTrue();
+ assertThat(mTracker.getRemainingTime()).isEqualTo(0);
+ }
+
+ @Test
+ public void testTimeoutExtendedExceedsMax() {
+ assertThat(mTracker.getTimeoutTime()).isEqualTo(
+ TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS);
+ assertThat(mTracker.extendTimeRemaining(TEST_MAX_TIMEOUT_DURATION_MILLIS))
+ .isEqualTo(TEST_MAX_TIMEOUT_DURATION_MILLIS - TEST_TIMEOUT_DURATION_MILLIS);
+ assertThat(mTracker.getTimeoutTime()).isEqualTo(
+ TEST_REQUEST_START_TIME + TEST_MAX_TIMEOUT_DURATION_MILLIS);
+ // It takes 4 calls to set the timeout, because the timeout time hasn't been reached for
+ // the first 3 calls
+ assertThat(mTracker.isTimeout()).isFalse();
+ assertThat(mTracker.isTimeout()).isFalse();
+ assertThat(mTracker.isTimeout()).isFalse();
+ assertThat(mTracker.isTimeout()).isTrue();
+ assertThat(mTracker.getRemainingTime()).isEqualTo(0);
+ }
+}
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java
new file mode 100644
index 0000000..be094b0
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2024 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.verify.pkg;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.expectThrows;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.SharedLibraryInfo;
+import android.content.pm.SigningInfo;
+import android.content.pm.VersionedPackage;
+import android.content.pm.verify.pkg.IVerifierService;
+import android.content.pm.verify.pkg.VerificationSession;
+import android.content.pm.verify.pkg.VerificationStatus;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+import android.util.Pair;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
+import com.android.server.pm.Computer;
+import com.android.server.pm.PackageInstallerSession;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VerifierControllerTest {
+ private static final int TEST_ID = 100;
+ private static final String TEST_PACKAGE_NAME = "com.foo";
+ private static final ComponentName TEST_VERIFIER_COMPONENT_NAME =
+ new ComponentName("com.verifier", "com.verifier.Service");
+ private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test");
+ private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo();
+ private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO1 =
+ new SharedLibraryInfo("sharedLibPath1", TEST_PACKAGE_NAME,
+ Collections.singletonList("path1"), "sharedLib1", 101,
+ SharedLibraryInfo.TYPE_DYNAMIC, new VersionedPackage(TEST_PACKAGE_NAME, 1),
+ null, null, false);
+ private static final SharedLibraryInfo TEST_SHARED_LIBRARY_INFO2 =
+ new SharedLibraryInfo("sharedLibPath2", TEST_PACKAGE_NAME,
+ Collections.singletonList("path2"), "sharedLib2", 102,
+ SharedLibraryInfo.TYPE_DYNAMIC,
+ new VersionedPackage(TEST_PACKAGE_NAME, 2), null, null, false);
+ private static final String TEST_KEY = "test key";
+ private static final String TEST_VALUE = "test value";
+ private static final String TEST_FAILURE_MESSAGE = "verification failed!";
+ private static final long TEST_REQUEST_START_TIME = 0L;
+ private static final long TEST_TIMEOUT_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(1);
+ private static final long TEST_MAX_TIMEOUT_DURATION_MILLIS =
+ TimeUnit.MINUTES.toMillis(10);
+
+ private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>();
+ private final PersistableBundle mTestExtensionParams = new PersistableBundle();
+ @Mock
+ Context mContext;
+ @Mock
+ Handler mHandler;
+ @Mock
+ VerifierController.Injector mInjector;
+ @Mock
+ ServiceConnector<IVerifierService> mMockServiceConnector;
+ @Mock
+ IVerifierService mMockService;
+ @Mock
+ Computer mSnapshot;
+ Supplier<Computer> mSnapshotSupplier = () -> mSnapshot;
+ @Mock
+ PackageInstallerSession.VerifierCallback mSessionCallback;
+
+ private VerifierController mVerifierController;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mInjector.isVerifierInstalled(any(Computer.class), anyInt())).thenReturn(true);
+ when(mInjector.getRemoteService(
+ any(Computer.class), any(Context.class), anyInt(), any(Handler.class)
+ )).thenReturn(new Pair<>(mMockServiceConnector, TEST_VERIFIER_COMPONENT_NAME));
+ when(mInjector.getVerificationRequestTimeoutMillis()).thenReturn(
+ TEST_TIMEOUT_DURATION_MILLIS);
+ when(mInjector.getMaxVerificationExtendedTimeoutMillis()).thenReturn(
+ TEST_MAX_TIMEOUT_DURATION_MILLIS);
+ // Mock time forward as the code continues to check for the current time
+ when(mInjector.getCurrentTimeMillis())
+ .thenReturn(TEST_REQUEST_START_TIME)
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS + 1);
+ when(mMockServiceConnector.post(any(ServiceConnector.VoidJob.class)))
+ .thenAnswer(
+ i -> {
+ ((ServiceConnector.VoidJob) i.getArguments()[0]).run(mMockService);
+ return new AndroidFuture<>();
+ });
+ when(mMockServiceConnector.run(any(ServiceConnector.VoidJob.class)))
+ .thenAnswer(
+ i -> {
+ ((ServiceConnector.VoidJob) i.getArguments()[0]).run(mMockService);
+ return true;
+ });
+
+ mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO1);
+ mTestDeclaredLibraries.add(TEST_SHARED_LIBRARY_INFO2);
+ mTestExtensionParams.putString(TEST_KEY, TEST_VALUE);
+
+ mVerifierController = new VerifierController(mContext, mHandler, mInjector);
+ }
+
+ @Test
+ public void testVerifierNotInstalled() {
+ when(mInjector.isVerifierInstalled(any(Computer.class), anyInt())).thenReturn(false);
+ when(mInjector.getRemoteService(
+ any(Computer.class), any(Context.class), anyInt(), any(Handler.class)
+ )).thenReturn(null);
+ assertThat(mVerifierController.isVerifierInstalled(mSnapshotSupplier, 0)).isFalse();
+ assertThat(mVerifierController.bindToVerifierServiceIfNeeded(mSnapshotSupplier, 0))
+ .isFalse();
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isFalse();
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ true)).isFalse();
+ verifyZeroInteractions(mSessionCallback);
+ }
+
+ @Test
+ public void testRebindService() {
+ assertThat(mVerifierController.bindToVerifierServiceIfNeeded(mSnapshotSupplier, 0))
+ .isTrue();
+ }
+
+ @Test
+ public void testVerifierAvailableButNotConnected() {
+ assertThat(mVerifierController.isVerifierInstalled(mSnapshotSupplier, 0)).isTrue();
+ when(mInjector.getRemoteService(
+ any(Computer.class), any(Context.class), anyInt(), any(Handler.class)
+ )).thenReturn(null);
+ assertThat(mVerifierController.bindToVerifierServiceIfNeeded(mSnapshotSupplier, 0))
+ .isFalse();
+ // Test that nothing crashes if the verifier is available even though there's no bound
+ mVerifierController.notifyPackageNameAvailable(TEST_PACKAGE_NAME);
+ mVerifierController.notifyVerificationCancelled(TEST_PACKAGE_NAME);
+ mVerifierController.notifyVerificationTimeout(-1);
+ // Since there was no bound, no call is made to the verifier
+ verifyZeroInteractions(mMockService);
+ }
+
+ @Test
+ public void testUnbindService() throws Exception {
+ ArgumentCaptor<ServiceConnector.ServiceLifecycleCallbacks> captor = ArgumentCaptor.forClass(
+ ServiceConnector.ServiceLifecycleCallbacks.class);
+ assertThat(mVerifierController.bindToVerifierServiceIfNeeded(mSnapshotSupplier, 0))
+ .isTrue();
+ verify(mMockServiceConnector).setServiceLifecycleCallbacks(captor.capture());
+ ServiceConnector.ServiceLifecycleCallbacks<IVerifierService> callbacks = captor.getValue();
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService, times(1)).onVerificationRequired(any(VerificationSession.class));
+ callbacks.onBinderDied();
+ // Test that nothing crashes if the service connection is lost
+ assertThat(mVerifierController.isVerifierInstalled(mSnapshotSupplier, 0)).isTrue();
+ mVerifierController.notifyPackageNameAvailable(TEST_PACKAGE_NAME);
+ mVerifierController.notifyVerificationCancelled(TEST_PACKAGE_NAME);
+ mVerifierController.notifyVerificationTimeout(TEST_ID);
+ verifyNoMoreInteractions(mMockService);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ true)).isTrue();
+ mVerifierController.notifyVerificationTimeout(TEST_ID);
+ verify(mMockService, times(1)).onVerificationTimeout(eq(TEST_ID));
+ }
+
+ @Test
+ public void testNotifyPackageNameAvailable() throws Exception {
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ mVerifierController.notifyPackageNameAvailable(TEST_PACKAGE_NAME);
+ verify(mMockService).onPackageNameAvailable(eq(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testNotifyVerificationCancelled() throws Exception {
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ mVerifierController.notifyVerificationCancelled(TEST_PACKAGE_NAME);
+ verify(mMockService).onVerificationCancelled(eq(TEST_PACKAGE_NAME));
+ }
+
+ @Test
+ public void testStartVerificationSession() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ assertThat(session.getId()).isEqualTo(TEST_ID);
+ assertThat(session.getInstallSessionId()).isEqualTo(TEST_ID);
+ assertThat(session.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(session.getStagedPackageUri()).isEqualTo(TEST_PACKAGE_URI);
+ assertThat(session.getSigningInfo().getSigningDetails())
+ .isEqualTo(TEST_SIGNING_INFO.getSigningDetails());
+ List<SharedLibraryInfo> declaredLibraries = session.getDeclaredLibraries();
+ // SharedLibraryInfo doesn't have a "equals" method, so we have to check it indirectly
+ assertThat(declaredLibraries.getFirst().toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO1.toString());
+ assertThat(declaredLibraries.get(1).toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO2.toString());
+ // We can't directly test with PersistableBundle.equals() because the parceled bundle's
+ // structure is different, but all the key/value pairs should be preserved as before.
+ assertThat(session.getExtensionParams().getString(TEST_KEY))
+ .isEqualTo(mTestExtensionParams.getString(TEST_KEY));
+ }
+
+ @Test
+ public void testNotifyVerificationRetry() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ true)).isTrue();
+ verify(mMockService).onVerificationRetry(captor.capture());
+ VerificationSession session = captor.getValue();
+ assertThat(session.getId()).isEqualTo(TEST_ID);
+ assertThat(session.getInstallSessionId()).isEqualTo(TEST_ID);
+ assertThat(session.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+ assertThat(session.getStagedPackageUri()).isEqualTo(TEST_PACKAGE_URI);
+ assertThat(session.getSigningInfo().getSigningDetails())
+ .isEqualTo(TEST_SIGNING_INFO.getSigningDetails());
+ List<SharedLibraryInfo> declaredLibraries = session.getDeclaredLibraries();
+ // SharedLibraryInfo doesn't have a "equals" method, so we have to check it indirectly
+ assertThat(declaredLibraries.getFirst().toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO1.toString());
+ assertThat(declaredLibraries.get(1).toString())
+ .isEqualTo(TEST_SHARED_LIBRARY_INFO2.toString());
+ // We can't directly test with PersistableBundle.equals() because the parceled bundle's
+ // structure is different, but all the key/value pairs should be preserved as before.
+ assertThat(session.getExtensionParams().getString(TEST_KEY))
+ .isEqualTo(mTestExtensionParams.getString(TEST_KEY));
+ }
+
+ @Test
+ public void testNotifyVerificationTimeout() throws Exception {
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ true)).isTrue();
+ mVerifierController.notifyVerificationTimeout(TEST_ID);
+ verify(mMockService).onVerificationTimeout(eq(TEST_ID));
+ }
+
+ @Test
+ public void testRequestTimeout() {
+ // Let the mock handler set request to TIMEOUT, immediately after the request is sent.
+ // We can't mock postDelayed because it's final, but we can mock the method it calls.
+ when(mHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer(
+ i -> {
+ ((Message) i.getArguments()[0]).getCallback().run();
+ return true;
+ });
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mHandler, times(1)).sendMessageAtTime(any(Message.class), anyLong());
+ verify(mSessionCallback, times(1)).onTimeout();
+ verify(mInjector, times(2)).getCurrentTimeMillis();
+ verify(mInjector, times(1)).stopTimeoutCountdown(eq(mHandler), any());
+ }
+
+ @Test
+ public void testRequestTimeoutWithRetryPass() throws Exception {
+ // Only let the first request timeout and let the second one pass
+ when(mHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer(
+ i -> {
+ ((Message) i.getArguments()[0]).getCallback().run();
+ return true;
+ })
+ .thenAnswer(i -> true);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mHandler, times(1)).sendMessageAtTime(any(Message.class), anyLong());
+ verify(mSessionCallback, times(1)).onTimeout();
+ verify(mInjector, times(2)).getCurrentTimeMillis();
+ verify(mInjector, times(1)).stopTimeoutCountdown(eq(mHandler), any());
+ // Then retry
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ true)).isTrue();
+ verify(mMockService).onVerificationRetry(captor.capture());
+ VerificationSession session = captor.getValue();
+ VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build();
+ session.reportVerificationComplete(status);
+ verify(mSessionCallback, times(1)).onVerificationCompleteReceived(
+ eq(status), eq(null));
+ verify(mInjector, times(2)).stopTimeoutCountdown(eq(mHandler), any());
+ }
+
+ @Test
+ public void testRequestIncomplete() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ session.reportVerificationIncomplete(VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN);
+ verify(mSessionCallback, times(1)).onVerificationIncompleteReceived(
+ eq(VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN));
+ verify(mInjector, times(1)).stopTimeoutCountdown(eq(mHandler), any());
+ }
+
+ @Test
+ public void testRequestCompleteWithSuccessWithExtensionResponse() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build();
+ PersistableBundle bundle = new PersistableBundle();
+ session.reportVerificationComplete(status, bundle);
+ verify(mSessionCallback, times(1)).onVerificationCompleteReceived(
+ eq(status), eq(bundle));
+ verify(mInjector, times(1)).stopTimeoutCountdown(eq(mHandler), any());
+ }
+
+ @Test
+ public void testRequestCompleteWithFailure() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ VerificationStatus status = new VerificationStatus.Builder()
+ .setVerified(false)
+ .setFailureMessage(TEST_FAILURE_MESSAGE)
+ .build();
+ session.reportVerificationComplete(status);
+ verify(mSessionCallback, times(1)).onVerificationCompleteReceived(
+ eq(status), eq(null));
+ verify(mInjector, times(1)).stopTimeoutCountdown(eq(mHandler), any());
+ }
+
+ @Test
+ public void testRepeatedRequestCompleteShouldThrow() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ assertThat(mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false)).isTrue();
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build();
+ session.reportVerificationComplete(status);
+ // getters should throw after the report
+ expectThrows(IllegalStateException.class, () -> session.getTimeoutTime());
+ // Report again should fail with exception
+ expectThrows(IllegalStateException.class, () -> session.reportVerificationComplete(status));
+ }
+
+ @Test
+ public void testExtendTimeRemaining() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false);
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ final long initialTimeoutTime = TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS;
+ assertThat(session.getTimeoutTime()).isEqualTo(initialTimeoutTime);
+ final long extendTimeMillis = TEST_TIMEOUT_DURATION_MILLIS;
+ assertThat(session.extendTimeRemaining(extendTimeMillis)).isEqualTo(extendTimeMillis);
+ assertThat(session.getTimeoutTime()).isEqualTo(initialTimeoutTime + extendTimeMillis);
+ }
+
+ @Test
+ public void testExtendTimeExceedsMax() throws Exception {
+ ArgumentCaptor<VerificationSession> captor =
+ ArgumentCaptor.forClass(VerificationSession.class);
+ mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false);
+ verify(mMockService).onVerificationRequired(captor.capture());
+ VerificationSession session = captor.getValue();
+ final long initialTimeoutTime = TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS;
+ final long maxTimeoutTime = TEST_REQUEST_START_TIME + TEST_MAX_TIMEOUT_DURATION_MILLIS;
+ assertThat(session.getTimeoutTime()).isEqualTo(initialTimeoutTime);
+ final long extendTimeMillis = TEST_MAX_TIMEOUT_DURATION_MILLIS;
+ assertThat(session.extendTimeRemaining(extendTimeMillis)).isEqualTo(
+ TEST_MAX_TIMEOUT_DURATION_MILLIS - TEST_TIMEOUT_DURATION_MILLIS);
+ assertThat(session.getTimeoutTime()).isEqualTo(maxTimeoutTime);
+ }
+
+ @Test
+ public void testTimeoutChecksMultipleTimes() {
+ // Mock message handling
+ when(mHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer(
+ i -> {
+ ((Message) i.getArguments()[0]).getCallback().run();
+ return true;
+ });
+ // Mock time forward as the code continues to check for the current time
+ when(mInjector.getCurrentTimeMillis())
+ // First called when the tracker is created
+ .thenReturn(TEST_REQUEST_START_TIME)
+ // Then mock the first timeout check when the timeout time isn't reached yet
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS - 1000)
+ // Then mock the same time used to check the remaining time
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS - 1000)
+ // Then mock the second timeout check when the timeout time isn't reached yet
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS - 100)
+ // Then mock the same time used to check the remaining time
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS - 100)
+ // Then mock the third timeout check when the timeout time has been reached
+ .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS + 1);
+ mVerifierController.startVerificationSession(
+ mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI,
+ TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback,
+ /* retry= */ false);
+ verify(mHandler, times(3)).sendMessageAtTime(any(Message.class), anyLong());
+ verify(mInjector, times(6)).getCurrentTimeMillis();
+ verify(mSessionCallback, times(1)).onTimeout();
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
index 6f9b8df..39acd8d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
@@ -756,7 +756,8 @@
/* isApplied */false,
/* stagedSessionErrorCode */ PackageManager.INSTALL_UNKNOWN,
/* stagedSessionErrorMessage */ "no error",
- /* preVerifiedDomains */ null);
+ /* preVerifiedDomains */ null,
+ /* verifierController */ null);
StagingManager.StagedSession stagedSession = spy(session.mStagedSession);
doReturn(packageName).when(stagedSession).getPackageName();