diff options
| author | 2024-10-05 20:38:17 +0000 | |
|---|---|---|
| committer | 2024-10-05 20:38:17 +0000 | |
| commit | bc57d80342edd76c3f95e1592dce59458a613060 (patch) | |
| tree | a50e3ff49e9cfb01a44ae8224ad1226aa7fb199c | |
| parent | 5671aa9fb7360b7692e4834e713666bdbe12181c (diff) | |
| parent | a4c17763bf6a104efef1717e758afd9f74a3211c (diff) | |
Merge "[2/N] implementation of verifier controller and status tracker" into main
8 files changed, 1496 insertions, 9 deletions
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index b1b1637c890b..34d939b07187 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.SystemService; 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 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements private final StagingManager mStagingManager; private AppOpsManager mAppOps; + private final VerifierController mVerifierController; private final HandlerThread mInstallThread; private final Handler mInstallHandler; @@ -325,6 +327,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements 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 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements 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 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements 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 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements 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 ff8a69de35bc..c581622914fa 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_FAILED_VERIFICATION_FAIL 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.DataLoaderManager; 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; 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.PackageLite; import android.content.pm.parsing.result.ParseResult; import android.content.pm.parsing.result.ParseTypeImpl; import android.content.pm.verify.domain.DomainSet; +import android.content.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.content.res.Resources; 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.Looper; 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.Installer.InstallerException; 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.CompletableFuture; 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { * 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mSilentUpdatePolicy = silentUpdatePolicy; mHandler = new Handler(looper, mHandlerCallback); mStagingManager = stagingManager; + mVerifierController = verifierController; this.sessionId = sessionId; this.userId = userId; @@ -1249,6 +1260,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { "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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + /** + * 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } 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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @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 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { 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 000000000000..db747f9940a0 --- /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 000000000000..7eac940933c2 --- /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 7aa2ff50dbbd..cbca434a6bb6 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.AtomicFile 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 @@ class PackageInstallerSessionTest { /* 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 @@ class PackageInstallerSessionTest { 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 @@ class PackageInstallerSessionTest { 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 000000000000..fa076db456ec --- /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 000000000000..be094b0152bc --- /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 6f9b8dfc023d..39acd8dd816a 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 @@ public class StagingManagerTest { /* 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(); |