diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-01-18 18:43:41 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-01-18 18:43:41 +0000 |
commit | 5b85c1579c31520b0a84b433d509e21ca38b502f (patch) | |
tree | 284ec116592def641a902ea37ac4f259151c1a3c | |
parent | e9c0be7c8ba51447e013a34a2f3252361659326b (diff) | |
parent | dafcc735a8240c9c1c1bca1299a9ab217457cb23 (diff) | |
download | AdServices-android14-mainline-extservices-release.tar.gz |
Merge cherrypicks of ['googleplex-android-review.googlesource.com/25924473', 'googleplex-android-review.googlesource.com/25854039', 'googleplex-android-review.googlesource.com/25936868'] into sparse-11243045-L95500030001370613.aml_ext_341414010android14-mainline-extservices-release
SPARSE_CHANGE: I4ad0cf0a2edea32a476fcd0bd20c97f28761f3bc
SPARSE_CHANGE: I6f912308c779cedeb4969dd4e2c7ca3187d9beb7
SPARSE_CHANGE: Ieab9c249b8201253273a672556a686d6a17f0596
Change-Id: I817542592ad7a7c0adc4a4becb87e7c0fe0a1698
11 files changed, 802 insertions, 154 deletions
diff --git a/adservices/service-core/java/com/android/adservices/service/Flags.java b/adservices/service-core/java/com/android/adservices/service/Flags.java index b181901a1..57dd4300e 100644 --- a/adservices/service-core/java/com/android/adservices/service/Flags.java +++ b/adservices/service-core/java/com/android/adservices/service/Flags.java @@ -4503,4 +4503,28 @@ public interface Flags extends CommonFlags { default int getBackgroundJobSamplingLoggingRate() { return DEFAULT_BACKGROUND_JOB_SAMPLING_LOGGING_RATE; } + + /** Default value of the timeout for AppSearch write operations */ + int DEFAULT_APPSEARCH_WRITE_TIMEOUT_MS = 3000; + + /** + * Gets the value of the timeout for AppSearch write operations, in milliseconds. + * + * @return the timeout, in milliseconds, for AppSearch write operations + */ + default int getAppSearchWriteTimeout() { + return DEFAULT_APPSEARCH_WRITE_TIMEOUT_MS; + } + + /** Default value of the timeout for AppSearch read operations */ + int DEFAULT_APPSEARCH_READ_TIMEOUT_MS = 750; + + /** + * Gets the value of the timeout for AppSearch read operations, in milliseconds. + * + * @return the timeout, in milliseconds, for AppSearch read operations + */ + default int getAppSearchReadTimeout() { + return DEFAULT_APPSEARCH_READ_TIMEOUT_MS; + } } diff --git a/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java b/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java index fb3ec484a..d3cc839a3 100644 --- a/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java +++ b/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java @@ -813,6 +813,8 @@ public final class FlagsConstants { public static final String KEY_PPAPI_APP_SIGNATURE_ALLOW_LIST = "ppapi_app_signature_allow_list"; + public static final String KEY_APPSEARCH_WRITE_TIMEOUT_MS = "appsearch_write_timeout_ms"; + public static final String KEY_APPSEARCH_READ_TIMEOUT_MS = "appsearch_read_timeout_ms"; public static final String KEY_APPSEARCH_WRITER_ALLOW_LIST_OVERRIDE = "appsearch_writer_allow_list_override"; diff --git a/adservices/service-core/java/com/android/adservices/service/PhFlags.java b/adservices/service-core/java/com/android/adservices/service/PhFlags.java index 1ddb750f4..fb3115315 100644 --- a/adservices/service-core/java/com/android/adservices/service/PhFlags.java +++ b/adservices/service-core/java/com/android/adservices/service/PhFlags.java @@ -16,6 +16,8 @@ package com.android.adservices.service; +import static com.android.adservices.service.FlagsConstants.KEY_APPSEARCH_READ_TIMEOUT_MS; +import static com.android.adservices.service.FlagsConstants.KEY_APPSEARCH_WRITE_TIMEOUT_MS; import static com.android.adservices.service.FlagsConstants.KEY_ENCRYPTION_KEY_JOB_PERIOD_MS; import static com.android.adservices.service.FlagsConstants.KEY_ENCRYPTION_KEY_JOB_REQUIRED_NETWORK_TYPE; import static com.android.adservices.service.FlagsConstants.KEY_FLEDGE_MEASUREMENT_REPORT_AND_REGISTER_EVENT_API_ENABLED; @@ -5500,6 +5502,8 @@ public final class PhFlags extends CommonPhFlags implements Flags { + KEY_MEASUREMENT_ENABLE_SESSION_STABLE_KILL_SWITCHES + " = " + getMeasurementEnableSessionStableKillSwitches()); + writer.println("\t" + KEY_APPSEARCH_WRITE_TIMEOUT_MS + " = " + getAppSearchWriteTimeout()); + writer.println("\t" + KEY_APPSEARCH_READ_TIMEOUT_MS + " = " + getAppSearchReadTimeout()); } @VisibleForTesting @@ -6526,4 +6530,20 @@ public final class PhFlags extends CommonPhFlags implements Flags { return loggingRatio; } + + @Override + public int getAppSearchWriteTimeout() { + return DeviceConfig.getInt( + FlagsConstants.NAMESPACE_ADSERVICES, + /* name= */ FlagsConstants.KEY_APPSEARCH_WRITE_TIMEOUT_MS, + /* defaultValue= */ DEFAULT_APPSEARCH_WRITE_TIMEOUT_MS); + } + + @Override + public int getAppSearchReadTimeout() { + return DeviceConfig.getInt( + FlagsConstants.NAMESPACE_ADSERVICES, + /* name= */ FlagsConstants.KEY_APPSEARCH_READ_TIMEOUT_MS, + /* defaultValue= */ DEFAULT_APPSEARCH_READ_TIMEOUT_MS); + } } diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentWorker.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentWorker.java index 42c5c1ed8..bda9d1744 100644 --- a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentWorker.java +++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentWorker.java @@ -65,15 +65,11 @@ import java.util.stream.Collectors; * source of truth for S-. When a device upgrades from S- to T+, the consent is initialized from * AppSearch. */ -// TODO(b/269798827): Enable for R. @RequiresApi(Build.VERSION_CODES.S) class AppSearchConsentWorker { // At the worker level, we ensure that writes do not conflict with any other writes/reads. private static final ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock(); - // Timeout for AppSearch write query in milliseconds. - private static final int TIMEOUT_MS = 2000; - private static final String CONSENT_DATABASE_NAME = "adservices_consent"; private static final String APP_CONSENT_DATABASE_NAME = "adservices_app_consent"; private static final String NOTIFICATION_DATABASE_NAME = "adservices_notification"; @@ -82,68 +78,69 @@ class AppSearchConsentWorker { private static final String UX_STATES_DATABASE_NAME = "adservices-ux-states"; // Required for allowing AdServices apk access to read consent written by ExtServices module. - private String mAdservicesPackageName; - private Context mContext; + private final String mAdservicesPackageName; - private ListenableFuture<AppSearchSession> mConsentSearchSession; - private ListenableFuture<AppSearchSession> mAppConsentSearchSession; - private ListenableFuture<AppSearchSession> mNotificationSearchSession; - private ListenableFuture<AppSearchSession> mInteractionsSearchSession; - private ListenableFuture<AppSearchSession> mTopicsSearchSession; - private ListenableFuture<AppSearchSession> mUxStatesSearchSession; + // Timeout for AppSearch write query in milliseconds. + private final int mTimeoutMs; + + private final ListenableFuture<AppSearchSession> mConsentSearchSession; + private final ListenableFuture<AppSearchSession> mAppConsentSearchSession; + private final ListenableFuture<AppSearchSession> mNotificationSearchSession; + private final ListenableFuture<AppSearchSession> mInteractionsSearchSession; + private final ListenableFuture<AppSearchSession> mTopicsSearchSession; + private final ListenableFuture<AppSearchSession> mUxStatesSearchSession; // When reading across APKs, a GlobalSearchSession is needed, hence we use it when reading. - private ListenableFuture<GlobalSearchSession> mGlobalSearchSession; - private Executor mExecutor = AdServicesExecutors.getBackgroundExecutor(); + private final ListenableFuture<GlobalSearchSession> mGlobalSearchSession; + private final Executor mExecutor = AdServicesExecutors.getBackgroundExecutor(); - private List<PackageIdentifier> mPackageIdentifiers = new ArrayList<>(); + private final List<PackageIdentifier> mPackageIdentifiers = new ArrayList<>(); // There is a single user ID for a given process, so this class would not be instantiated // across two user IDs. - private String mUid = getUserIdentifierFromBinderCallingUid(); + private final String mUid = getUserIdentifierFromBinderCallingUid(); private static final String SPLITTER = ","; private AppSearchConsentWorker(@NonNull Context context) { Objects.requireNonNull(context); - mContext = context; // We write with multiple schemas, so we need to initialize sessions per db. mConsentSearchSession = PlatformStorage.createSearchSessionAsync( - new PlatformStorage.SearchContext.Builder(mContext, CONSENT_DATABASE_NAME) + new PlatformStorage.SearchContext.Builder(context, CONSENT_DATABASE_NAME) .build()); mAppConsentSearchSession = PlatformStorage.createSearchSessionAsync( new PlatformStorage.SearchContext.Builder( - mContext, APP_CONSENT_DATABASE_NAME) + context, APP_CONSENT_DATABASE_NAME) .build()); mNotificationSearchSession = PlatformStorage.createSearchSessionAsync( new PlatformStorage.SearchContext.Builder( - mContext, NOTIFICATION_DATABASE_NAME) + context, NOTIFICATION_DATABASE_NAME) .build()); mInteractionsSearchSession = PlatformStorage.createSearchSessionAsync( new PlatformStorage.SearchContext.Builder( - mContext, INTERACTIONS_DATABASE_NAME) + context, INTERACTIONS_DATABASE_NAME) .build()); mTopicsSearchSession = PlatformStorage.createSearchSessionAsync( - new PlatformStorage.SearchContext.Builder(mContext, TOPICS_DATABASE_NAME) + new PlatformStorage.SearchContext.Builder(context, TOPICS_DATABASE_NAME) .build()); mUxStatesSearchSession = PlatformStorage.createSearchSessionAsync( - new PlatformStorage.SearchContext.Builder(mContext, UX_STATES_DATABASE_NAME) + new PlatformStorage.SearchContext.Builder(context, UX_STATES_DATABASE_NAME) .build()); // We use global session for reads since we may perform read on T+ AdServices package to // restore consent data post OTA. mGlobalSearchSession = PlatformStorage.createGlobalSearchSessionAsync( - new PlatformStorage.GlobalSearchContext.Builder(mContext).build()); + new PlatformStorage.GlobalSearchContext.Builder(context).build()); // The package identifier of the AdServices package on T+ should always have access to read // data written by AdExtServices package on S-. - mAdservicesPackageName = getAdServicesPackageName(mContext); + mAdservicesPackageName = getAdServicesPackageName(context); String shaCertsFlagValue = FlagsFactory.getFlags().getAdservicesApkShaCertificate(); for (String shaCert : shaCertsFlagValue.split(SPLITTER)) { @@ -151,6 +148,8 @@ class AppSearchConsentWorker { new PackageIdentifier( mAdservicesPackageName, new Signature(shaCert).toByteArray())); } + + mTimeoutMs = FlagsFactory.getFlags().getAppSearchWriteTimeout(); } /** Get an instance of AppSearchConsentWorker. */ @@ -175,8 +174,8 @@ class AppSearchConsentWorker { /** * Sets the consent for this user ID for this API type in AppSearch. If we do not get - * confirmation that the write was successful, then we throw an exception so that user does not - * incorrectly think that the consent is updated. + * confirmation that the write operation was successful, then we throw an exception so that user + * does not incorrectly think that the consent is updated. */ void setConsent(@NonNull String apiType, @NonNull Boolean consented) { Objects.requireNonNull(apiType); @@ -193,11 +192,11 @@ class AppSearchConsentWorker { apiType, consented.toString()); dao.writeData(mConsentSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote consent data to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -235,10 +234,10 @@ class AppSearchConsentWorker { mExecutor, AppSearchAppConsentDao.getRowId(mUid, consentType), AppSearchAppConsentDao.NAMESPACE) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to delete consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to delete consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -280,11 +279,11 @@ class AppSearchConsentWorker { dao.setApps(apps); } dao.writeData(mAppConsentSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote app consent data to AppSearch (add): " + dao); return true; } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); + LogUtil.e(e, "Failed to write consent to AppSearch"); return false; } finally { READ_WRITE_LOCK.writeLock().unlock(); @@ -293,8 +292,8 @@ class AppSearchConsentWorker { /** * Removes an app from the list of apps with this consentType for this user. If we do not get - * confirmation that the write was successful, then we throw an exception so that user does not - * incorrectly think that the consent is updated. + * confirmation that the write operation was successful, then we throw an exception so that user + * does not incorrectly think that the consent is updated. */ void removeAppWithConsent(@NonNull String consentType, @NonNull String app) { Objects.requireNonNull(consentType); @@ -320,11 +319,11 @@ class AppSearchConsentWorker { .filter(filterApp -> !filterApp.equals(app)) .collect(Collectors.toList())); dao.writeData(mAppConsentSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote app consent data to AppSearch (remove): " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.readLock().unlock(); } @@ -367,11 +366,11 @@ class AppSearchConsentWorker { /* wasNotificationDisplayed= */ wasNotificationDisplayed, /* wasGaUxNotificationDisplayed= */ wasGaUxNotificationDisplayed()); dao.writeData(mNotificationSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote notification data to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write notification data to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write notification data to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -389,11 +388,11 @@ class AppSearchConsentWorker { /* wasNotificationDisplayed= */ wasNotificationDisplayed(), /* wasGaUxNotificationDisplayed= */ wasNotificationDisplayed); dao.writeData(mNotificationSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote notification data to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write notification data to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write notification data to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -426,11 +425,11 @@ class AppSearchConsentWorker { apiType, currentFeatureType.ordinal()); dao.writeData(mInteractionsSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote feature type data to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write interactions data to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write interactions data to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -466,11 +465,11 @@ class AppSearchConsentWorker { apiType, interaction); dao.writeData(mInteractionsSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote interactions data to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write interactions data to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write interactions data to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -512,11 +511,11 @@ class AppSearchConsentWorker { dao.addBlockedTopic(topic); } dao.writeData(mTopicsSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote topics consent data to AppSearch (block): " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -539,11 +538,11 @@ class AppSearchConsentWorker { } dao.removeBlockedTopic(topic); dao.writeData(mTopicsSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote topics consent data to AppSearch (unblock): " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -554,7 +553,7 @@ class AppSearchConsentWorker { READ_WRITE_LOCK.writeLock().lock(); try { // We don't do {read, modify, write} here since the DAO has no other information besides - // blocked topics so we can rewrite it. + // blocked topics, so we can rewrite it. AppSearchTopicsConsentDao dao = new AppSearchTopicsConsentDao( mUid, @@ -564,11 +563,11 @@ class AppSearchConsentWorker { List.of(), List.of()); dao.writeData(mTopicsSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote topics consent data to AppSearch (clear): " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -606,7 +605,7 @@ class AppSearchConsentWorker { AdServicesCommon.ADEXTSERVICES_PACKAGE_NAME_SUFFIX, AdServicesCommon.ADSERVICES_APK_PACKAGE_NAME_SUFFIX); } - // If we don't know the AdServices package name, we can't do a write. + // If we don't know the AdServices package name, we can't write. throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); } @@ -637,11 +636,11 @@ class AppSearchConsentWorker { } dao.setAdIdEnabled(isAdIdEnabled); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote the isAdIdEnabled bit to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write the isAdIdEnabled to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write the isAdIdEnabled to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -674,11 +673,11 @@ class AppSearchConsentWorker { } dao.setU18Account(isU18Account); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote the isU18Account bit to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write the isU18Account to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write the isU18Account to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -711,11 +710,11 @@ class AppSearchConsentWorker { } dao.setEntryPointEnabled(isEntryPointEnabled); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote the isEntryPointEnabled bit to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write the isEntryPointEnabled to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write the isEntryPointEnabled to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -748,11 +747,11 @@ class AppSearchConsentWorker { } dao.setAdultAccount(isAdultAccount); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote the isAdultAccount bit to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write the isAdultAccount to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write the isAdultAccount to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -785,11 +784,11 @@ class AppSearchConsentWorker { } dao.setU18NotificationDisplayed(wasU18NotificationDisplayed); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote the wasU18NotificationDisplayed bit to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write the wasU18NotificationDisplayed to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write the wasU18NotificationDisplayed to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -822,11 +821,11 @@ class AppSearchConsentWorker { } dao.setUx(ux.toString()); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote PrivacySandboxUx to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write PrivacySandboxUx to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write PrivacySandboxUx to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -865,11 +864,11 @@ class AppSearchConsentWorker { } dao.setEnrollmentChannel(enrollmentChannel.toString()); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote PrivacySandboxUx to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write PrivacySandboxUx to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write PrivacySandboxUx to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchDao.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchDao.java index 4fdf36f67..48d27d1c5 100644 --- a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchDao.java +++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchDao.java @@ -16,6 +16,8 @@ package com.android.adservices.service.appsearch; +import static com.android.adservices.service.consent.ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE; + import android.annotation.NonNull; import android.os.Build; @@ -31,6 +33,7 @@ import androidx.appsearch.app.RemoveByDocumentIdRequest; import androidx.appsearch.app.SearchResults; import androidx.appsearch.app.SearchSpec; import androidx.appsearch.app.SetSchemaRequest; +import androidx.appsearch.app.SetSchemaResponse.MigrationFailure; import androidx.appsearch.exceptions.AppSearchException; import com.android.adservices.AdServicesCommon; @@ -38,7 +41,6 @@ import com.android.adservices.LogUtil; import com.android.adservices.service.Flags; import com.android.adservices.service.FlagsFactory; import com.android.adservices.service.common.AllowLists; -import com.android.adservices.service.consent.ConsentConstants; import com.android.internal.annotations.VisibleForTesting; import com.google.common.util.concurrent.FluentFuture; @@ -58,12 +60,8 @@ import java.util.function.BiFunction; * Base class for all data access objects for AppSearch. This class handles the common logic for * reading from and writing to AppSearch. */ -// TODO(b/269798827): Enable for R. @RequiresApi(Build.VERSION_CODES.S) class AppSearchDao { - // Timeout for AppSearch search query in milliseconds. - private static final int TIMEOUT_MS = 500; - /** * Iterate over the search results returned for the search query by AppSearch. * @@ -88,7 +86,7 @@ class AppSearchDao { // Converts GenericDocument object to the type of object passed in cls. documentResult = genericDocument.toDocumentClass(cls); } catch (AppSearchException e) { - LogUtil.e("Failed to convert GenericDocument to " + cls.getName(), e); + LogUtil.e(e, "Failed to convert GenericDocument to %s", cls.getName()); } } @@ -203,9 +201,11 @@ class AppSearchDao { results -> iterateSearchResults(cls, results, executor), executor) .transform(result -> ((T) result), executor); - return future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + + int timeout = FlagsFactory.getFlags().getAppSearchReadTimeout(); + return future.get(timeout, TimeUnit.MILLISECONDS); } catch (ExecutionException | InterruptedException | TimeoutException e) { - LogUtil.e("getConsent() Appsearch lookup failed with: ", e); + LogUtil.e(e, "getConsent() AppSearch lookup failed"); } return null; } @@ -216,7 +216,7 @@ class AppSearchDao { * we specify the packageIdentifier as that of the T+ AdServices APK, which after OTA, needs * access to the data written before OTA. What is written is the subclass type of DAO. * - * @return the result of the write. + * @return the result of the write operation. */ FluentFuture<AppSearchBatchResult<String, Void>> writeData( @NonNull ListenableFuture<AppSearchSession> appSearchSession, @@ -245,14 +245,17 @@ class AppSearchDao { // If we get failures in schemaResponse then we cannot try // to write. if (!setSchemaResponse.getMigrationFailures().isEmpty()) { + MigrationFailure failure = + setSchemaResponse.getMigrationFailures().get(0); LogUtil.e( "SetSchemaResponse migration failure: " - + setSchemaResponse - .getMigrationFailures() - .get(0)); - throw new RuntimeException( - ConsentConstants - .ERROR_MESSAGE_APPSEARCH_FAILURE); + + failure); + String message = + String.format( + "%s Migration failure: %s", + ERROR_MESSAGE_APPSEARCH_FAILURE, + failure.getAppSearchResult()); + throw new RuntimeException(message); } // The database knows about this schemaType and write can // occur. @@ -264,17 +267,17 @@ class AppSearchDao { executor); return putFuture; } catch (AppSearchException e) { - LogUtil.e("Cannot instantiate AppSearch database: " + e.getMessage()); + LogUtil.e(e, "Cannot instantiate AppSearch database"); + return FluentFuture.from( + Futures.immediateFailedFuture( + new RuntimeException(ERROR_MESSAGE_APPSEARCH_FAILURE, e))); } - return FluentFuture.from( - Futures.immediateFailedFuture( - new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE))); } /** * Delete a row from the database. * - * @return the result of the delete. + * @return the result of the delete operation. */ protected static <T> FluentFuture<AppSearchBatchResult<String, Void>> deleteData( @NonNull Class<T> cls, @@ -302,14 +305,17 @@ class AppSearchDao { // If we get failures in schemaResponse then we cannot try // to write. if (!setSchemaResponse.getMigrationFailures().isEmpty()) { + MigrationFailure failure = + setSchemaResponse.getMigrationFailures().get(0); LogUtil.e( "SetSchemaResponse migration failure: " - + setSchemaResponse - .getMigrationFailures() - .get(0)); - throw new RuntimeException( - ConsentConstants - .ERROR_MESSAGE_APPSEARCH_FAILURE); + + failure); + String message = + String.format( + "%s Migration failure: %s", + ERROR_MESSAGE_APPSEARCH_FAILURE, + failure.getAppSearchResult()); + throw new RuntimeException(message); } // The database knows about this schemaType and write can // occur. @@ -321,10 +327,10 @@ class AppSearchDao { executor); return deleteFuture; } catch (AppSearchException e) { - LogUtil.e("Cannot instantiate AppSearch database: " + e.getMessage()); + LogUtil.e(e, "Cannot instantiate AppSearch database"); + return FluentFuture.from( + Futures.immediateFailedFuture( + new RuntimeException(ERROR_MESSAGE_APPSEARCH_FAILURE, e))); } - return FluentFuture.from( - Futures.immediateFailedFuture( - new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE))); } } diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorker.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorker.java index f81566cc2..85e7baf07 100644 --- a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorker.java +++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorker.java @@ -28,6 +28,7 @@ import androidx.appsearch.platformstorage.PlatformStorage; import com.android.adservices.LogUtil; import com.android.adservices.concurrency.AdServicesExecutors; +import com.android.adservices.service.FlagsFactory; import com.android.adservices.service.common.compat.FileCompatUtils; import com.android.adservices.service.consent.ConsentConstants; import com.android.adservices.service.measurement.rollback.MeasurementRollbackWorker; @@ -59,7 +60,7 @@ public final class AppSearchMeasurementRollbackWorker implements MeasurementRoll private static final ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock(); // Timeout for AppSearch write query in milliseconds. - private static final int TIMEOUT_MS = 2000; + private final int mTimeoutMs; private final String mUserId; private final String mAdServicesPackageName; @@ -75,6 +76,8 @@ public final class AppSearchMeasurementRollbackWorker implements MeasurementRoll mSearchSession = PlatformStorage.createSearchSessionAsync( new PlatformStorage.SearchContext.Builder(context, DATABASE_NAME).build()); + + mTimeoutMs = FlagsFactory.getFlags().getAppSearchWriteTimeout(); } /** Return an instance of {@link AppSearchMeasurementRollbackWorker} */ @@ -98,11 +101,11 @@ public final class AppSearchMeasurementRollbackWorker implements MeasurementRoll // don't need to share it with the T package. Thus, we can send an empty list for the // packageIdentifiers parameter. dao.writeData(mSearchSession, List.of(), mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote measurement rollback data to AppSearch: %s", dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { LogUtil.e(e, "Failed to write measurement rollback to AppSearch"); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -130,11 +133,11 @@ public final class AppSearchMeasurementRollbackWorker implements MeasurementRoll mExecutor, storageIdentifier, AppSearchMeasurementRollbackDao.NAMESPACE) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Deleted MeasurementRollback data from AppSearch for: %s", storageIdentifier); } catch (InterruptedException | TimeoutException | ExecutionException e) { LogUtil.e(e, "Failed to delete MeasurementRollback data in AppSearch"); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } diff --git a/adservices/service-core/java/com/android/adservices/service/consent/ConsentManager.java b/adservices/service-core/java/com/android/adservices/service/consent/ConsentManager.java index 5b77deccb..10140e56f 100644 --- a/adservices/service-core/java/com/android/adservices/service/consent/ConsentManager.java +++ b/adservices/service-core/java/com/android/adservices/service/consent/ConsentManager.java @@ -2185,7 +2185,8 @@ public class ConsentManager { if (errorLogger != null) { errorLogger.apply(e); } - throw new RuntimeException(getClass().getSimpleName() + " failed. " + e.getMessage()); + throw new RuntimeException( + getClass().getSimpleName() + " failed. " + e.getMessage(), e); } finally { mReadWriteLock.writeLock().unlock(); } diff --git a/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchConsentWorkerTest.java b/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchConsentWorkerTest.java index 1114e11d2..23da8f97d 100644 --- a/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchConsentWorkerTest.java +++ b/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchConsentWorkerTest.java @@ -62,6 +62,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; import org.junit.Before; @@ -72,9 +74,12 @@ import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; @SmallTest public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { @@ -88,17 +93,20 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { private static final Topic TOPIC1 = Topic.create(0, 1, 11); private static final Topic TOPIC2 = Topic.create(12, 2, 22); private static final Topic TOPIC3 = Topic.create(123, 3, 33); - private List<Topic> mTopics = new ArrayList<>(); + private MockitoSession mMockitoSession; - @Mock - Flags mMockFlags; + private static final int APPSEARCH_TIMEOUT_MS = 1000; @Rule(order = 0) public final SdkLevelSupportRule sdkLevelRule = SdkLevelSupportRule.forAtLeastS(); + private final List<Topic> mTopics = Arrays.asList(TOPIC1, TOPIC2, TOPIC3); + private final ListeningExecutorService mExecutorService = + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); + @Mock private Flags mMockFlags; + @Before public void setup() { - mTopics.addAll(List.of(TOPIC1, TOPIC2, TOPIC3)); mMockitoSession = ExtendedMockito.mockitoSession() .mockStatic(FlagsFactory.class) @@ -109,6 +117,9 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { when(mMockFlags.getAdservicesApkShaCertificate()) .thenReturn(Flags.ADSERVICES_APK_SHA_CERTIFICATE); when(mMockFlags.getAppsearchWriterAllowListOverride()).thenReturn(""); + // Reduce AppSearch write timeout to speed up the tests. + when(mMockFlags.getAppSearchWriteTimeout()).thenReturn(APPSEARCH_TIMEOUT_MS); + when(mMockFlags.getAppSearchReadTimeout()).thenReturn(APPSEARCH_TIMEOUT_MS); } @After @@ -185,6 +196,34 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void testSetConsent_failure_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + initTimeoutResponse(); + RuntimeException e = + assertThrows( + RuntimeException.class, + () -> + AppSearchConsentWorker.getInstance() + .setConsent(API_TYPE, CONSENTED)); + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void testSetConsent() { MockitoSession staticMockSessionLocal = null; try { @@ -379,6 +418,8 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { RuntimeException.class, () -> appSearchConsentWorker.clearAppsWithConsent(TEST)); assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(ExecutionException.class); } finally { if (staticMockSessionLocal != null) { staticMockSessionLocal.finishMocking(); @@ -561,6 +602,8 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { RuntimeException.class, () -> appSearchConsentWorker.removeAppWithConsent(consentType, TEST)); assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(ExecutionException.class); } finally { if (staticMockSessionLocal != null) { staticMockSessionLocal.finishMocking(); @@ -677,7 +720,7 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test - public void testRecordGaUxNotificationDisplayed_faiure() { + public void testRecordGaUxNotificationDisplayed_failure() { runRecordNotificationDisplayedTestFailure(/* isBetaUx= */ false); } @@ -715,6 +758,52 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void testRecordNotificationDisplayed_failure_timeout() { + runRecordNotificationDisplayedTestFailureTimeout(/* isBetaUx= */ true); + } + + @Test + public void testRecordGaUxNotificationDisplayed_failure_timeout() { + runRecordNotificationDisplayedTestFailureTimeout(/* isBetaUx= */ false); + } + + private void runRecordNotificationDisplayedTestFailureTimeout(boolean isBetaUx) { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + initTimeoutResponse(); + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + + RuntimeException e; + if (isBetaUx) { + e = + assertThrows( + RuntimeException.class, + () -> worker.recordNotificationDisplayed(true)); + } else { + e = + assertThrows( + RuntimeException.class, + () -> worker.recordGaUxNotificationDisplayed(true)); + } + + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void testRecordNotificationDisplayed() { runRecordNotificationDisplayed(/* isBetaUx= */ true); } @@ -808,6 +897,36 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void testSetCurrentPrivacySandboxFeature_failure_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + + initTimeoutResponse(); + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + RuntimeException e = + assertThrows( + RuntimeException.class, + () -> + worker.setCurrentPrivacySandboxFeature( + PrivacySandboxFeatureType.PRIVACY_SANDBOX_RECONSENT)); + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void testSetCurrentPrivacySandboxFeature() { MockitoSession staticMockSessionLocal = null; try { @@ -882,6 +1001,40 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void testRecordUserManualInteractionWithConsent_failure_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .mockStatic(AppSearchInteractionsDao.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + + initTimeoutResponse(); + + when(AppSearchInteractionsDao.getRowId(any(), any())).thenReturn("" + UID); + + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + int interactions = ConsentManager.MANUAL_INTERACTIONS_RECORDED; + + RuntimeException e = + assertThrows( + RuntimeException.class, + () -> worker.recordUserManualInteractionWithConsent(interactions)); + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void testRecordUserManualInteractionWithConsent() { MockitoSession staticMockSessionLocal = null; try { @@ -954,6 +1107,38 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void testRecordBlockedTopic_failure_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .spyStatic(UserHandle.class) + .spyStatic(AppSearchTopicsConsentDao.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + initTimeoutResponse(); + + String query = "" + UID; + ExtendedMockito.doReturn(query).when(() -> AppSearchTopicsConsentDao.getQuery(any())); + ExtendedMockito.doReturn(null) + .when(() -> AppSearchTopicsConsentDao.readConsentData(any(), any(), any(), any())); + + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + RuntimeException e = + assertThrows(RuntimeException.class, () -> worker.recordBlockedTopic(TOPIC1)); + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void testRecordBlockedTopics_new() { MockitoSession staticMockSessionLocal = null; try { @@ -973,7 +1158,6 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { () -> AppSearchTopicsConsentDao.readConsentData( any(), any(), any(), any())); - AppSearchConsentWorker appSearchConsentWorker = AppSearchConsentWorker.getInstance(); appSearchConsentWorker.recordBlockedTopic(TOPIC1); } finally { @@ -1046,6 +1230,15 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { verify(dao).removeBlockedTopic(TOPIC1); verify(dao).writeData(any(), any(), any()); assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + + + Throwable cause = e.getCause(); + assertThat(cause).isNotNull(); + assertThat(cause).isInstanceOf(ExecutionException.class); + + Throwable rootCause = cause.getCause(); + assertThat(rootCause).isNotNull(); + assertThat(rootCause).isInstanceOf(InterruptedException.class); } finally { if (staticMockSessionLocal != null) { staticMockSessionLocal.finishMocking(); @@ -1141,6 +1334,32 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void testClearBlockedTopics_failure_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + initTimeoutResponse(); + + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + RuntimeException e = + assertThrows(RuntimeException.class, () -> worker.clearBlockedTopics()); + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void testClearBlockedTopics() { MockitoSession staticMockSessionLocal = null; try { @@ -1180,6 +1399,26 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { when(mockResponse.getMigrationFailures()).thenReturn(List.of()); } + private void initTimeoutResponse() { + AppSearchSession mockSession = Mockito.mock(AppSearchSession.class); + UserHandle mockUserHandle = Mockito.mock(UserHandle.class); + Mockito.when(UserHandle.getUserHandleForUid(Binder.getCallingUid())) + .thenReturn(mockUserHandle); + Mockito.when(mockUserHandle.getIdentifier()).thenReturn(UID); + ExtendedMockito.doReturn(Futures.immediateFuture(mockSession)) + .when(() -> PlatformStorage.createSearchSessionAsync(any())); + verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class)); + + SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class); + when(mockSession.setSchemaAsync(any(SetSchemaRequest.class))) + .thenReturn(Futures.immediateFuture(mockResponse)); + AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class); + when(mockSession.putAsync(any())).thenReturn(getLongRunningOperation(result)); + + verify(mockResponse, atMost(1)).getMigrationFailures(); + when(mockResponse.getMigrationFailures()).thenReturn(List.of()); + } + private void initFailureResponse() { AppSearchSession mockSession = Mockito.mock(AppSearchSession.class); ExtendedMockito.doReturn(Futures.immediateFuture(mockSession)) @@ -1190,7 +1429,8 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { when(mockSession.setSchemaAsync(any(SetSchemaRequest.class))) .thenReturn(Futures.immediateFuture(mockResponse)); - AppSearchResult mockResult = Mockito.mock(AppSearchResult.class); + AppSearchResult<String> mockResult = + AppSearchResult.newFailedResult(AppSearchResult.RESULT_INVALID_ARGUMENT, "test"); SetSchemaResponse.MigrationFailure failure = new SetSchemaResponse.MigrationFailure( /* namespace= */ TEST, @@ -1200,6 +1440,15 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { when(mockResponse.getMigrationFailures()).thenReturn(List.of(failure)); } + private <T> ListenableFuture<T> getLongRunningOperation(T result) { + // Wait for a time that's longer than the AppSearch write timeout, then return the result. + return mExecutorService.submit( + () -> { + TimeUnit.MILLISECONDS.sleep(APPSEARCH_TIMEOUT_MS + 1000); + return result; + }); + } + @Test public void isAdIdEnabledTest_trueBit() { isAdIdEnabledTest(true); @@ -1221,6 +1470,7 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { .startMocking(); ExtendedMockito.doReturn(isAdIdEnabled) .when(() -> AppSearchUxStatesDao.readIsAdIdEnabled(any(), any(), any(), any())); + AppSearchConsentWorker appSearchConsentWorker = AppSearchConsentWorker.getInstance(); assertThat(appSearchConsentWorker.isAdIdEnabled()).isEqualTo(isAdIdEnabled); } finally { @@ -1242,7 +1492,6 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { .strictness(Strictness.WARN) .initMocks(this) .startMocking(); - initSuccessResponse(); String query = "" + UID; ExtendedMockito.doReturn(query).when(() -> AppSearchUxStatesDao.getQuery(any())); @@ -1265,16 +1514,16 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test - public void setAdIdEnabledTest_trueBit() { - setAdIdEnabledTest(true); + public void setAdIdEnabledTest_failure_trueBit() { + setAdIdEnabledTestFailure(true); } @Test - public void setAdIdEnabledTest_falseBit() { - setAdIdEnabledTest(false); + public void setAdIdEnabledTest_failure_falseBit() { + setAdIdEnabledTestFailure(false); } - private void setAdIdEnabledTest(boolean isAdIdEnabled) { + private void setAdIdEnabledTestFailure(boolean isAdIdEnabled) { MockitoSession staticMockSessionLocal = null; try { staticMockSessionLocal = @@ -1297,6 +1546,40 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void setAdIdEnabledTest_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .spyStatic(UserHandle.class) + .mockStatic(AppSearchUxStatesDao.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + + String query = "" + UID; + ExtendedMockito.doReturn(query).when(() -> AppSearchUxStatesDao.getQuery(any())); + AppSearchUxStatesDao dao = Mockito.mock(AppSearchUxStatesDao.class); + ExtendedMockito.doReturn(dao) + .when(() -> AppSearchUxStatesDao.readData(any(), any(), any(), any())); + when(dao.writeData(any(), any(), any())) + .thenReturn(FluentFuture.from(getLongRunningOperation(null))); + + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + RuntimeException e = + assertThrows(RuntimeException.class, () -> worker.setAdIdEnabled(true)); + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void isU18AccountTest_trueBit() { isU18AccountTest(true); } @@ -1392,6 +1675,41 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void setU18AccountTest_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .mockStatic(AppSearchUxStatesDao.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + + String query = "" + UID; + ExtendedMockito.doReturn(query).when(() -> AppSearchUxStatesDao.getQuery(any())); + AppSearchUxStatesDao dao = Mockito.mock(AppSearchUxStatesDao.class); + ExtendedMockito.doReturn(dao) + .when(() -> AppSearchUxStatesDao.readData(any(), any(), any(), any())); + AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class); + when(dao.writeData(any(), any(), any())) + .thenReturn(FluentFuture.from(getLongRunningOperation(result))); + + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + RuntimeException e = + assertThrows(RuntimeException.class, () -> worker.setU18Account(false)); + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void isEntryPointEnabledTest_trueBit() { isEntryPointEnabledTest(true); } @@ -1492,6 +1810,43 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void setEntryPointEnabledTest_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .mockStatic(AppSearchUxStatesDao.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + + initSuccessResponse(); + + String query = "" + UID; + ExtendedMockito.doReturn(query).when(() -> AppSearchUxStatesDao.getQuery(any())); + AppSearchUxStatesDao dao = Mockito.mock(AppSearchUxStatesDao.class); + ExtendedMockito.doReturn(dao) + .when(() -> AppSearchUxStatesDao.readData(any(), any(), any(), any())); + AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class); + when(dao.writeData(any(), any(), any())) + .thenReturn(FluentFuture.from(getLongRunningOperation(result))); + + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + RuntimeException e = + assertThrows(RuntimeException.class, () -> worker.setEntryPointEnabled(true)); + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void isAdultAccountTest_trueBit() { isAdultAccountTest(true); } @@ -1591,6 +1946,42 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void setAdultAccountTest_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .mockStatic(AppSearchUxStatesDao.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + initSuccessResponse(); + + String query = "" + UID; + ExtendedMockito.doReturn(query).when(() -> AppSearchUxStatesDao.getQuery(any())); + AppSearchUxStatesDao dao = Mockito.mock(AppSearchUxStatesDao.class); + ExtendedMockito.doReturn(dao) + .when(() -> AppSearchUxStatesDao.readData(any(), any(), any(), any())); + AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class); + when(dao.writeData(any(), any(), any())) + .thenReturn(FluentFuture.from(getLongRunningOperation(result))); + + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + RuntimeException e = + assertThrows(RuntimeException.class, () -> worker.setAdultAccount(true)); + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void wasU18NotificationDisplayedTest_trueBit() { wasU18NotificationDisplayedTest(true); } @@ -1692,6 +2083,43 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void setU18NotificationDisplayedTest_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .mockStatic(AppSearchUxStatesDao.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + initSuccessResponse(); + + String query = "" + UID; + ExtendedMockito.doReturn(query).when(() -> AppSearchUxStatesDao.getQuery(any())); + AppSearchUxStatesDao dao = Mockito.mock(AppSearchUxStatesDao.class); + ExtendedMockito.doReturn(dao) + .when(() -> AppSearchUxStatesDao.readData(any(), any(), any(), any())); + AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class); + when(dao.writeData(any(), any(), any())) + .thenReturn(FluentFuture.from(getLongRunningOperation(result))); + + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + RuntimeException e = + assertThrows( + RuntimeException.class, () -> worker.setU18NotificationDisplayed(true)); + assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void getUxTest_allUxs() { MockitoSession staticMockSessionLocal = null; try { @@ -1740,7 +2168,7 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } } } - + @Test public void setUxTest_allUxsSuccess() { MockitoSession staticMockSessionLocal = null; @@ -1761,7 +2189,7 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { AppSearchUxStatesDao dao = Mockito.mock(AppSearchUxStatesDao.class); ExtendedMockito.doReturn(dao) .when(() -> AppSearchUxStatesDao.readData(any(), any(), any(), any())); - AppSearchBatchResult<String, Void> result = + AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class); when(dao.writeData(any(), any(), any())) .thenReturn(FluentFuture.from(Futures.immediateFuture(result))); @@ -1780,6 +2208,45 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } @Test + public void setUxTest_allUxs_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .mockStatic(AppSearchUxStatesDao.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + initSuccessResponse(); + + for (PrivacySandboxUxCollection ux : PrivacySandboxUxCollection.values()) { + String query = "" + UID; + ExtendedMockito.doReturn(query).when(() -> AppSearchUxStatesDao.getQuery(any())); + AppSearchUxStatesDao dao = Mockito.mock(AppSearchUxStatesDao.class); + ExtendedMockito.doReturn(dao) + .when(() -> AppSearchUxStatesDao.readData(any(), any(), any(), any())); + AppSearchBatchResult<String, Void> result = Mockito.mock( + AppSearchBatchResult.class); + when(dao.writeData(any(), any(), any())) + .thenReturn(FluentFuture.from(getLongRunningOperation(result))); + + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + RuntimeException e = assertThrows(RuntimeException.class, () -> worker.setUx(ux)); + assertThat(e.getMessage()).isEqualTo( + ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } + + @Test public void getEnrollmentChannelTest_allUxsAllEnrollmentChannels() { MockitoSession staticMockSessionLocal = null; try { @@ -1882,4 +2349,50 @@ public final class AppSearchConsentWorkerTest extends AdServicesUnitTestCase { } } } + + @Test + + public void setEnrollmentChannelTest_allUxsAllEnrollmentChannels_timeout() { + MockitoSession staticMockSessionLocal = null; + try { + staticMockSessionLocal = + ExtendedMockito.mockitoSession() + .spyStatic(PlatformStorage.class) + .mockStatic(AppSearchUxStatesDao.class) + .spyStatic(UserHandle.class) + .strictness(Strictness.WARN) + .initMocks(this) + .startMocking(); + + for (PrivacySandboxUxCollection ux : PrivacySandboxUxCollection.values()) { + for (PrivacySandboxEnrollmentChannelCollection channel : + ux.getEnrollmentChannelCollection()) { + String query = "" + UID; + ExtendedMockito.doReturn(query) + .when(() -> AppSearchUxStatesDao.getQuery(any())); + AppSearchUxStatesDao dao = Mockito.mock(AppSearchUxStatesDao.class); + ExtendedMockito.doReturn(dao) + .when(() -> AppSearchUxStatesDao.readData(any(), any(), any(), any())); + AppSearchBatchResult<String, Void> result = + Mockito.mock(AppSearchBatchResult.class); + when(dao.writeData(any(), any(), any())) + .thenReturn(FluentFuture.from(getLongRunningOperation(result))); + + AppSearchConsentWorker worker = AppSearchConsentWorker.getInstance(); + RuntimeException e = + assertThrows( + RuntimeException.class, + () -> worker.setEnrollmentChannel(ux, channel)); + assertThat(e.getMessage()) + .isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOf(TimeoutException.class); + } + } + } finally { + if (staticMockSessionLocal != null) { + staticMockSessionLocal.finishMocking(); + } + } + } } diff --git a/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchDaoTest.java b/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchDaoTest.java index 47bc18645..5627ff9fb 100644 --- a/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchDaoTest.java +++ b/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchDaoTest.java @@ -52,6 +52,8 @@ import com.android.adservices.service.consent.ConsentConstants; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import org.junit.Before; import org.junit.Rule; @@ -63,6 +65,8 @@ import org.mockito.MockitoAnnotations; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; @SmallTest public class AppSearchDaoTest { @@ -87,6 +91,8 @@ public class AppSearchDaoTest { new PackageIdentifier( /* packageName= */ TEST, /* sha256= */ new Signature(SHA).toByteArray()); + private static final int APPSEARCH_READ_TIMEOUT_MS = 500; + @Rule public final AdServicesExtendedMockitoRule adServicesExtendedMockitoRule = new AdServicesExtendedMockitoRule.Builder(this).mockStatic(FlagsFactory.class).build(); @@ -95,6 +101,7 @@ public class AppSearchDaoTest { public void before() { MockitoAnnotations.initMocks(this); when(mFlags.getAppsearchWriterAllowListOverride()).thenReturn(""); + when(mFlags.getAppSearchReadTimeout()).thenReturn(APPSEARCH_READ_TIMEOUT_MS); doReturn(mFlags).when(FlagsFactory::getFlags); } @@ -205,6 +212,30 @@ public class AppSearchDaoTest { } @Test + public void testReadConsentData_timeout() { + AppSearchDao result = + AppSearchDao.readConsentData( + AppSearchConsentDao.class, + getLongRunningOperation(mGlobalSearchSession), + mExecutor, + NAMESPACE, + TEST, + mAdServicesPackageName); + assertThat(result).isNull(); + } + + private <T> ListenableFuture<T> getLongRunningOperation(T result) { + // Wait for a time that's longer than the AppSearch read timeout, then return the result. + ListeningExecutorService ls = + MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); + return ls.submit( + () -> { + TimeUnit.MILLISECONDS.sleep(APPSEARCH_READ_TIMEOUT_MS + 500); + return result; + }); + } + + @Test public void testReadAppSearchData_emptyQuery() { AppSearchDao dao = AppSearchDao.readAppSearchSessionData( @@ -262,7 +293,8 @@ public class AppSearchDaoTest { when(mockSession.setSchemaAsync(any(SetSchemaRequest.class))) .thenReturn(Futures.immediateFuture(mockResponse)); - AppSearchResult mockResult = Mockito.mock(AppSearchResult.class); + AppSearchResult<Void> mockResult = + AppSearchResult.newFailedResult(AppSearchResult.RESULT_INVALID_ARGUMENT, "test"); SetSchemaResponse.MigrationFailure failure = new SetSchemaResponse.MigrationFailure( /* namespace= */ TEST, @@ -278,11 +310,12 @@ public class AppSearchDaoTest { Futures.immediateFuture(mockSession), List.of(PACKAGE_IDENTIFIER), mExecutor); - ExecutionException e = assertThrows(ExecutionException.class, () -> result.get()); + ExecutionException e = assertThrows(ExecutionException.class, result::get); assertThat(e.getMessage()) .isEqualTo( "java.lang.RuntimeException: " - + ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + + ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE + + " Migration failure: [FAILURE(3)]: test"); } @Test @@ -320,7 +353,8 @@ public class AppSearchDaoTest { when(mockSession.setSchemaAsync(any(SetSchemaRequest.class))) .thenReturn(Futures.immediateFuture(mockResponse)); - AppSearchResult mockResult = Mockito.mock(AppSearchResult.class); + AppSearchResult<String> mockResult = + AppSearchResult.newFailedResult(AppSearchResult.RESULT_INVALID_ARGUMENT, "test"); SetSchemaResponse.MigrationFailure failure = new SetSchemaResponse.MigrationFailure( /* namespace= */ TEST, @@ -337,11 +371,12 @@ public class AppSearchDaoTest { mExecutor, TEST, NAMESPACE); - ExecutionException e = assertThrows(ExecutionException.class, () -> result.get()); + ExecutionException e = assertThrows(ExecutionException.class, result::get); assertThat(e.getMessage()) .isEqualTo( "java.lang.RuntimeException: " - + ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + + ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE + + " Migration failure: [FAILURE(3)]: test"); } @Test diff --git a/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorkerTest.java b/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorkerTest.java index d9bccb54e..e8137c71c 100644 --- a/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorkerTest.java +++ b/adservices/tests/unittest/service-core/appsearch/src/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorkerTest.java @@ -40,6 +40,8 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import com.android.adservices.concurrency.AdServicesExecutors; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; import com.android.adservices.service.common.compat.FileCompatUtils; import com.android.adservices.service.consent.ConsentConstants; import com.android.dx.mockito.inline.extended.ExtendedMockito; @@ -60,6 +62,7 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; @SmallTest public class AppSearchMeasurementRollbackWorkerTest { @@ -67,7 +70,7 @@ public class AppSearchMeasurementRollbackWorkerTest { FileCompatUtils.getAdservicesFilename("measurement_rollback"); private static final String USERID = "user1"; private static final long APEX_VERSION = 100L; - private static final int FUTURE_TIMEOUT_MILLISECONDS = 3000; + private static final int APPSEARCH_WRITE_TIMEOUT_MS = 1000; private final Context mContext = ApplicationProvider.getApplicationContext(); private final String mAdServicesPackageName = @@ -75,8 +78,8 @@ public class AppSearchMeasurementRollbackWorkerTest { private final Executor mExecutor = AdServicesExecutors.getBackgroundExecutor(); private AppSearchMeasurementRollbackWorker mWorker; private MockitoSession mMockitoSession; - @Mock private ListenableFuture<AppSearchSession> mAppSearchSession; + @Mock private Flags mMockFlags; @Before public void setup() { @@ -84,10 +87,14 @@ public class AppSearchMeasurementRollbackWorkerTest { ExtendedMockito.mockitoSession() .mockStatic(PlatformStorage.class) .mockStatic(AppSearchDao.class) + .mockStatic(FlagsFactory.class) .strictness(Strictness.LENIENT) .initMocks(this) .startMocking(); + doReturn(mMockFlags).when(FlagsFactory::getFlags); + doReturn(APPSEARCH_WRITE_TIMEOUT_MS).when(mMockFlags).getAppSearchWriteTimeout(); + ArgumentCaptor<PlatformStorage.SearchContext> cap = ArgumentCaptor.forClass(PlatformStorage.SearchContext.class); doReturn(mAppSearchSession) @@ -115,7 +122,7 @@ public class AppSearchMeasurementRollbackWorkerTest { @SuppressWarnings("FutureReturnValueIgnored") @Test public void testClearAdServicesDeletionOccurred() { - FluentFuture mockResult = + FluentFuture<AppSearchBatchResult<String, Void>> mockResult = FluentFuture.from( Futures.immediateFuture( new AppSearchBatchResult.Builder<String, Void>().build())); @@ -136,13 +143,14 @@ public class AppSearchMeasurementRollbackWorkerTest { @Test public void testClearAdServicesDeletionOccurred_throwsChecked() { - Callable<Void> callable = + Callable<AppSearchBatchResult<String, Void>> callable = () -> { - TimeUnit.MILLISECONDS.sleep(FUTURE_TIMEOUT_MILLISECONDS); + TimeUnit.MILLISECONDS.sleep(APPSEARCH_WRITE_TIMEOUT_MS + 500); return null; }; - FluentFuture mockResult = FluentFuture.from(Futures.submit(callable, mExecutor)); + FluentFuture<AppSearchBatchResult<String, Void>> mockResult = + FluentFuture.from(Futures.submit(callable, mExecutor)); doReturn(mockResult).when(() -> AppSearchDao.deleteData(any(), any(), any(), any(), any())); RuntimeException e = @@ -150,6 +158,8 @@ public class AppSearchMeasurementRollbackWorkerTest { RuntimeException.class, () -> mWorker.clearAdServicesDeletionOccurred("mock_row_id")); assertThat(e).hasMessageThat().contains(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e).hasCauseThat().isNotNull(); + assertThat(e).hasCauseThat().isInstanceOf(TimeoutException.class); } @Test @@ -242,17 +252,17 @@ public class AppSearchMeasurementRollbackWorkerTest { @Test public void testRecordAdServicesDeletionOccurred_throwsChecked() { - // The manager class waits for 2 seconds on the future.get() call before timing out. So - // creating a future that takes longer than 2 sec to resolve, in order to create a + // The manager class waits for a few seconds on the future.get() call before timing out. So + // creating a future that takes longer than the timeout to resolve, in order to create a // TimeoutException. - Callable<Void> callable = + Callable<AppSearchBatchResult<String, Void>> callable = () -> { - TimeUnit.MILLISECONDS.sleep(FUTURE_TIMEOUT_MILLISECONDS); + TimeUnit.MILLISECONDS.sleep(APPSEARCH_WRITE_TIMEOUT_MS + 500); return null; }; - ListenableFuture<Void> future = Futures.submit(callable, mExecutor); - FluentFuture mockFuture = FluentFuture.from(future); + FluentFuture<AppSearchBatchResult<String, Void>> mockFuture = + FluentFuture.from(Futures.submit(callable, mExecutor)); AppSearchMeasurementRollbackDao dao = mock(AppSearchMeasurementRollbackDao.class); doReturn(mockFuture).when(dao).writeData(any(), any(), any()); @@ -269,6 +279,8 @@ public class AppSearchMeasurementRollbackWorkerTest { spyWorker.recordAdServicesDeletionOccurred( AdServicesManager.MEASUREMENT_DELETION, APEX_VERSION)); assertThat(e).hasMessageThat().contains(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + assertThat(e).hasCauseThat().isNotNull(); + assertThat(e).hasCauseThat().isInstanceOf(TimeoutException.class); } @Test diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/PhFlagsTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/PhFlagsTest.java index 3e29468e4..673405ad4 100644 --- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/PhFlagsTest.java +++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/PhFlagsTest.java @@ -50,6 +50,8 @@ import static com.android.adservices.service.Flags.DEFAULT_ADSERVICES_CONSENT_MI import static com.android.adservices.service.Flags.DEFAULT_ADSERVICES_ENABLEMENT_CHECK_ENABLED; import static com.android.adservices.service.Flags.DEFAULT_ADSERVICES_VERSION_MAPPINGS; import static com.android.adservices.service.Flags.DEFAULT_AD_ID_FETCHER_TIMEOUT_MS; +import static com.android.adservices.service.Flags.DEFAULT_APPSEARCH_READ_TIMEOUT_MS; +import static com.android.adservices.service.Flags.DEFAULT_APPSEARCH_WRITE_TIMEOUT_MS; import static com.android.adservices.service.Flags.DEFAULT_AUCTION_SERVER_AD_ID_FETCHER_TIMEOUT_MS; import static com.android.adservices.service.Flags.DEFAULT_BACKGROUND_JOB_SAMPLING_LOGGING_RATE; import static com.android.adservices.service.Flags.DEFAULT_BLOCKED_TOPICS_SOURCE_OF_TRUTH; @@ -10111,6 +10113,37 @@ public class PhFlagsTest { assertThrows(IllegalArgumentException.class, mPhFlags::getBackgroundJobSamplingLoggingRate); } + @Test + public void testGetAppSearchWriteTimeout() { + // Without any overriding, the value is the hard coded constant. + assertThat(mPhFlags.getAppSearchWriteTimeout()) + .isEqualTo(DEFAULT_APPSEARCH_WRITE_TIMEOUT_MS); + + int phOverridingValue = DEFAULT_APPSEARCH_WRITE_TIMEOUT_MS + 1000; + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_ADSERVICES, + FlagsConstants.KEY_APPSEARCH_WRITE_TIMEOUT_MS, + Integer.toString(phOverridingValue), + /* makeDefault */ false); + + assertThat(mPhFlags.getAppSearchWriteTimeout()).isEqualTo(phOverridingValue); + } + + @Test + public void testGetAppSearchReadTimeout() { + // Without any overriding, the value is the hard coded constant. + assertThat(mPhFlags.getAppSearchReadTimeout()).isEqualTo(DEFAULT_APPSEARCH_READ_TIMEOUT_MS); + + int phOverridingValue = DEFAULT_APPSEARCH_READ_TIMEOUT_MS + 500; + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_ADSERVICES, + FlagsConstants.KEY_APPSEARCH_READ_TIMEOUT_MS, + Integer.toString(phOverridingValue), + /* makeDefault */ false); + + assertThat(mPhFlags.getAppSearchReadTimeout()).isEqualTo(phOverridingValue); + } + private void overrideBackgroundJobSamplingLoggingRate(int phOverridingValue) { DeviceConfig.setProperty( DeviceConfig.NAMESPACE_ADSERVICES, |