diff options
author | Xin Li <delphij@google.com> | 2024-03-06 09:30:08 -0800 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2024-03-06 09:30:08 -0800 |
commit | 209031ab4c42cbf931729533666a571295d6747e (patch) | |
tree | ff0874151c073d757c20d0196a2bb3599efa7f0d | |
parent | b1e3041c540e6397ac86ae6a5eeb8958c423fc1d (diff) | |
parent | 14beeea07284a8f611fb73e81e37e03f86e01724 (diff) | |
download | ExtServices-main.tar.gz |
Bug: 319669529
Merged-In: Ifecdd1cd8d47fc2d71aecd13b8155cb976a96ba3
Change-Id: I0836ddc9b10b1742c01128edf305c5bb05169742
29 files changed, 2848 insertions, 463 deletions
@@ -36,16 +36,14 @@ android_library { "java/src/**/*.java", ], - // Need to use empty manifest to avoid getting INSTALL_FAILED_DUPLICATE_PERMISSION when running - // unit tests, because the manifest is included in this library which is statically included in - // the test module. The actual manifest is included in the android_app target below. - manifest: "EmptyManifest.xml", - sdk_version: "module_current", min_sdk_version: "30", resource_dirs: [ "java/res", ], + + manifest: "EmptyManifest.xml", + static_libs: [ "androidx.annotation_annotation", "androidx.autofill_autofill", @@ -53,6 +51,8 @@ android_library { "TextClassifierNotificationLibNoManifest", "androidx.work_work-runtime", "modules-utils-build", + "androidx.appsearch_appsearch", + "androidx.appsearch_appsearch-platform-storage", ], libs: [ @@ -68,7 +68,37 @@ android_library { } android_app { - name: "ExtServices", + name: "ExtServices-tplus", + sdk_version: "module_current", + min_sdk_version: "30", + manifest: "AndroidManifest.xml", + optimize: { + optimize: true, + shrink_resources: true, + proguard_compatibility: false, + proguard_flags_files: ["proguard.proguard"], + }, + privileged: true, + privapp_allowlist: ":privapp_allowlist_android.ext_tplus.services.xml", + static_libs: [ + "ExtServices-core", + ], + jarjar_rules: "jarjar-rules.txt", + jni_libs: [ + "libtextclassifier", + "libextservices_jni", + "libhpke_jni", + ], + use_embedded_native_libs: true, + apex_available: [ + "//apex_available:platform", + "com.android.extservices", + "test_com.android.extservices", + ], +} + +android_app { + name: "ExtServices-sminus", sdk_version: "module_current", min_sdk_version: "30", manifest: "AndroidManifest.xml", @@ -79,15 +109,19 @@ android_app { proguard_flags_files: ["proguard.proguard"], }, privileged: true, - privapp_allowlist: ":privapp_allowlist_android.ext.services.xml", + privapp_allowlist: ":privapp_allowlist_android.ext_sminus.services.xml", static_libs: [ "ExtServices-core", + "AdServices-core", ], jarjar_rules: "jarjar-rules.txt", jni_libs: [ "libtextclassifier", "libextservices_jni", + "libhpke_jni", ], + libs: ["android.ext.adservices"], + optional_uses_libs: ["android.ext.adservices"], use_embedded_native_libs: true, apex_available: [ "//apex_available:platform", diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 354ea51..6453ad9 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -36,11 +36,9 @@ <!-- Remove unused permissions merged from WorkManager library --> <uses-permission android:name="android.permission.WAKE_LOCK" tools:node="remove" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" tools:node="remove" /> + <!-- Need this permission to receive the Boot-Completed broadcast --> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> - <permission android:name="android.permission.INIT_EXT_SERVICES" - android:protectionLevel="signature"/> - <uses-permission android:name="android.permission.INIT_EXT_SERVICES" /> <application android:name=".ExtServicesApplication" android:label="@string/app_name" @@ -54,6 +52,9 @@ <process android:process=":modelDownloaderServiceProcess"> <allow-permission android:name="android.permission.INTERNET" /> </process> + <process android:process=".adservices"> + <allow-permission android:name="android.permission.INTERNET" /> + </process> </processes> <service android:name=".storage.CacheQuotaServiceImpl" @@ -161,13 +162,17 @@ android:authorities="${applicationId}.androidx-startup" tools:node="remove" /> - <!-- Boot completed receiver sends privileged startup broadcast. --> - <receiver android:name=".common.BootCompletedReceiver" - android:enabled="@bool/enableBootCompletedReceiver" + <receiver android:name=".common.AdServicesFilesCleanupBootCompleteReceiver" + android:enabled="@bool/enableAdServicesDataCleanupReceiver" android:exported="true"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> </intent-filter> </receiver> + <service android:name="android.ext.services.common.AdServicesAppsearchDeleteJob" + android:enabled="@bool/enableAdServicesDataCleanupReceiver" + android:permission="android.permission.BIND_JOB_SERVICE" + android:exported="false"> + </service> </application> </manifest> diff --git a/EmptyManifest.xml b/EmptyManifest.xml index 4c6b790..66cc859 100644 --- a/EmptyManifest.xml +++ b/EmptyManifest.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 The Android Open Source Project +<!-- Copyright (C) 2023 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. @@ -1,3 +1,4 @@ +# Bug component: 1101073 adarshsridhar@google.com npattan@google.com # Autofill @@ -11,7 +12,5 @@ juliacr@google.com toki@google.com tonymak@google.com licha@google.com -# AdServices -npattan@google.com -include platform/packages/modules/common:/MODULES_OWNERS # see go/mainline-owners-policy +include platform/packages/modules/common:/MODULES_OWNERS # see go/mainline-owners-policy
\ No newline at end of file diff --git a/TEST_MAPPING b/TEST_MAPPING index 8333156..553833b 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -1,10 +1,16 @@ { "presubmit": [ { - "name": "ExtServicesUnitTests" + "name": "ExtServicesUnitTests-tplus" }, { - "name": "libextservices_test" + "name": "ExtServicesUnitTests-sminus" + }, + { + "name": "libextservices_test-tplus" + }, + { + "name": "libextservices_test-sminus" } ], "imports": [ @@ -14,10 +20,22 @@ ], "mainline-presubmit": [ { - "name": "ExtServicesUnitTests[com.google.android.extservices.apex]" + "name": "ExtServicesUnitTests-tplus[com.google.android.extservices_tplus.apex]" + }, + { + "name": "libextservices_test-tplus[com.google.android.extservices_tplus.apex]" + }, + { + "name": "ExtServicesUnitTests-sminus[com.google.android.extservices.apex]" + }, + { + "name": "libextservices_test-sminus[com.google.android.extservices.apex]" + }, + { + "name": "CtsExtServicesHostTests-tplus[com.google.android.extservices_tplus.apex]" }, { - "name": "libextservices_test[com.google.android.extservices.apex]" + "name": "CtsExtServicesHostTests-sminus[com.google.android.extservices.apex]" } ] } diff --git a/apex/Android.bp b/apex/Android.bp index ec608c2..1c5b7bc 100644 --- a/apex/Android.bp +++ b/apex/Android.bp @@ -23,26 +23,31 @@ package { } apex { + name: "com.android.extservices_tplus", + apex_available_name: "com.android.extservices", + defaults: ["com.android.extservices-defaults"], + apps: ["ExtServices-tplus"], + manifest: "tplus_apex_manifest.json", + variant_version: "3", + min_sdk_version: "33", +} + +apex { name: "com.android.extservices", defaults: ["com.android.extservices-defaults"], - manifest: "apex_manifest.json", + apps: ["ExtServices-sminus"], + java_libs: ["android.ext.adservices"], + jni_libs: ["libtflite_support_classifiers_native"], } apex_defaults { name: "com.android.extservices-defaults", defaults: ["r-launched-apex-module"], + manifest: "apex_manifest.json", key: "com.android.extservices.key", certificate: ":com.android.extservices.certificate", - apps: [ - "ExtServices", - "AdExtServicesApk", - ], - java_libs: ["android.ext.adservices"], - jni_libs: [ - "libtflite_support_classifiers_native", - "libhpke_jni", - ], prebuilts: ["current_sdkinfo"], + file_contexts: ":com.android.extservices-file_contexts", // Indicates that pre-installed version of this apex can be compressed. // Whether it actually will be compressed is controlled on per-device basis. compressible: true, diff --git a/apex/permissions/Android.bp b/apex/permissions/Android.bp index d815f67..ae69906 100644 --- a/apex/permissions/Android.bp +++ b/apex/permissions/Android.bp @@ -16,10 +16,18 @@ package { default_team: "trendy_team_android_rubidium", default_applicable_licenses: ["Android-Apache-2.0"], - default_visibility: ["//packages/modules/ExtServices:__subpackages__"], + default_visibility: [ + "//packages/modules/AdServices:__subpackages__", + "//packages/modules/ExtServices:__subpackages__", + ], } filegroup { - name: "privapp_allowlist_android.ext.services.xml", - srcs: ["android.ext.services.xml"], + name: "privapp_allowlist_android.ext_tplus.services.xml", + srcs: ["android.ext_tplus.services.xml"], +} + +filegroup { + name: "privapp_allowlist_android.ext_sminus.services.xml", + srcs: ["android.ext_sminus.services.xml"], } diff --git a/apex/permissions/android.ext.services.xml b/apex/permissions/android.ext_sminus.services.xml index 4fb465d..4fb465d 100644 --- a/apex/permissions/android.ext.services.xml +++ b/apex/permissions/android.ext_sminus.services.xml diff --git a/apex/permissions/android.ext_tplus.services.xml b/apex/permissions/android.ext_tplus.services.xml new file mode 100644 index 0000000..1c75bb1 --- /dev/null +++ b/apex/permissions/android.ext_tplus.services.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 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 + --> +<permissions> + <privapp-permissions package="android.ext.services"> + <permission name="android.permission.PROVIDE_RESOLVER_RANKER_SERVICE" /> + <permission name="android.permission.MONITOR_DEFAULT_SMS_PACKAGE" /> + <permission name="android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE" /> + <permission name="android.permission.INTERACT_ACROSS_USERS" /> + </privapp-permissions> +</permissions> diff --git a/apex/tplus_apex_manifest.json b/apex/tplus_apex_manifest.json new file mode 100644 index 0000000..977f2ee --- /dev/null +++ b/apex/tplus_apex_manifest.json @@ -0,0 +1,7 @@ +{ + "name": "com.android.extservices", + + // Placeholder module version to be replaced during build. + // Do not change! + "version": 0 +} diff --git a/java/res/values-v31/bools.xml b/java/res/values-v33/bools.xml index f39c88f..fb9ea77 100644 --- a/java/res/values-v31/bools.xml +++ b/java/res/values-v33/bools.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 The Android Open Source Project +<!-- Copyright (C) 2023 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. @@ -15,5 +15,6 @@ --> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <bool name="enableBootCompletedReceiver">true</bool> + <!-- Overriding the default value to only run the AdServices cleanup receiver on Android T+ --> + <bool name="enableAdServicesDataCleanupReceiver">true</bool> </resources> diff --git a/java/res/values/bools.xml b/java/res/values/bools.xml index d21ed12..c08d918 100644 --- a/java/res/values/bools.xml +++ b/java/res/values/bools.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2016 The Android Open Source Project +<!-- Copyright (C) 2023 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. @@ -15,5 +15,9 @@ --> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <bool name="enableBootCompletedReceiver">false</bool> + <!-- By default, we do not want the cleanup receiver to run. This needs to only run on Android + T+ because it's meant to clean up AdServices files in the ExtServices apk data directory after + OTA from S and lower. Running this receiver on lower Android versions will delete files that + should not be deleted--> + <bool name="enableAdServicesDataCleanupReceiver">false</bool> </resources> diff --git a/java/src/android/ext/services/common/AdServicesAppsearchDeleteJob.java b/java/src/android/ext/services/common/AdServicesAppsearchDeleteJob.java new file mode 100644 index 0000000..24497c4 --- /dev/null +++ b/java/src/android/ext/services/common/AdServicesAppsearchDeleteJob.java @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2023 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 android.ext.services.common; + + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.appsearch.app.AppSearchSession; +import androidx.appsearch.app.SearchResults; +import androidx.appsearch.app.SearchSpec; +import androidx.appsearch.app.SetSchemaRequest; +import androidx.appsearch.app.SetSchemaResponse; +import androidx.appsearch.platformstorage.PlatformStorage; + +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Background Period Job that deletes Appsearch Data after OTA To T and the data has been migrated + * to System Server. First time it runs, it stores the timestamp and check if Appsearch has any data + * and if not, cancels itself. Next time, it checks if the maximum allowed time from OTA to keep the + * Appsearch data has passed and if so deletes the data and cancels itself. Else it checks + * if minimum time to the device has AdServices enabled in Flags, has passed. If so, it checks + * if the AdServices is enabled and the first time it finds enabled, the job stores the timestamp. + * Next runs it keeps checking if the minimum allowed time to run the data migration after enabling + * AdServices has passed and if so deletes the Appsearch data and cancels itself. + **/ +public class AdServicesAppsearchDeleteJob extends JobService { + + private static final String TAG = "extservices"; + public static final int JOB_ID = 27; // The job id matches the placeholder in AdServicesJobInfo + private static final String KEY_EXT_ADSERVICES_APPSEARCH_PREFS = + "ext_adservices_appsearch_delete_job_prefs"; + + static final String SHARED_PREFS_KEY_OTA_DATE = "ota_date"; + static final String SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND = "appsearch_data_found"; + static final String SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE = "adservices_enabled_date"; + + static final String SHARED_PREFS_KEY_ADSERVICES_APPSEARCH_DELETED = + "adservices_appsearch_deleted"; + + static final String SHARED_PREFS_KEY_ATTEMPTED_DELETE_COUNT = + "attempted_delete_count"; + static final String SHARED_PREFS_KEY_JOB_RUN_COUNT = "job_run_count"; + + 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"; + private static final String INTERACTIONS_DATABASE_NAME = "adservices_interactions"; + private static final String TOPICS_DATABASE_NAME = "adservices-topics"; + private static final String UX_STATES_DATABASE_NAME = "adservices-ux-states"; + private static final String MEASUREMENT_ROLLBACK_DATABASE_NAME = "measurement_rollback"; + + private static final List<String> AD_SERVICES_APPSEARCH_DBS_TO_DELETE = List.of( + CONSENT_DATABASE_NAME, + APP_CONSENT_DATABASE_NAME, + NOTIFICATION_DATABASE_NAME, + INTERACTIONS_DATABASE_NAME, + TOPICS_DATABASE_NAME, + UX_STATES_DATABASE_NAME, + MEASUREMENT_ROLLBACK_DATABASE_NAME); + + private final Executor mExecutor = Executors.newCachedThreadPool(); + + @Override + public boolean onStartJob(JobParameters params) { + int jobId = params.getJobId(); + Log.i(TAG, "AdServicesAppsearchDeleteJobService invoked with job id: " + + jobId); + SharedPreferences sharedPref = getSharedPreferences(); + SharedPreferences.Editor editor = sharedPref.edit(); + + try { + AdservicesPhFlags adservicesPhFlags = getAdservicesPhFlags(); + if (!adservicesPhFlags.isAppsearchDeleteJobEnabled()) { + Log.d(TAG, + "AdServicesAppsearchDeleteJobService is not enabled in config," + + " cancelling job id: " + params.getJobId()); + cancelPeriodicJob(this, params); + return false; + } + if (adservicesPhFlags.shouldDoNothingAdServicesAppsearchDeleteJob()) { + Log.d(TAG, + "AdServicesAppsearchDeleteJobService is set to do nothing in config," + + " returning.... "); + return false; + } + + long jobRunCount = sharedPref.getLong(SHARED_PREFS_KEY_JOB_RUN_COUNT, + /* defaultValue= */ 0L) + 1; + Log.d(TAG, + "AdServicesAppsearchDeleteJobService job run count is " + jobRunCount); + editor.putLong(SHARED_PREFS_KEY_JOB_RUN_COUNT, jobRunCount); + + long otaDate = sharedPref.getLong(SHARED_PREFS_KEY_OTA_DATE, 0L); + // Check if the job is run first time after OTA + if (otaDate == 0L) { + long currentTime = System.currentTimeMillis(); + Log.d(TAG, + "AdServicesAppsearchDeleteJobService OTA to T " + + " on : " + currentTime); + editor.putLong(SHARED_PREFS_KEY_OTA_DATE, currentTime); + boolean foundData = !isAppsearchDbEmpty(this, mExecutor, + NOTIFICATION_DATABASE_NAME); + editor.putBoolean(SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND, foundData); + Log.d(TAG, "AdServicesAppsearchDeleteJobService found data in Appsearch: " + + foundData); + if (!foundData) { + cancelPeriodicJob(this, params); + } + } else if (hasMinMinutesPassed(otaDate, + adservicesPhFlags.getMinMinutesFromOtaToDeleteAppsearchData())) { + Log.d(TAG, "Deleting Appsearch Data as maximum allowed time passed " + + "from OTA"); + deleteAppsearchData(params, editor, sharedPref, + adservicesPhFlags.getMaxAppsearchAdServicesDeleteAttempts()); + } else if (!hasMinMinutesPassed(otaDate, + adservicesPhFlags.getMinMinutesFromOtaToCheckAdServicesStatus())) { + Log.d(TAG, "Minimum time to check AdServices status from OTA " + + "has not passed, returning...."); + } else { + long adServicesEnabledDate = sharedPref + .getLong(SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, 0L); + boolean adServicesEnabled = adservicesPhFlags.isAdServicesEnabled(); + if (!adServicesEnabled) { + // restart the timer + editor.putLong(SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, 0L); + Log.d(TAG, + "AdServicesAppsearchDeleteJobService found " + + "AdServices Disabled"); + } else if (adServicesEnabledDate == 0L) { + long currentTime = System.currentTimeMillis(); + editor.putLong(SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, currentTime); + Log.d(TAG, "AdServicesAppsearchDeleteJobService found " + + "AdServices Enabled on : " + currentTime); + } else if (hasMinMinutesPassed(adServicesEnabledDate, + adservicesPhFlags.getMinMinutesToDeleteFromAdServicesEnabled())) { + Log.d(TAG, "Deleting Appsearch Data after verifying minimum time passed " + + "from AdServices enabled"); + deleteAppsearchData(params, editor, sharedPref, + adservicesPhFlags.getMaxAppsearchAdServicesDeleteAttempts()); + } else { + Log.d(TAG, "Not Deleting Appsearch Data as minimum time" + + " has not passed from AdServices enabled"); + } + } + + } catch (Exception e) { + Log.e(TAG, "Exception in AdServicesAppsearchDeleteJob " + e); + } + + if (!editor.commit()) { + Log.e(TAG, "AdServicesAppsearchDeleteJob could not commit shared prefs"); + } + return false; + } + + private void deleteAppsearchData(JobParameters params, SharedPreferences.Editor editor, + SharedPreferences sharedPreferences, int maxAttempts) { + + if (deleteAppsearchDbs(this, mExecutor, AD_SERVICES_APPSEARCH_DBS_TO_DELETE)) { + Log.d(TAG, + "AdServicesAppsearchDeleteJobService deleted data in Appsearch " + + " cancelling future runs of job id: " + + params.getJobId()); + editor.putLong(SHARED_PREFS_KEY_ADSERVICES_APPSEARCH_DELETED, + System.currentTimeMillis()); + cancelPeriodicJob(this, params); + } else { + int attemptedDeletes = sharedPreferences + .getInt(SHARED_PREFS_KEY_ATTEMPTED_DELETE_COUNT, 0) + 1; + editor.putInt(SHARED_PREFS_KEY_ATTEMPTED_DELETE_COUNT, attemptedDeletes); + Log.e(TAG, + "AdServicesAppsearchDeleteJobService did not delete" + + " all Appsearch dbs on attempt " + attemptedDeletes); + if (attemptedDeletes >= maxAttempts) { + Log.e(TAG, "Max attempts to deletes has been reached, cancelling future jobs"); + cancelPeriodicJob(this, params); + } + } + } + + @Override + public boolean onStopJob(JobParameters params) { + Log.d(TAG, "AdServicesAppsearchDeleteJobService onStopJob invoked with " + + "job id " + params.getJobId()); + return false; + } + + /** + * checks if data is empty in Appsearch database + * + * @param context android context + * @param executor Executor service + * @param db Appsearch db to check data is empty + * @return {@code true} Appsearch database is empty; else {@code false}. + **/ + @VisibleForTesting + @TargetApi(31) + public boolean isAppsearchDbEmpty(Context context, Executor executor, String db) + throws TimeoutException, ExecutionException, InterruptedException { + final ListenableFuture<AppSearchSession> appSearchSession = + PlatformStorage.createSearchSessionAsync( + new PlatformStorage.SearchContext.Builder(context, db).build()); + + ListenableFuture<SearchResults> searchFuture = + Futures.transform( + appSearchSession, + session -> session.search("", new SearchSpec.Builder().build()), + executor); + FluentFuture<Integer> future = + FluentFuture.from(searchFuture) + .transformAsync( + results -> + Futures.transform(results.getNextPageAsync(), + List::size, executor), + executor); + int resultsSize = future.get(500, TimeUnit.MILLISECONDS); + Log.d(TAG, "Appsearch found results of size " + resultsSize); + return resultsSize == 0; + } + + /** + * Deletes App search Database by calling setForceOverride true on the schemaRequest + * + * @param context the android context + * @param executor Executor + * @param dbs the list of database names to be deleted + * @return {@code true} deletion was a success; else {@code false}. + **/ + @VisibleForTesting + public boolean deleteAppsearchDbs(Context context, Executor executor, List<String> dbs) { + int successCount = 0; + for (String appsearchDb : dbs) { + if (deleteAppsearchDb(context, executor, appsearchDb)) successCount++; + } + boolean success = successCount == dbs.size(); + Log.d(TAG, "AdServicesAppsearchDeleteJobService Complete with success " + successCount + + " out of " + dbs.size() + ",success status is " + success); + return success; + + } + + /** + * Deletes App search Database + * + * @param context the android context + * @param executor Executor + * @param db the database name to be deleted + * @return {@code true} deletion was a success; else {@code false}. + **/ + @VisibleForTesting + public boolean deleteAppsearchDb(Context context, Executor executor, String db) { + Log.d(TAG, "Deleting AdServices Appsearch db " + db); + try { + SetSchemaResponse setSchemaResponse = getDeleteSchemaResponse(context, + executor, + db); + if (!setSchemaResponse.getMigrationFailures().isEmpty()) { + Log.e(TAG, + "Delete failed for AdServices Appsearch db " + db + + " , SetSchemaResponse migration failure: " + + setSchemaResponse + .getMigrationFailures() + .get(0)); + return false; + } + Log.d(TAG, "Delete types size " + setSchemaResponse.getDeletedTypes().size()); + for (String deletedType : setSchemaResponse.getDeletedTypes()) { + Log.d(TAG, "Deleted type is " + deletedType); + } + Log.d(TAG, "Delete successful for AdServices Appsearch db " + db); + return true; + + } catch (Exception e) { + Log.e(TAG, "Delete failed for AdServices Appsearch db " + db + " " + e); + return false; + } + } + + /** + * Creates the appSearch session and calls schema request to setForceOverride on a database + * to delete it. + * + * @param context the android context + * @param executor executor service + * @param db database name to delete + * @return SetSchemaResponse from executing the schema request to delete db + **/ + @VisibleForTesting + @TargetApi(31) + public SetSchemaResponse getDeleteSchemaResponse(Context context, Executor executor, + String db) throws InterruptedException, TimeoutException, ExecutionException { + final ListenableFuture<AppSearchSession> appSearchSession = + PlatformStorage.createSearchSessionAsync( + new PlatformStorage.SearchContext.Builder(context, db).build()); + SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().setForceOverride( + true).build(); + return FluentFuture.from(appSearchSession) + .transformAsync( + session -> session.setSchemaAsync(setSchemaRequest), executor) + .get(500, TimeUnit.MILLISECONDS); + } + + /** + * Checks if the give date in string has passed the minutes compared to current date + * + * @param timestampToCheckInMillis date time in millis to check against + * @param minMinutesToCheck minimum minutes + **/ + @VisibleForTesting + public boolean hasMinMinutesPassed(long timestampToCheckInMillis, + long minMinutesToCheck) { + long currentTimestamp = System.currentTimeMillis(); + long millisToMinutes = 60000L; + long minutesPassed = (currentTimestamp - timestampToCheckInMillis) + / millisToMinutes; + Log.d(TAG, "The minutes from current date " + currentTimestamp + + " to dateToCheck " + timestampToCheckInMillis + + " is " + minutesPassed + + " and minimum minutes to check " + minMinutesToCheck); + return minutesPassed >= minMinutesToCheck; + } + + + /** + * Cancels the current periodic job and sets the job to not reschedule + * + * @param context the android context + * @param params the job params + **/ + @VisibleForTesting + public void cancelPeriodicJob(Context context, JobParameters params) { + final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + if (jobScheduler != null) { + int jobId = params.getJobId(); + jobScheduler.cancel(jobId); + Log.d(TAG, "AdServicesAppsearchDeletePeriodicJobService cancelled job " + + jobId); + } + setReschedule(params, false); + } + + /** + * call the parent jobFinished method with setting the re-schedule flag + * + * @param jobParameters job params + * @param reschedule whether to reschedule the job + **/ + @VisibleForTesting + public void setReschedule(JobParameters jobParameters, boolean reschedule) { + jobFinished(jobParameters, reschedule); + } + + /** + * returns the instance of Adservices Ph flags object + **/ + @VisibleForTesting + public AdservicesPhFlags getAdservicesPhFlags() { + return new AdservicesPhFlags(); + } + + /** + * returns the shared prefs object + **/ + @VisibleForTesting + SharedPreferences getSharedPreferences() { + return this.getSharedPreferences(KEY_EXT_ADSERVICES_APPSEARCH_PREFS, + Context.MODE_PRIVATE); + } + + /** + * Schedules AdServicesAppsearchDeleteJob run periodically to check the + * AdServices status and then create the actual delete job to delete all the AdServices + * app search data. + * + * @param context the android context + **/ + @SuppressLint("MissingPermission") + public static void scheduleAdServicesAppsearchDeletePeriodicJob( + Context context, AdservicesPhFlags adservicesPhFlags) { + try { + Log.d(TAG, + "Scheduling AdServicesAppsearchDeleteJobService ..."); + + if (!adservicesPhFlags.isAppsearchDeleteJobEnabled()) { + Log.d(TAG, + "AdServicesAppsearchDeleteJobService periodic job disabled in " + + "config, Cancelling Scheduling ..."); + return; + } + + final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + if (jobScheduler == null) { + Log.e(TAG, "AdServicesAppsearchDeleteJobService JobScheduler is null"); + return; + } + final JobInfo oldJob = jobScheduler.getPendingJob(JOB_ID); + if (oldJob != null) { + Log.d(TAG, "AdServicesAppsearchDeleteJobService already scheduled" + + " with job id:" + JOB_ID + + ", skipping reschedule"); + return; + } + JobInfo.Builder jobInfoBuild = new JobInfo.Builder(JOB_ID, + new ComponentName(context, AdServicesAppsearchDeleteJob.class)); + jobInfoBuild.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + jobInfoBuild.setPersisted(true); + + jobInfoBuild.setPeriodic( + adservicesPhFlags.getAppsearchDeletePeriodicIntervalMillis(), + adservicesPhFlags.getAppsearchDeleteJobFlexMillis()); + + final JobInfo job = jobInfoBuild.build(); + + jobScheduler.schedule(job); + Log.d(TAG, "Scheduled AdServicesAppsearchDeleteJobService with job id: " + JOB_ID); + } catch (Exception e) { + Log.e(TAG, "Exception in scheduling job AdServicesAppsearchDeleteJobService " + e); + } + } + +} diff --git a/java/src/android/ext/services/common/AdServicesFilesCleanupBootCompleteReceiver.java b/java/src/android/ext/services/common/AdServicesFilesCleanupBootCompleteReceiver.java new file mode 100644 index 0000000..55ea0d2 --- /dev/null +++ b/java/src/android/ext/services/common/AdServicesFilesCleanupBootCompleteReceiver.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2023 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 android.ext.services.common; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.provider.DeviceConfig; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import java.io.File; +import java.util.function.ToIntBiFunction; + +/** + * Handles the BootCompleted initialization for ExtServices APK on T+. + * <p> + * The BootCompleted receiver deletes files created by the AdServices code on S- that persist on + * disk after an OTA to T+. Once these files are deleted, this receiver disables itself. + * <p> + * Since this receiver disables itself after the first run, it will not be re-run after any code + * changes to this class. In order to re-enable this receiver and run the updated code, the simplest + * way is to rename the class every upon every module release that changes the code. Also, in order + * to protect against accidental name re-use, the {@code testReceiverDoesNotReuseClassNames} unit + * test tracking used names should be updated upon each rename as well. + */ +public class AdServicesFilesCleanupBootCompleteReceiver extends BroadcastReceiver { + private static final String TAG = "extservices"; + private static final String KEY_RECEIVER_ENABLED = + "extservices_adservices_data_cleanup_enabled"; + + // All files created by the AdServices code within ExtServices should have this prefix. + private static final String ADSERVICES_PREFIX = "adservices"; + + @TargetApi(Build.VERSION_CODES.TIRAMISU) // Receiver disabled in manifest for S- devices + @SuppressWarnings("ReturnValueIgnored") // Intentionally ignoring return value of Log.d/Log.e + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "AdServices files cleanup receiver received BOOT_COMPLETED broadcast for user " + + context.getUser().getIdentifier()); + + // Check if the feature flag is enabled, otherwise exit without doing anything. + if (!isReceiverEnabled()) { + Log.d(TAG, "AdServices files cleanup receiver not enabled in config, exiting"); + return; + } + + try { + // Look through and delete any files in the data dir that have the `adservices` prefix + boolean success = deleteAdServicesFiles(context.getDataDir()); + + // Log as `d` or `e` depending on success or failure. + ToIntBiFunction<String, String> function = success ? Log::d : Log::e; + function.applyAsInt(TAG, + "AdServices files cleanup receiver data deletion success: " + success); + + scheduleAppsearchDeleteJob(context); + } finally { + unregisterSelf(context); + } + } + + private void unregisterSelf(Context context) { + context.getPackageManager().setComponentEnabledSetting( + new ComponentName(context, this.getClass()), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + /* flags= */ 0); + Log.d(TAG, "Disabled AdServices files cleanup receiver"); + } + + @VisibleForTesting + public boolean isReceiverEnabled() { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ADSERVICES, + /* name= */ KEY_RECEIVER_ENABLED, + /* defaultValue= */ true); + } + + /** + * Recursively delete all files with a prefix of "adservices" from the specified directory. + * <p> + * Note: It expects the input File object to be a directory and not a regular file. Also, + * it only deletes the contents of the input directory, and not the directory itself, even if + * the name of the directory starts with the prefix. + * + * @param currentDirectory the directory to scan for files + * @return {@code true} if all adservices files were successfully deleted; else {@code false}. + */ + @VisibleForTesting + public boolean deleteAdServicesFiles(File currentDirectory) { + if (currentDirectory == null) { + Log.d(TAG, "Argument passed to deleteAdServicesFiles is null"); + return true; + } + + try { + if (!currentDirectory.isDirectory()) { + Log.d(TAG, "Argument passed to deleteAdServicesFiles is not a directory"); + return true; + } + + boolean allSuccess = true; + + File[] files = currentDirectory.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + // Delete ALL data if the directory name starts with the adservices prefix. + // Otherwise, delete any file in the subtree that starts with the prefix. + if (doesFileNameStartWithPrefix(file)) { + // Directory starting with adservices, so delete everything inside it. + allSuccess = deleteAllData(file) && allSuccess; + } else { + // Directory but not starting with adservices, so only delete adservices + // files. + allSuccess = deleteAdServicesFiles(file) && allSuccess; + } + } else if (doesFileNameStartWithPrefix(file)) { + allSuccess = safeDelete(file) && allSuccess; + } + } + + return allSuccess; + } catch (RuntimeException e) { + Log.e(TAG, "Error deleting directory " + currentDirectory.getName(), e); + return false; + } + } + + private boolean doesFileNameStartWithPrefix(File file) { + // Do a case-insensitive comparison + return ADSERVICES_PREFIX.regionMatches( + /* ignoreCase= */ true, + /* toOffset= */ 0, + file.getName(), + /* ooffset= */ 0, + /* len= */ ADSERVICES_PREFIX.length()); + } + + private boolean deleteAllData(File currentDirectory) { + if (currentDirectory == null) { + Log.d(TAG, "Argument passed to deleteAllData is null"); + return true; + } + + try { + if (!currentDirectory.isDirectory()) { + Log.d(TAG, "Argument passed to deleteAllData is not a directory"); + return true; + } + + boolean allSuccess = true; + + for (File file : currentDirectory.listFiles()) { + allSuccess = (file.isDirectory() ? deleteAllData(file) : safeDelete(file)) + && allSuccess; + } + + // If deleting the entire subdirectory has been successful, then (and only then) delete + // the current directory. + allSuccess = allSuccess && safeDelete(currentDirectory); + + return allSuccess; + } catch (RuntimeException e) { + Log.e(TAG, "Error deleting directory " + currentDirectory.getName(), e); + return false; + } + } + + private boolean safeDelete(File file) { + try { + return file.delete(); + } catch (RuntimeException e) { + String message = String.format( + "AdServices files cleanup receiver: Error deleting %s - %s", file.getName(), + e.getMessage()); + Log.e(TAG, message, e); + return false; + } + } + + /** + * Schedules background periodic job AdservicesAppsearchDeleteJob + * to delete Appsearch data after OTA and data migration + * + * @param context the android context + **/ + @VisibleForTesting + public void scheduleAppsearchDeleteJob(Context context) { + AdServicesAppsearchDeleteJob + .scheduleAdServicesAppsearchDeletePeriodicJob(context, + new AdservicesPhFlags()); + } +} diff --git a/java/src/android/ext/services/common/AdservicesPhFlags.java b/java/src/android/ext/services/common/AdservicesPhFlags.java new file mode 100644 index 0000000..a65c031 --- /dev/null +++ b/java/src/android/ext/services/common/AdservicesPhFlags.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2023 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 android.ext.services.common; + +import android.annotation.TargetApi; +import android.provider.DeviceConfig; + + +/** + * Adservices Flags Implementation that delegates to DeviceConfig. + */ +public class AdservicesPhFlags { + /** + * Gets the value of periodic interval to run the AdServices check deletion job from + * device config flags + * + * @return value periodic interval in millis + **/ + @TargetApi(33) + public Long getAppsearchDeletePeriodicIntervalMillis() { + return DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ADSERVICES, + /* name= */ "ext_appsearch_delete_periodic_job_interval_ms", + /* defaultValue= */ 7 * 24 * 60 * 60 * 1000L); // 1 week in millis + } + + /** + * Gets the value of periodic flex to run the adservices check deletion job from + * device config flags + * + * @return value periodic flex in millis + **/ + @TargetApi(33) + public Long getAppsearchDeleteJobFlexMillis() { + return DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ADSERVICES, + /* name= */ "ext_appsearch_delete_scheduler_job_flex_ms", + /* defaultValue= */ 15 * 60 * 1000L); // 15 Minutes in millis + } + + /** + * Check if AdServices flags are set to enable AdServices, this includes checking if the + * global_kill_switch is disabled and the adservice_enabled is true. + * + * @return {@code true} AdService is enabled; else {@code false}. + **/ + @TargetApi(33) + public boolean isAdServicesEnabled() { + return !DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ADSERVICES, + /* flagName */ "global_kill_switch", + /* defaultValue */ true) + && DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ADSERVICES, + /* flagName */ "adservice_enabled", + /* defaultValue */ false); + } + + /** + * Check if Appsearch deletion job is enabled. + * + * @return {@code true} Appsearch deletion job is enabled; else {@code false}. + **/ + @TargetApi(33) + public boolean isAppsearchDeleteJobEnabled() { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ADSERVICES, + /* flagName */ "ext_enable_appsearch_delete_job", + /* defaultValue */ true); + } + + /** + * Gets the value of min minutes after which to check AdServices status + * + * @return value of min minutes + **/ + @TargetApi(33) + public Long getMinMinutesFromOtaToCheckAdServicesStatus() { + return DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ADSERVICES, + /* name= */ "ext_min_minutes_from_ota_check_adservices_status", + /* defaultValue= */ 7 * 24 * 60L); // 1 Week in minutes + } + + /** + * Gets the value of min minutes to delete Appsearch data from date of OTA + * + * @return value of min minutes + **/ + @TargetApi(33) + public Long getMinMinutesFromOtaToDeleteAppsearchData() { + return DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ADSERVICES, + /* name= */ "ext_min_minutes_from_ota_delete_appsearch_data", + /* defaultValue= */ 365 * 24 * 60L); // 1 Year in minutes + } + + /** + * Gets the value of min minutes to delete Appsearch data from date of first time AdServices + * was found to be enabled + * + * @return value of min minutes + **/ + @TargetApi(33) + public Long getMinMinutesToDeleteFromAdServicesEnabled() { + return DeviceConfig.getLong( + DeviceConfig.NAMESPACE_ADSERVICES, + /* name= */ "ext_min_minutes_to_delete_from_adservices_enabled", + /* defaultValue= */ 2 * 7 * 24 * 60L); // 2 Weeks in minutes + } + + /** + * max delete attempts on appsearch dbs before cancelling the job + * + * @return value of min delete attempts + **/ + @TargetApi(33) + public int getMaxAppsearchAdServicesDeleteAttempts() { + return DeviceConfig.getInt( + DeviceConfig.NAMESPACE_ADSERVICES, + /* name= */ "ext_max_appsearch_adservices_delete_attempts", + /* defaultValue= */ 10); + } + + /** + * returns if the job should do nothing in case the job needs to be updated + * and job cannot be scheduled again, so keeping the periodic job running + * + * @return true if job should do nothing + **/ + @TargetApi(33) + public boolean shouldDoNothingAdServicesAppsearchDeleteJob() { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_ADSERVICES, + /* name= */ "ext_do_nothing_adservices_appsearch_delete_job", + /* defaultValue= */ false); + } + +} diff --git a/java/src/android/ext/services/common/BootCompletedReceiver.java b/java/src/android/ext/services/common/BootCompletedReceiver.java deleted file mode 100644 index af28dd7..0000000 --- a/java/src/android/ext/services/common/BootCompletedReceiver.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (C) 2023 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 android.ext.services.common; - -import android.annotation.SuppressLint; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.os.Build; -import android.provider.DeviceConfig; -import android.util.Log; - -import androidx.annotation.ChecksSdkIntAtLeast; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - -/** - * Handles the BootCompleted initialization for AdExtServices APK on S-. - * The BootCompleted receiver re-broadcasts a different intent that is handled by the - * AdExtBootCompletedReceiver within the AdServices apk. The reason for doing this here instead of - * within the AdServices APK is due to problematic platform modifications (b/286070595). - */ -public class BootCompletedReceiver extends BroadcastReceiver { - private static final String TAG = "extservices"; - private static final String KEY_PRIVACY_EXCLUDE_LIST = "privacy_exclude_list"; - private static final String KEY_EXTSERVICES_BOOT_COMPLETE_RECEIVER = - "extservices_bootcomplete_enabled"; - - private static final String ADEXTBOOTCOMPLETEDRECEIVER_CLASS_NAME = - "com.android.adservices.service.common.AdExtBootCompletedReceiver"; - private static final String REBROADCAST_INTENT_ACTION = - "android.adservices.action.INIT_EXT_SERVICES"; - private static final String ADSERVICES_SETTINGS_MAINACTIVITY = - "com.android.adservices.ui.settings.activities.AdServicesSettingsMainActivity"; - - @SuppressLint("MissingPermission") - @Override - public void onReceive(Context context, Intent intent) { - Log.i(TAG, "BootCompletedReceiver received BOOT_COMPLETED broadcast (f): " - + Build.FINGERPRINT); - - // Check if the feature is enabled, otherwise exit without doing anything. - if (!isReceiverEnabled()) { - Log.d(TAG, "BootCompletedReceiver not enabled in config, exiting"); - return; - } - - String adServicesPackageName = getAdExtServicesPackageName(context); - if (adServicesPackageName == null) { - Log.d(TAG, "AdServices package was not present, exiting BootCompletedReceiver"); - return; - } - - // No need to run this on every boot if we're on T+ and the AdExtServices components have - // already been disabled. - if (shouldDisableReceiver(context, adServicesPackageName)) { - context.getPackageManager().setComponentEnabledSetting( - new ComponentName(context.getPackageName(), this.getClass().getName()), - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - 0); - Log.d(TAG, "Disabled BootCompletedReceiver as AdServices is already initialized."); - return; - } - - // Check if this device is among a list of excluded devices - String excludeList = getExcludedFingerprints(); - Log.d(TAG, "Read BOOT_COMPLETED broadcast exclude list: " + excludeList); - if (Arrays.stream(excludeList.split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .anyMatch(Build.FINGERPRINT::startsWith)) { - Log.d(TAG, "Device is present in the exclude list, exiting BootCompletedReceiver"); - return; - } - - // Re-broadcast the intent - Intent intentToSend = new Intent(REBROADCAST_INTENT_ACTION); - intentToSend.setComponent( - new ComponentName(adServicesPackageName, ADEXTBOOTCOMPLETEDRECEIVER_CLASS_NAME)); - intentToSend.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); - context.sendBroadcast(intentToSend); - Log.i(TAG, "BootCompletedReceiver sending init broadcast: " + intentToSend); - } - - @SuppressLint("MissingPermission") - @VisibleForTesting - public boolean isReceiverEnabled() { - return DeviceConfig.getBoolean( - DeviceConfig.NAMESPACE_ADSERVICES, - /* flagName */ KEY_EXTSERVICES_BOOT_COMPLETE_RECEIVER, - /* defaultValue */ false); - } - - @SuppressLint("MissingPermission") - @VisibleForTesting - public String getExcludedFingerprints() { - return DeviceConfig.getString( - DeviceConfig.NAMESPACE_ADSERVICES, - /* flagName */ KEY_PRIVACY_EXCLUDE_LIST, - /* defaultValue */ ""); - } - - @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) - @VisibleForTesting - public boolean isAtLeastT() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU; - } - - private boolean shouldDisableReceiver(@NonNull Context context, - @NonNull String adServicesPackageName) { - Objects.requireNonNull(context); - Objects.requireNonNull(adServicesPackageName); - return isAtLeastT() && !isExtServicesInitialized(context, adServicesPackageName); - } - - private boolean isExtServicesInitialized(Context context, String adServicesPackageName) { - Intent intent = new Intent(); - intent.setComponent( - new ComponentName(adServicesPackageName, ADSERVICES_SETTINGS_MAINACTIVITY)); - List<ResolveInfo> list = context.getPackageManager().queryIntentActivities(intent, - PackageManager.MATCH_DEFAULT_ONLY); - Log.d(TAG, "Components matching AdServicesSettingsMainActivity: " + list); - return list != null && !list.isEmpty(); - } - - private String getAdExtServicesPackageName(@NonNull Context context) { - Objects.requireNonNull(context); - - List<PackageInfo> installedPackages = - context.getPackageManager().getInstalledPackages(PackageManager.MATCH_SYSTEM_ONLY); - - return installedPackages.stream() - .filter(s -> s.packageName.endsWith("android.ext.adservices.api")) - .map(s -> s.packageName) - .findFirst() - .orElse(null); - } -} diff --git a/java/tests/Android.bp b/java/tests/Android.bp index 4e573d9..77fe49b 100644 --- a/java/tests/Android.bp +++ b/java/tests/Android.bp @@ -9,7 +9,45 @@ package { } android_test { - name: "ExtServicesUnitTests", + name: "ExtServicesUnitTests-tplus", + + // Include all test java files. + srcs: ["src/**/*.java"], + + libs: [ + "android.test.runner", + "android.test.base", + ], + + static_libs: [ + "ExtServices-core", + "androidx.test.rules", + "compatibility-device-util-axt", + "mockito-target-extended-minus-junit4", + "androidx.test.espresso.core", + "truth", + "testables", + "testng", + ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libextservices_jni", + "libstaticjvmtiagent", + ], + test_suites: [ + "general-tests", + "mts-extservices", + "automotive-tests", + "automotive-general-tests", + ], + min_sdk_version: "33", + sdk_version: "test_current", + compile_multilib: "both", + test_config: "AndroidTest-tplus.xml", +} + +android_test { + name: "ExtServicesUnitTests-sminus", // Include all test java files. srcs: ["src/**/*.java"], @@ -43,4 +81,5 @@ android_test { min_sdk_version: "30", sdk_version: "test_current", compile_multilib: "both", + test_config: "AndroidTest-sminus.xml", } diff --git a/java/tests/AndroidTest-sminus.xml b/java/tests/AndroidTest-sminus.xml new file mode 100644 index 0000000..39d97d2 --- /dev/null +++ b/java/tests/AndroidTest-sminus.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 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. +--> +<configuration description="Runs Tests for ExtServices"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="test-file-name" value="ExtServicesUnitTests-sminus.apk" /> + </target_preparer> + + <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.extservices.apex" /> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="ExtServicesUnitTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="android.ext.services.tests.unit" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> + + <!-- Prevent test from running on Android T+ --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.MaxSdkModuleController"> + <option name="max-sdk-level" value="32"/> + </object> + + <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <option name="mainline-module-package-name" value="com.google.android.extservices" /> + </object> +</configuration> diff --git a/java/tests/AndroidTest.xml b/java/tests/AndroidTest-tplus.xml index 8202164..a9dc50f 100644 --- a/java/tests/AndroidTest.xml +++ b/java/tests/AndroidTest-tplus.xml @@ -15,10 +15,10 @@ --> <configuration description="Runs Tests for ExtServices"> <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> - <option name="test-file-name" value="ExtServicesUnitTests.apk" /> + <option name="test-file-name" value="ExtServicesUnitTests-tplus.apk" /> </target_preparer> - <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.extservices.apex" /> + <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.extservices_tplus.apex" /> <option name="test-suite-tag" value="apct" /> <option name="test-suite-tag" value="framework-base-presubmit" /> <option name="test-tag" value="ExtServicesUnitTests" /> @@ -28,6 +28,10 @@ <option name="hidden-api-checks" value="false"/> </test> + <!-- Prevent tests from running on Android S- --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController"/> + <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> <option name="mainline-module-package-name" value="com.google.android.extservices" /> </object> diff --git a/java/tests/hosttests/Android.bp b/java/tests/hosttests/Android.bp new file mode 100644 index 0000000..7630408 --- /dev/null +++ b/java/tests/hosttests/Android.bp @@ -0,0 +1,59 @@ +// Copyright (C) 2023 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_test_host { + name: "CtsExtServicesHostTests-tplus", + defaults: ["cts_defaults"], + srcs: ["src/**/*.java"], + // tag this module as a cts test artifact + test_suites: [ + "general-tests", + "mts-extservices", + ], + libs: [ + "cts-tradefed", + "tradefed", + ], + static_libs: [ + "androidx.annotation_annotation", + "adservices-host-side-test-utility", + ], + per_testcase_directory: true, + test_config: "AndroidTest-tplus.xml" +} + +java_test_host { + name: "CtsExtServicesHostTests-sminus", + defaults: ["cts_defaults"], + srcs: ["src/**/*.java"], + // tag this module as a cts test artifact + test_suites: [ + "general-tests", + "mts-extservices", + ], + libs: [ + "cts-tradefed", + "tradefed", + ], + static_libs: [ + "androidx.annotation_annotation", + "adservices-host-side-test-utility", + ], + per_testcase_directory: true, + test_config: "AndroidTest-sminus.xml" +} diff --git a/java/tests/hosttests/AndroidTest-sminus.xml b/java/tests/hosttests/AndroidTest-sminus.xml new file mode 100644 index 0000000..9b511ba --- /dev/null +++ b/java/tests/hosttests/AndroidTest-sminus.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 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. +--> +<configuration description="Runs CTS Host Tests for ExtServices"> + <option name="test-suite-tag" value="cts" /> + <option name="config-descriptor:metadata" key="component" value="framework" /> + <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" /> + <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" /> + <option name="config-descriptor:metadata" key="parameter" value="secondary_user" /> + <option name="config-descriptor:metadata" key="parameter" value="no_foldable_states" /> + <!-- Needed for correctly being picked up in presubmit --> + <option name="config-descriptor:metadata" key="mainline-param" + value="com.google.android.extservices.apex" /> + + <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > + <option name="jar" value="CtsExtServicesHostTests-sminus.jar" /> + </test> + + <!-- NOTE: DeviceConfig and SystemProperties are handled by AdServicesFlagsSetterRule --> + + <!-- Prevent tests from running on Android Q- --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.Sdk30ModuleController"/> + + <!-- Prevent test from running on Android T+ --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.MaxSdkModuleController"> + <option name="max-sdk-level" value="32"/> + </object> + + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <option name="mainline-module-package-name" value="com.google.android.extservices"/> + </object> +</configuration> diff --git a/java/tests/hosttests/AndroidTest-tplus.xml b/java/tests/hosttests/AndroidTest-tplus.xml new file mode 100644 index 0000000..7114d68 --- /dev/null +++ b/java/tests/hosttests/AndroidTest-tplus.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 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. +--> +<configuration description="Runs CTS Host Tests for ExtServices"> + <option name="test-suite-tag" value="cts" /> + <option name="config-descriptor:metadata" key="component" value="framework" /> + <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" /> + <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" /> + <option name="config-descriptor:metadata" key="parameter" value="secondary_user" /> + <option name="config-descriptor:metadata" key="parameter" value="no_foldable_states" /> + <!-- Needed for correctly being picked up in presubmit --> + <option name="config-descriptor:metadata" key="mainline-param" + value="com.google.android.extservices_tplus.apex" /> + + <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > + <option name="jar" value="CtsExtServicesHostTests-tplus.jar" /> + </test> + + <!-- NOTE: DeviceConfig and SystemProperties are handled by AdServicesFlagsSetterRule --> + + <!-- Prevent tests from running on Android S- --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController"/> + + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <option name="mainline-module-package-name" value="com.google.android.extservices"/> + </object> +</configuration> diff --git a/java/tests/hosttests/src/android/ext/services/hosttests/AdServicesFilesCleanupBootCompleteReceiverHostTest.java b/java/tests/hosttests/src/android/ext/services/hosttests/AdServicesFilesCleanupBootCompleteReceiverHostTest.java new file mode 100644 index 0000000..c3fc6f4 --- /dev/null +++ b/java/tests/hosttests/src/android/ext/services/hosttests/AdServicesFilesCleanupBootCompleteReceiverHostTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2023 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 android.ext.services.hosttests; + +import static com.android.adservices.common.TestDeviceHelper.runShellCommand; + +import static com.google.common.truth.Truth.assertWithMessage; + +import com.android.adservices.common.AdServicesHostSideFlagsSetterRule; +import com.android.adservices.common.AdServicesHostSideTestCase; +import com.android.adservices.common.BackgroundLogReceiver; +import com.android.adservices.common.HostSideSdkLevelSupportRule; +import com.android.adservices.common.RequiresSdkLevelAtLeastT; +import com.android.adservices.common.RequiresSdkLevelLessThanT; +import com.android.adservices.common.TestDeviceHelper; +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.device.PackageInfo; +import com.android.tradefed.log.LogUtil.CLog; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.Locale; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +@RunWith(DeviceJUnit4ClassRunner.class) +public class AdServicesFilesCleanupBootCompleteReceiverHostTest extends AdServicesHostSideTestCase { + private static final String EXTSERVICES_PACKAGE_SUFFIX = "android.ext.services"; + private static final String CLEANUP_RECEIVER_CLASS_NAME = + "android.ext.services.common.AdServicesFilesCleanupBootCompleteReceiver"; + private static final String LOGCAT_COMMAND = "logcat -s extservices"; + private static final String RECEIVER_DISABLED_LOG_TEXT = + "Disabled AdServices files cleanup receiver"; + private static final String RECEIVER_EXECUTED_LOG_TEXT = "AdServices files cleanup receiver"; + + private String mAdServicesFilePath; + private String mExtServicesPackageName; + + @Rule + public final HostSideSdkLevelSupportRule sdkLevelSupportRule = + HostSideSdkLevelSupportRule.forAnyLevel(); + + @Rule + public final AdServicesHostSideFlagsSetterRule flags = + AdServicesHostSideFlagsSetterRule.forGlobalKillSwitchDisabledTests() + .setLogcatTag("extservices", "VERBOSE"); + + @Before + public void setUp() throws Exception { + ITestDevice device = getDevice(); + + // Enabling the boot-completed receiver throws a SecurityException unless adb runs as root + Assume.assumeTrue("Needs adb root to enable the receiver", device.enableAdbRoot()); + + logDeviceMetadata(); + + // Find the extservices package + PackageInfo extServicesPackage = + device.getAppPackageInfos().stream() + .filter(s -> s.getPackageName().endsWith(EXTSERVICES_PACKAGE_SUFFIX)) + .findFirst() + .orElse(null); + assertWithMessage("ExtServices package").that(extServicesPackage).isNotNull(); + mExtServicesPackageName = extServicesPackage.getPackageName(); + + // Put some data in the ExtServices apk + mAdServicesFilePath = + String.format( + "/data/user/%d/%s/adservices_data.txt", + device.getCurrentUser(), extServicesPackage.getPackageName()); + runShellCommand("echo \"Hello\" > %s", mAdServicesFilePath); + assertWithMessage("%s exists", mAdServicesFilePath) + .that(device.doesFileExist(mAdServicesFilePath)) + .isTrue(); + } + + @After + public void tearDown() throws Exception { + if (mDevice != null) { + if (mAdServicesFilePath != null && mDevice.doesFileExist(mAdServicesFilePath)) { + mDevice.deleteFile(mAdServicesFilePath); + } + + mDevice.disableAdbRoot(); + } + } + + @Test + @RequiresSdkLevelLessThanT(reason = "Testing functionality that only runs on S-") + public void testReceiver_doesNotExecuteOnSMinus() throws Exception { + ITestDevice device = getDevice(); + + // TODO(b/297207132) - use a rule instead of this shell command + // Enable the flag that the receiver checks. By default, the flag is enabled in the binary, + // so it's enough to just delete the flag override, if any. + device.executeShellCommand( + "device_config delete adservices extservices_adservices_data_cleanup_enabled"); + + // Reboot, wait, and verify logs. + verifyReceiverDidNotExecute(device); + + // Verify that adservices files are still present. + assertWithMessage("%s exists", mAdServicesFilePath) + .that(device.doesFileExist(mAdServicesFilePath)) + .isTrue(); + } + + @Test + @RequiresSdkLevelAtLeastT(reason = "Testing functionality that only runs on T+") + public void testReceiver_deletesFiles() throws Exception { + ITestDevice device = getDevice(); + + // Re-enable the cleanup receiver in case it's been disabled due to a prior run + enableReceiver(); + + // Enable the flag that the receiver checks. By default, the flag is enabled in the binary, + // so it's enough to just delete the flag override, if any. + device.executeShellCommand( + "device_config delete adservices extservices_adservices_data_cleanup_enabled"); + + // Reboot, wait, and verify logs. + verifyReceiverExecuted(device); + + // Verify that all adservices files were deleted. + assertWithMessage("%s exists", mAdServicesFilePath) + .that(device.doesFileExist(mAdServicesFilePath)) + .isFalse(); + + String lsCommand = + String.format( + "ls /data/user/%d/%s -R", device.getCurrentUser(), mExtServicesPackageName); + String lsOutput = device.executeShellCommand(lsCommand).toLowerCase(Locale.ROOT); + assertWithMessage("Output of %s", lsCommand).that(lsOutput).doesNotContain("adservices"); + + // Verify that after a reboot the receiver does not execute + verifyReceiverDidNotExecute(device); + } + + @Test + @RequiresSdkLevelAtLeastT(reason = "Testing functionality that only runs on T+") + public void testReceiver_doesNotExecuteIfFlagDisabled() throws Exception { + ITestDevice device = getDevice(); + + // Re-enable the cleanup receiver in case it's been disabled due to a prior run + enableReceiver(); + + // Disable the flag that the receiver checks + device.executeShellCommand( + "device_config put adservices extservices_adservices_data_cleanup_enabled false"); + + // Verify that after a reboot the receiver executes but doesn't disable itself + BackgroundLogReceiver logcatReceiver = + rebootDeviceAndCollectLogs(device, RECEIVER_DISABLED_LOG_TEXT); + Pattern errorPattern = Pattern.compile(makePattern(RECEIVER_DISABLED_LOG_TEXT)); + assertWithMessage("Presence of log indicating receiver disabled itself") + .that(logcatReceiver.patternMatches(errorPattern)) + .isFalse(); + + // Verify that the file is still there and that the receiver didn't delete it. + assertWithMessage("%s exists", mAdServicesFilePath) + .that(device.doesFileExist(mAdServicesFilePath)) + .isTrue(); + } + + private void verifyReceiverExecuted(ITestDevice device) + throws DeviceNotAvailableException, InterruptedException { + BackgroundLogReceiver logcatReceiver = + rebootDeviceAndCollectLogs(device, RECEIVER_DISABLED_LOG_TEXT); + Pattern errorPattern = Pattern.compile(makePattern(RECEIVER_DISABLED_LOG_TEXT)); + assertWithMessage("Presence of log indicating receiver disabled itself") + .that(logcatReceiver.patternMatches(errorPattern)) + .isTrue(); + } + + private void verifyReceiverDidNotExecute(ITestDevice device) + throws DeviceNotAvailableException, InterruptedException { + BackgroundLogReceiver logcatReceiver = + rebootDeviceAndCollectLogs(device, RECEIVER_EXECUTED_LOG_TEXT); + + Pattern errorPattern = Pattern.compile(makePattern(RECEIVER_EXECUTED_LOG_TEXT)); + assertWithMessage("Presence of log indicating receiver was invoked") + .that(logcatReceiver.patternMatches(errorPattern)) + .isFalse(); + } + + private Predicate<String[]> stopIfTextOccurs(String toMatch) { + return (s) -> Arrays.stream(s).anyMatch(t -> t.contains(toMatch)); + } + + private BackgroundLogReceiver rebootDeviceAndCollectLogs(ITestDevice device, String text) + throws DeviceNotAvailableException, InterruptedException { + // reboot the device + device.reboot(); + device.waitForDeviceAvailable(); + + flags.setLogcatTag("extservices", "VERBOSE"); + + // Start log collection + BackgroundLogReceiver logcatReceiver = + new BackgroundLogReceiver.Builder() + .setDevice(device) + .setLogCatCommand(LOGCAT_COMMAND) + .setEarlyStopCondition(stopIfTextOccurs(text)) + .build(); + + // Collect logs for up to 5 minutes + boolean isEarlyExit = logcatReceiver.collectLogs(/* timeoutMilliseconds= */ 5 * 60 * 1000); + + CLog.d("Finished collecting logs. Early exit = %s, collected logs = %s", isEarlyExit, + logcatReceiver.getCollectedLogs()); + + return logcatReceiver; + } + + private String makePattern(String text) { + return ".*" + text + ".*"; + } + + private void enableReceiver() { + TestDeviceHelper.enableComponent(mExtServicesPackageName, CLEANUP_RECEIVER_CLASS_NAME); + } + + private void logDeviceMetadata() { + String apexVersion = TestDeviceHelper.runShellCommand( + "pm list packages --apex-only --show-versioncode extservices").trim(); + String apkVersion = TestDeviceHelper.runShellCommand( + "pm list packages --show-versioncode ext.services").trim(); + String privAppName = TestDeviceHelper.runShellCommand( + "ls /apex/com.android.extservices/priv-app").trim(); + CLog.d("ExtServices apex version = <%s>, apk version = <%s>, priv-app = <%s>", apexVersion, + apkVersion, privAppName); + } +} diff --git a/java/tests/src/android/ext/services/common/AdServicesAppsearchDeleteJobTest.java b/java/tests/src/android/ext/services/common/AdServicesAppsearchDeleteJobTest.java new file mode 100644 index 0000000..de7a63f --- /dev/null +++ b/java/tests/src/android/ext/services/common/AdServicesAppsearchDeleteJobTest.java @@ -0,0 +1,955 @@ +/* + * Copyright (C) 2023 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 android.ext.services.common; + + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.appsearch.app.AppSearchResult; +import androidx.appsearch.app.SetSchemaResponse; +import androidx.test.core.app.ApplicationProvider; + +import com.google.common.truth.Expect; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoSession; +import org.mockito.Spy; +import org.mockito.quality.Strictness; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public final class AdServicesAppsearchDeleteJobTest { + + private MockitoSession mMockitoSession; + + @Spy + private final Context mContext = ApplicationProvider.getApplicationContext(); + + @Spy + private AdServicesAppsearchDeleteJob mAdServicesAppsearchDeleteJob; + + @Mock + private AdservicesPhFlags mAdservicesPhFlags; + + @Mock + private SharedPreferences mSharedPreferences; + + @Mock + private SharedPreferences.Editor mEditor; + + @Rule + public final Expect expect = Expect.create(); + + public static final String TEST = "test"; + + private final Executor mExecutor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<Runnable>()); + + @Before + public void setup() { + mMockitoSession = mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .startMocking(); + doReturn(mAdservicesPhFlags).when(mAdServicesAppsearchDeleteJob).getAdservicesPhFlags(); + doReturn(mSharedPreferences).when(mAdServicesAppsearchDeleteJob).getSharedPreferences(); + doReturn(mEditor).when(mSharedPreferences).edit(); + doReturn(mEditor).when(mEditor).putLong(any(), anyLong()); + doReturn(mEditor).when(mEditor).putBoolean(any(), anyBoolean()); + doReturn(mEditor).when(mEditor).putInt(any(), anyInt()); + doReturn(true).when(mEditor).commit(); + doNothing().when(mAdServicesAppsearchDeleteJob).setReschedule(any(), anyBoolean()); + doReturn(0L).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 0L); + } + + @After + public void tearDown() { + if (mMockitoSession != null) { + mMockitoSession.finishMocking(); + } + } + + @Test + public void hasMinMinutestPassed_test() { + assertWithMessage("hasMinMinutesPassed from 1693785600L to 1000 mins should be true") + .that(mAdServicesAppsearchDeleteJob + .hasMinMinutesPassed(1693785600L, 1000)) + .isTrue(); + assertWithMessage("hasMinMinutesPassed from current time to 1000 mins" + + " should be false").that(mAdServicesAppsearchDeleteJob + .hasMinMinutesPassed(System.currentTimeMillis(), + 1000)).isFalse(); + } + + @Test + public void deleteAppsearchDb_onMigrationfailure_shouldBeFalse() + throws Exception { + SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class); + SetSchemaResponse.MigrationFailure failure = + new SetSchemaResponse.MigrationFailure( + /* namespace= */ TEST, + /* id= */ TEST, + /* schemaType= */ TEST, + /* appSearchResult= */ AppSearchResult.newFailedResult(1, TEST)); + when(mockResponse.getMigrationFailures()).thenReturn(List.of(failure)); + doReturn(mockResponse).when(mAdServicesAppsearchDeleteJob).getDeleteSchemaResponse( + any(), any(), any()); + assertWithMessage("deleteAppsearchDb result should be false") + .that(mAdServicesAppsearchDeleteJob.deleteAppsearchDb(mContext, mExecutor, TEST)) + .isFalse(); + } + + @Test + public void deleteAppsearchDb_onException_shouldBeFalse() + throws Exception { + doThrow(new RuntimeException(TEST)).when( + mAdServicesAppsearchDeleteJob).getDeleteSchemaResponse(any(), any(), any()); + assertWithMessage("deleteAppsearchDb result should be false") + .that(mAdServicesAppsearchDeleteJob.deleteAppsearchDb(mContext, mExecutor, TEST)) + .isFalse(); + } + + @Test + public void deleteAppsearchDb_onSuccess_shouldBeTrue() + throws Exception { + SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class); + when(mockResponse.getMigrationFailures()).thenReturn(new ArrayList<>()); + doReturn(mockResponse).when(mAdServicesAppsearchDeleteJob).getDeleteSchemaResponse( + any(), any(), any()); + assertWithMessage("deleteAppsearchDb result should be true") + .that(mAdServicesAppsearchDeleteJob.deleteAppsearchDb(mContext, mExecutor, TEST)) + .isTrue(); + + } + + @Test + public void onCancelJob_shouldNotReschedule() + throws Exception { + doNothing().when(mAdServicesAppsearchDeleteJob).setReschedule(any(), + anyBoolean()); + JobParameters jobParameters = mock(JobParameters.class); + doReturn(0).when(jobParameters).getJobId(); + final JobScheduler jobScheduler = mock(JobScheduler.class); + when(jobScheduler.getPendingJob(anyInt())).thenReturn(null); + when(mContext.getSystemService(JobScheduler.class)).thenReturn(jobScheduler); + doNothing().when(jobScheduler).cancel(anyInt()); + + // Execute + mAdServicesAppsearchDeleteJob.cancelPeriodicJob(mContext, jobParameters); + + // Validate + verify(jobScheduler).cancel(0); + verify(mAdServicesAppsearchDeleteJob).setReschedule(any(), eq(false)); + + } + + @Test + public void schedulePeriodic_onDisabledFlag_shouldNotSchedule() { + final JobScheduler jobScheduler = mock(JobScheduler.class); + when(jobScheduler.getPendingJob(anyInt())) + .thenReturn(null); + when(mContext.getSystemService(JobScheduler.class)).thenReturn(jobScheduler); + when(jobScheduler.schedule(any())).thenReturn(1); + doReturn(false).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + // Execute + AdServicesAppsearchDeleteJob + .scheduleAdServicesAppsearchDeletePeriodicJob( + mContext, mAdservicesPhFlags); + // Validate + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(jobScheduler, never()).schedule(any()); + + } + + @Test + public void schedulePeriodic_onExistingJob_shouldNotSchedule() { + final JobScheduler jobScheduler = mock(JobScheduler.class); + when(jobScheduler.getPendingJob(anyInt())) + .thenReturn(Mockito.mock((JobInfo.class))); + when(mContext.getSystemService(JobScheduler.class)).thenReturn(jobScheduler); + when(jobScheduler.schedule(any())).thenReturn(1); + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + // Execute + AdServicesAppsearchDeleteJob + .scheduleAdServicesAppsearchDeletePeriodicJob( + mContext, mAdservicesPhFlags); + // Validate + + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(jobScheduler).getPendingJob(anyInt()); + verify(jobScheduler, never()).schedule(any()); + + } + + @Test + public void schedulePeriodic_onNonExistingJob_EnabledJob_shouldSchedule() { + final JobScheduler jobScheduler = mock(JobScheduler.class); + when(jobScheduler.getPendingJob(anyInt())) + .thenReturn(null); + when(mContext.getSystemService(JobScheduler.class)).thenReturn(jobScheduler); + when(jobScheduler.schedule(any())).thenReturn(0); + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + doReturn(3600000L).when(mAdservicesPhFlags) + .getAppsearchDeletePeriodicIntervalMillis(); + doReturn(3600000L).when(mAdservicesPhFlags).getAppsearchDeleteJobFlexMillis(); + + final ArgumentCaptor<JobInfo> captor = ArgumentCaptor.forClass(JobInfo.class); + + // Execute + AdServicesAppsearchDeleteJob + .scheduleAdServicesAppsearchDeletePeriodicJob( + mContext, mAdservicesPhFlags); + + // Validate + verify(jobScheduler) + .getPendingJob(AdServicesAppsearchDeleteJob.JOB_ID); + verify(mAdservicesPhFlags) + .isAppsearchDeleteJobEnabled(); + verify(mAdservicesPhFlags) + .getAppsearchDeletePeriodicIntervalMillis(); + verify(mAdservicesPhFlags) + .getAppsearchDeleteJobFlexMillis(); + verify(jobScheduler).schedule(captor.capture()); + assertNotNull(captor.getValue()); + assertEquals(AdServicesAppsearchDeleteJob.JOB_ID, + captor.getValue().getId()); + assertEquals("android.ext.services.common" + + ".AdServicesAppsearchDeleteJob", + captor.getValue().getService().getClassName()); + assertTrue(captor.getValue().isPersisted()); + assertEquals(3600000L, captor.getValue().getIntervalMillis()); + assertEquals(3600000L, captor.getValue().getFlexMillis()); + + } + + @Test + public void onStartJob_disabledJob() + throws Exception { + doReturn(false).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + // Wanted + verify(mAdservicesPhFlags) + .isAppsearchDeleteJobEnabled(); + verify(mAdServicesAppsearchDeleteJob) + .cancelPeriodicJob(any(), any()); + + // Not Wanted + verify(mSharedPreferences, never()) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdServicesAppsearchDeleteJob, never()) + .isAppsearchDbEmpty(any(), any(), any()); + verify(mAdservicesPhFlags, never()).isAdServicesEnabled(); + + } + + @Test + public void onStartJob_onDoNothing() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + doReturn(true).when(mAdservicesPhFlags) + .shouldDoNothingAdServicesAppsearchDeleteJob(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + // Wanted + verify(mAdservicesPhFlags) + .isAppsearchDeleteJobEnabled(); + + // Not Wanted + verify(mSharedPreferences, never()) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdServicesAppsearchDeleteJob, never()) + .isAppsearchDbEmpty(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()) + .cancelPeriodicJob(any(), any()); + verify(mAdservicesPhFlags, never()).isAdServicesEnabled(); + + } + + @Test + public void onStartJob_enabledJob_firstTimeOta_appsearchDataNotFound() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + doReturn(true).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + doReturn(0L).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + // Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mEditor) + .putLong(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE), anyLong()); + verify(mAdServicesAppsearchDeleteJob).isAppsearchDbEmpty(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob).cancelPeriodicJob(any(), any()); + verify(mEditor) + .putBoolean(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND, + false); + + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mAdServicesAppsearchDeleteJob, never()).hasMinMinutesPassed(anyLong(), anyLong()); + verify(mAdservicesPhFlags, never()).isAdServicesEnabled(); + verify(mAdServicesAppsearchDeleteJob, never()).deleteAppsearchDbs(any(), any(), any()); + } + + @Test + public void onStartJob_enabledJob_firstTimeOta_appsearchDataFound() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + doReturn(0L).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + // Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mEditor) + .putLong(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE), anyLong()); + verify(mAdServicesAppsearchDeleteJob).isAppsearchDbEmpty(any(), any(), any()); + verify(mEditor) + .putBoolean(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND, + true); + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mAdservicesPhFlags, never()).getMinMinutesFromOtaToDeleteAppsearchData(); + verify(mAdservicesPhFlags, never()).isAdServicesEnabled(); + verify(mAdServicesAppsearchDeleteJob, never()).cancelPeriodicJob(any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()).setReschedule(jobParameters, false); + verify(mAdServicesAppsearchDeleteJob, never()).hasMinMinutesPassed(anyLong(), anyLong()); + verify(mAdServicesAppsearchDeleteJob, never()).deleteAppsearchDbs(any(), any(), any()); + } + + @Test + public void onStartJob_enabledJob_minMinutesFromOTAPassed_shouldDelete() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + + + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + + long otaDate = 1693785600L; + long minMinutesFromOtaToTToDelete = 100L; + doReturn(otaDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + + doReturn(minMinutesFromOtaToTToDelete).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToDeleteAppsearchData(); + doReturn(true).when(mAdServicesAppsearchDeleteJob) + .deleteAppsearchDbs(any(), any(), any()); + doNothing().when(mAdServicesAppsearchDeleteJob).cancelPeriodicJob(any(), any()); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + //Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToDeleteAppsearchData(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesFromOtaToTToDelete); + verify(mAdServicesAppsearchDeleteJob).deleteAppsearchDbs(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob).cancelPeriodicJob(any(), any()); + verify(mEditor).putLong( + eq(AdServicesAppsearchDeleteJob + .SHARED_PREFS_KEY_ADSERVICES_APPSEARCH_DELETED), + anyLong()); + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mEditor, never()) + .putLong(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE), anyLong()); + verify(mAdServicesAppsearchDeleteJob, never()).isAppsearchDbEmpty(any(), any(), any()); + verify(mAdservicesPhFlags, never()).getMinMinutesFromOtaToCheckAdServicesStatus(); + verify(mAdservicesPhFlags, never()).isAdServicesEnabled(); + verify(mEditor, never()) + .putBoolean(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND), + anyBoolean()); + } + + @Test + public void onStartJob_enabledJob_deleteDbsException_shouldNotCancel() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + doReturn(5).when(mAdservicesPhFlags).getMaxAppsearchAdServicesDeleteAttempts(); + doReturn(1).when(mSharedPreferences) + .getInt(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ATTEMPTED_DELETE_COUNT, 0); + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + + long otaDate = 1693785600L; + long minMinutesFromOtaToTToDelete = 100L; + doReturn(otaDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + doReturn(minMinutesFromOtaToTToDelete).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToDeleteAppsearchData(); + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .deleteAppsearchDbs(any(), any(), any()); + doNothing().when(mAdServicesAppsearchDeleteJob).cancelPeriodicJob(any(), any()); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + // Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToDeleteAppsearchData(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesFromOtaToTToDelete); + verify(mAdServicesAppsearchDeleteJob).deleteAppsearchDbs(any(), any(), any()); + + verify(mEditor).putInt(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ATTEMPTED_DELETE_COUNT, + 2); + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mEditor, never()) + .putLong(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE), anyLong()); + verify(mAdServicesAppsearchDeleteJob, never()).isAppsearchDbEmpty(any(), any(), any()); + verify(mAdservicesPhFlags, never()).getMinMinutesFromOtaToCheckAdServicesStatus(); + verify(mAdServicesAppsearchDeleteJob, never()).cancelPeriodicJob(any(), any()); + verify(mEditor, never()).putLong( + eq(AdServicesAppsearchDeleteJob + .SHARED_PREFS_KEY_ADSERVICES_APPSEARCH_DELETED), + anyLong()); + verify(mAdservicesPhFlags, never()).isAdServicesEnabled(); + verify(mEditor, never()) + .putBoolean(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND), + anyBoolean()); + + } + + @Test + public void onStartJob_enabledJob_deleteDbsException_onMaxAttempts_shouldCancel() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + doReturn(5).when(mAdservicesPhFlags).getMaxAppsearchAdServicesDeleteAttempts(); + doReturn(4).when(mSharedPreferences) + .getInt(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ATTEMPTED_DELETE_COUNT, 0); + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + + long otaDate = 1693785600L; + long minMinutesFromOtaToTToDelete = 100L; + doReturn(otaDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + + doReturn(minMinutesFromOtaToTToDelete).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToDeleteAppsearchData(); + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .deleteAppsearchDbs(any(), any(), any()); + doNothing().when(mAdServicesAppsearchDeleteJob).cancelPeriodicJob(any(), any()); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + //Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToDeleteAppsearchData(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesFromOtaToTToDelete); + verify(mAdServicesAppsearchDeleteJob).deleteAppsearchDbs(any(), any(), any()); + verify(mEditor).putInt(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ATTEMPTED_DELETE_COUNT, + 5); + verify(mAdServicesAppsearchDeleteJob).cancelPeriodicJob(any(), any()); + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mEditor, never()) + .putLong(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE), anyLong()); + verify(mAdServicesAppsearchDeleteJob, never()) + .isAppsearchDbEmpty(any(), any(), any()); + verify(mAdservicesPhFlags, never()).getMinMinutesFromOtaToCheckAdServicesStatus(); + verify(mEditor, never()).putLong( + eq(AdServicesAppsearchDeleteJob + .SHARED_PREFS_KEY_ADSERVICES_APPSEARCH_DELETED), + anyLong()); + verify(mAdservicesPhFlags, never()).isAdServicesEnabled(); + verify(mEditor, never()) + .putBoolean(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND), + anyBoolean()); + + } + + @Test + public void onStartJob_enabledJob_minMinutesToCheckAdServicesNotPassed_shouldDoNothing() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + + long otaDate = System.currentTimeMillis(); + long minMinutesFromOtaToTToDelete = 1000L; + doReturn(otaDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + doReturn(minMinutesFromOtaToTToDelete).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToDeleteAppsearchData(); + long minMinutesToCheckAdServices = 100L; + doReturn(minMinutesToCheckAdServices).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToCheckAdServicesStatus(); + doNothing().when(mAdServicesAppsearchDeleteJob).cancelPeriodicJob(any(), any()); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + // Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToDeleteAppsearchData(); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToCheckAdServicesStatus(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesFromOtaToTToDelete); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesToCheckAdServices); + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mEditor, never()) + .putLong(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE), anyLong()); + verify(mAdServicesAppsearchDeleteJob, never()) + .isAppsearchDbEmpty(any(), any(), any()); + verify(mAdservicesPhFlags, never()).isAdServicesEnabled(); + verify(mEditor, never()) + .putBoolean(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND), + anyBoolean()); + verify(mAdServicesAppsearchDeleteJob, never()).deleteAppsearchDbs(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()).cancelPeriodicJob(any(), any()); + } + + @Test + public void onStartJob_enabledJob_adservicesDisabled_shouldNotDelete() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + + long otaDate = System.currentTimeMillis(); + long minMinutesFromOtaToTToDelete = 10000L; + doReturn(otaDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + doReturn(minMinutesFromOtaToTToDelete).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToDeleteAppsearchData(); + doReturn(true).when(mAdServicesAppsearchDeleteJob) + .deleteAppsearchDbs(any(), any(), any()); + doReturn(0L).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, + 0L); + doReturn(false).when(mAdservicesPhFlags).isAdServicesEnabled(); + long minMinutesToCheckAdServices = 0; + doReturn(minMinutesToCheckAdServices).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToCheckAdServicesStatus(); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + //Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToDeleteAppsearchData(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesFromOtaToTToDelete); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToCheckAdServicesStatus(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesToCheckAdServices); + verify(mAdservicesPhFlags).isAdServicesEnabled(); + + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mAdServicesAppsearchDeleteJob, never()).isAppsearchDbEmpty(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()).deleteAppsearchDbs(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()).cancelPeriodicJob(any(), any()); + verify(mEditor, never()) + .putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0); + verify(mEditor, never()) + .putBoolean(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND), + anyBoolean()); + + } + + @Test + public void onStartJob_enabledJob_adservicesEnabledFirstTime_shouldNotDelete() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + + long otaDate = System.currentTimeMillis(); + long minMinutesFromOtaToTToDelete = 10000L; + doReturn(otaDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + doReturn(minMinutesFromOtaToTToDelete).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToDeleteAppsearchData(); + + doReturn(true).when(mAdservicesPhFlags).isAdServicesEnabled(); + doReturn(0L).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, + 0L); + long minMinutesToCheckAdServices = 0L; + doReturn(minMinutesToCheckAdServices).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToCheckAdServicesStatus(); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + //Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToDeleteAppsearchData(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesFromOtaToTToDelete); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToCheckAdServicesStatus(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesToCheckAdServices); + verify(mAdservicesPhFlags).isAdServicesEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, + 0L); + verify(mAdservicesPhFlags).isAdServicesEnabled(); + verify(mEditor).putLong( + eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE), + anyLong()); + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mEditor, never()) + .putLong(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE), anyLong()); + verify(mAdServicesAppsearchDeleteJob, never()).isAppsearchDbEmpty(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()).deleteAppsearchDbs(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()).cancelPeriodicJob(any(), any()); + verify(mAdservicesPhFlags, never()).getMinMinutesToDeleteFromAdServicesEnabled(); + verify(mEditor, never()) + .putBoolean(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND), + anyBoolean()); + + } + + @Test + public void onStartJob_enabledJob_adservicesEnabled_MinMinsNotPassed_shouldNotDelete() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + + long otaDate = System.currentTimeMillis(); + long minMinutesFromOtaToTToDelete = 10000L; + doReturn(otaDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + doReturn(minMinutesFromOtaToTToDelete).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToDeleteAppsearchData(); + + doReturn(true).when(mAdservicesPhFlags).isAdServicesEnabled(); + long adServicesDate = System.currentTimeMillis(); + doReturn(adServicesDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, + 0L); + long minMinsForAdServicesEnabled = 9000000L; + doReturn(minMinsForAdServicesEnabled).when(mAdservicesPhFlags) + .getMinMinutesToDeleteFromAdServicesEnabled(); + long minMinutesToCheckAdServices = 0; + doReturn(minMinutesToCheckAdServices).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToCheckAdServicesStatus(); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + // Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesFromOtaToTToDelete); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToCheckAdServicesStatus(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesToCheckAdServices); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, + 0L); + verify(mAdservicesPhFlags).isAdServicesEnabled(); + verify(mAdservicesPhFlags).getMinMinutesToDeleteFromAdServicesEnabled(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(adServicesDate, minMinsForAdServicesEnabled); + + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mEditor, never()) + .putLong(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE), anyLong()); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToDeleteAppsearchData(); + verify(mAdServicesAppsearchDeleteJob, never()).isAppsearchDbEmpty(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()).deleteAppsearchDbs(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()).cancelPeriodicJob(any(), any()); + verify(mEditor, never()).putLong( + eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE), + anyLong()); + verify(mEditor, never()) + .putBoolean(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND), + anyBoolean()); + + } + + @Test + public void onStartJob_enabledJob_adservicesDisabled_PostEnabled_shouldOverrideEnabledDate() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + + long otaDate = System.currentTimeMillis(); + long minMinutesFromOtaToTToDelete = 10000L; + doReturn(otaDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + doReturn(minMinutesFromOtaToTToDelete).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToDeleteAppsearchData(); + + doReturn(false).when(mAdservicesPhFlags).isAdServicesEnabled(); + long adServicesDate = System.currentTimeMillis(); + doReturn(adServicesDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, + 0L); + long minMinsForAdServicesEnabled = 0L; + doReturn(minMinsForAdServicesEnabled).when(mAdservicesPhFlags) + .getMinMinutesToDeleteFromAdServicesEnabled(); + long minMinutesToCheckAdServices = 0; + doReturn(minMinutesToCheckAdServices).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToCheckAdServicesStatus(); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + //Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesFromOtaToTToDelete); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToCheckAdServicesStatus(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesToCheckAdServices); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, + 0L); + verify(mEditor) + .putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, + 0); + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mEditor, never()) + .putLong(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE), anyLong()); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToDeleteAppsearchData(); + verify(mAdServicesAppsearchDeleteJob, never()).isAppsearchDbEmpty(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()).deleteAppsearchDbs(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob, never()).cancelPeriodicJob(any(), any()); + verify(mEditor, never()) + .putBoolean(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND), + anyBoolean()); + } + + @Test + public void onStartJob_enabledJob_adservicesEnabled_MinMinsPassed_shouldDelete() + throws Exception { + doReturn(true).when(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + + JobParameters jobParameters = mock(JobParameters.class); + doReturn(AdServicesAppsearchDeleteJob.JOB_ID).when(jobParameters).getJobId(); + doReturn(false).when(mAdServicesAppsearchDeleteJob) + .isAppsearchDbEmpty(any(), any(), any()); + doReturn(true).when(mAdServicesAppsearchDeleteJob) + .deleteAppsearchDbs(any(), any(), any()); + long otaDate = System.currentTimeMillis(); + long minMinutesFromOtaToTToDelete = 10000L; + doReturn(otaDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + doReturn(minMinutesFromOtaToTToDelete).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToDeleteAppsearchData(); + + doReturn(true).when(mAdservicesPhFlags).isAdServicesEnabled(); + long adServicesDate = 1693785600L; + doReturn(adServicesDate).when(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, + 0L); + long minMinsForAdServicesEnabled = 10L; + doReturn(minMinsForAdServicesEnabled).when(mAdservicesPhFlags) + .getMinMinutesToDeleteFromAdServicesEnabled(); + long minMinutesToCheckAdServices = 0L; + doReturn(minMinutesToCheckAdServices).when(mAdservicesPhFlags) + .getMinMinutesFromOtaToCheckAdServicesStatus(); + doNothing().when(mAdServicesAppsearchDeleteJob).cancelPeriodicJob(any(), any()); + + // Execute + mAdServicesAppsearchDeleteJob.onStartJob(jobParameters); + + // Validate + // Wanted + verify(mAdservicesPhFlags).isAppsearchDeleteJobEnabled(); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE, 0L); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToDeleteAppsearchData(); + verify(mAdservicesPhFlags).getMinMinutesFromOtaToCheckAdServicesStatus(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(otaDate, minMinutesFromOtaToTToDelete); + verify(mSharedPreferences) + .getLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE, + 0L); + verify(mAdservicesPhFlags).isAdServicesEnabled(); + verify(mAdservicesPhFlags).getMinMinutesToDeleteFromAdServicesEnabled(); + verify(mAdServicesAppsearchDeleteJob) + .hasMinMinutesPassed(adServicesDate, minMinsForAdServicesEnabled); + verify(mAdServicesAppsearchDeleteJob).deleteAppsearchDbs(any(), any(), any()); + verify(mAdServicesAppsearchDeleteJob).cancelPeriodicJob(any(), any()); + verify(mEditor).putLong( + eq(AdServicesAppsearchDeleteJob + .SHARED_PREFS_KEY_ADSERVICES_APPSEARCH_DELETED), + anyLong()); + verify(mEditor).putLong(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_JOB_RUN_COUNT, 1); + verify(mEditor).commit(); + + // Not Wanted + verify(mEditor, never()) + .putLong(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_OTA_DATE), anyLong()); + verify(mAdServicesAppsearchDeleteJob, never()) + .isAppsearchDbEmpty(any(), any(), any()); + verify(mEditor, never()).putLong( + eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_ADSERVICES_ENABLED_DATE), + anyLong()); + verify(mEditor, never()) + .putBoolean(eq(AdServicesAppsearchDeleteJob.SHARED_PREFS_KEY_APPSEARCH_DATA_FOUND), + anyBoolean()); + + } + +} diff --git a/java/tests/src/android/ext/services/common/AdServicesFilesCleanupBootCompleteReceiverTest.java b/java/tests/src/android/ext/services/common/AdServicesFilesCleanupBootCompleteReceiverTest.java new file mode 100644 index 0000000..82e3014 --- /dev/null +++ b/java/tests/src/android/ext/services/common/AdServicesFilesCleanupBootCompleteReceiverTest.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2023 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 android.ext.services.common; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.sqlite.SQLiteDatabase; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import com.google.common.truth.Expect; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.Spy; +import org.mockito.quality.Strictness; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.List; + +public final class AdServicesFilesCleanupBootCompleteReceiverTest { + private static final String ADSERVICES_FILE_NAME = "adservices_file"; + private static final String NON_ADSERVICES_FILE_NAME = "some_other_file"; + private static final String NON_ADSERVICES_FILE_WITH_PREFIX_IN_NAME = + "some_file_with_adservices_in_name"; + private static final String ADSERVICES_FILE_NAME_MIXED_CASE = "AdServicesFileMixedCase.txt"; + private static final String NON_ADSERVICE_FILE_NAME_2 = "adservice_but_no_s.txt"; + + // Update this list with the previous name every time the receiver is renamed + private static final List<String> PREVIOUSLY_USED_CLASS_NAMES = List.of(); + + // TODO(b/297207132): Replace with AdServicesExtendedMockitoRule + private MockitoSession mMockitoSession; + + @Spy + private final Context mContext = ApplicationProvider.getApplicationContext(); + + @Spy + private AdServicesFilesCleanupBootCompleteReceiver mReceiver; + + @Mock + private PackageManager mPackageManager; + + @Rule + public final Expect expect = Expect.create(); + + @Before + public void setup() { + mMockitoSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .strictness(Strictness.WARN) + .startMocking(); + + doReturn(mPackageManager).when(mContext).getPackageManager(); + doNothing().when(mReceiver).scheduleAppsearchDeleteJob(any()); + } + + @After + public void tearDown() { + if (mMockitoSession != null) { + mMockitoSession.finishMocking(); + } + } + + @Test + public void testReceiverDoesNotReuseClassNames() { + assertThat(PREVIOUSLY_USED_CLASS_NAMES) + .doesNotContain(AdServicesFilesCleanupBootCompleteReceiver.class.getName()); + } + + @Test + public void testReceiverSkipsDeletionIfDisabled() { + mockReceiverEnabled(false); + + mReceiver.onReceive(mContext, /* intent= */ null); + + verify(mContext, never()).getDataDir(); + verify(mContext, never()).getPackageManager(); + verify(mReceiver, never()).scheduleAppsearchDeleteJob(any()); + } + + @Test + public void testReceiverDisablesItselfIfDeleteSuccessful() { + mockReceiverEnabled(true); + doNothing().when(mPackageManager).setComponentEnabledSetting(any(), anyInt(), anyInt()); + doReturn(true).when(mReceiver).deleteAdServicesFiles(any()); + + mReceiver.onReceive(mContext, /* intent= */ null); + verify(mReceiver).scheduleAppsearchDeleteJob(any()); + verifyDisableComponentCalled(); + } + + @Test + public void testReceiverDisablesItselfIfDeleteUnsuccessful() { + mockReceiverEnabled(true); + doReturn(false).when(mReceiver).deleteAdServicesFiles(any()); + + mReceiver.onReceive(mContext, /* intent= */ null); + verify(mReceiver).scheduleAppsearchDeleteJob(any()); + verifyDisableComponentCalled(); + } + + @Test + public void testReceiverDeletesAdServicesFiles() throws Exception { + List<String> adServicesNames = List.of(ADSERVICES_FILE_NAME, + ADSERVICES_FILE_NAME_MIXED_CASE); + List<String> nonAdServicesNames = List.of(NON_ADSERVICES_FILE_NAME, + NON_ADSERVICES_FILE_WITH_PREFIX_IN_NAME, NON_ADSERVICE_FILE_NAME_2); + + try { + createFiles(adServicesNames); + createFiles(nonAdServicesNames); + createDatabases(adServicesNames); + createDatabases(nonAdServicesNames); + + mReceiver.deleteAdServicesFiles(mContext.getDataDir()); + + // Check if the appropriate files were deleted + String[] remainingFiles = mContext.getFilesDir().list(); + List<String> remainingFilesList = Arrays.asList(remainingFiles); + expect.that(remainingFilesList).containsNoneIn(adServicesNames); + expect.that(remainingFilesList).containsAtLeastElementsIn(nonAdServicesNames); + expectDatabasesExist(nonAdServicesNames); + expectDatabasesDoNotExist(adServicesNames); + } finally { + deleteFiles(adServicesNames); + deleteFiles(nonAdServicesNames); + deleteDatabases(adServicesNames); + deleteDatabases(nonAdServicesNames); + } + } + + @Test + public void testReceiverDeletesAdServicesDirectories() throws Exception { + String dataRoot = "data_root"; + Path root = mContext.getFilesDir().toPath(); + + try { + File file1 = createFile(root, dataRoot, "level_1.txt"); // Preserved + File file2 = createFile(root, dataRoot, "adservices_level_1.txt"); // Deleted + File file3 = createFile(root, dataRoot + "/non_adservices", + "level_2.txt"); // Preserved + File file4 = createFile(root, dataRoot + "/non_adservices", + "adservices_level_2.txt"); // Deleted + File file5 = createFile(root, dataRoot + "/non_adservices/adservices_nested", + "level_3.txt"); // Deleted + File file6 = createFile(root, dataRoot + "/non_adservices/adservices_nested", + "adservices.level_3.txt"); // Deleted + File file7 = createFile(root, dataRoot + "/non_adservices", + "AdServices_level_2.txt"); // Deleted + File file8 = createFile(root, dataRoot + "/adservices-data", + "level_2.txt"); // Deleted + File file9 = createFile(root, dataRoot + "/adservices-data/nested", + "level_3.txt"); // Deleted + File file10 = createFile(root, dataRoot + "/AdServices-data/nested", + "level_3_1.txt"); + + mReceiver.deleteAdServicesFiles(mContext.getDataDir()); + + expectFilesExist(file1, file3); + expectFilesDoNotExist(file2, file4, file5, file6, file7, file8, file9, file10); + } finally { + deletePathRecursively(root.resolve(dataRoot)); + } + } + + @Test + public void testReceiverHandlesSecurityException() { + // Simulate a directory with three files, and the first one throws an exception on delete + File file1 = mock(File.class); + doReturn(ADSERVICES_FILE_NAME).when(file1).getName(); + doThrow(SecurityException.class).when(file1).delete(); + + File file2 = mock(File.class); + doReturn(ADSERVICES_FILE_NAME_MIXED_CASE).when(file2).getName(); + + File file3 = mock(File.class); + doReturn(NON_ADSERVICES_FILE_NAME).when(file3).getName(); + + File dir = mock(File.class); + doReturn(true).when(dir).isDirectory(); + doReturn(new File[] { file1, file2, file3 }).when(dir).listFiles(); + + // Execute the receiver + mReceiver.deleteAdServicesFiles(dir); + + // Verify that deletion of both file1 and file2 was attempted, in spite of the exception + verify(file1).delete(); + verify(file2).delete(); + verify(file3, never()).delete(); + } + + @Test + public void testDeleteAdServicesFiles_invalidInput() { + // Null input + assertThat(mReceiver.deleteAdServicesFiles(null)).isTrue(); + + // Not a directory + File file = mock(File.class); + assertThat(mReceiver.deleteAdServicesFiles(file)).isTrue(); + verify(file, never()).listFiles(); + + // Throws an exception + File file2 = mock(File.class); + doThrow(SecurityException.class).when(file2).isDirectory(); + assertThat(mReceiver.deleteAdServicesFiles(file2)).isFalse(); + verify(file2, never()).listFiles(); + } + + private void mockReceiverEnabled(boolean value) { + doReturn(value).when(mReceiver).isReceiverEnabled(); + } + + private void verifyDisableComponentCalled() { + verify(mPackageManager).setComponentEnabledSetting(any(), + eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), eq(0)); + } + + private void expectFilesExist(File... files) { + for (File file: files) { + expect.withMessage("%s exists", file.getPath()).that(file.exists()).isTrue(); + } + } + + private void expectFilesDoNotExist(File... files) { + for (File file: files) { + expect.withMessage("%s exists", file.getPath()).that(file.exists()).isFalse(); + } + } + + private void expectDatabasesExist(List<String> databaseNames) { + for (String db: databaseNames) { + expect.withMessage("%s exists", db) + .that(mContext.getDatabasePath(db).exists()) + .isTrue(); + } + } + + private void expectDatabasesDoNotExist(List<String> databaseNames) { + for (String db: databaseNames) { + expect.withMessage("%s exists", db) + .that(mContext.getDatabasePath(db).exists()) + .isFalse(); + } + } + + private void createFiles(List<String> names) throws Exception { + File dir = mContext.getFilesDir(); + for (String name : names) { + createFile(name, dir); + } + } + + private void createDatabases(List<String> names) { + for (String name : names) { + try (SQLiteDatabase unused = mContext.openOrCreateDatabase(name, 0, null)) { + // Intentionally do nothing. + } + } + } + + private void deleteFiles(List<String> names) { + for (String name : names) { + File file = new File(mContext.getFilesDir(), name); + if (file.exists()) { + file.delete(); + } + } + } + + private void deleteDatabases(List<String> names) { + for (String name : names) { + mContext.deleteDatabase(name); + } + } + + private File createFile(String name, File directory) throws Exception { + File file = new File(directory, name); + try (FileWriter writer = new FileWriter(file)) { + writer.append("test data"); + writer.flush(); + } + + return file; + } + + private File createFile(Path root, String path, String fileName) throws Exception { + Path dir = root.resolve(path); + Files.createDirectories(dir); + return createFile(fileName, dir.toFile()); + } + + private void deletePathRecursively(Path path) throws Exception { + Files.walkFileTree(path, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } +} diff --git a/java/tests/src/android/ext/services/common/BootCompletedReceiverTest.java b/java/tests/src/android/ext/services/common/BootCompletedReceiverTest.java deleted file mode 100644 index 8a58a9f..0000000 --- a/java/tests/src/android/ext/services/common/BootCompletedReceiverTest.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright (C) 2023 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 android.ext.services.common; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.any; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.eq; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.os.Build; - -import androidx.test.core.app.ApplicationProvider; - -import com.android.dx.mockito.inline.extended.ExtendedMockito; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoSession; -import org.mockito.Spy; -import org.mockito.quality.Strictness; - -import java.util.List; - -public class BootCompletedReceiverTest { - private static final String ADSERVICES_EXT_PACKAGE_NAME = "com.android.ext.adservices.api"; - - private MockitoSession mMockitoSession; - - @Spy - private final Context mContext = ApplicationProvider.getApplicationContext(); - - @Spy - private BootCompletedReceiver mReceiver; - - @Mock - private PackageManager mPackageManager; - - @Before - public void setup() { - mMockitoSession = ExtendedMockito.mockitoSession() - .initMocks(this) - .strictness(Strictness.WARN) - .startMocking(); - - doReturn(mPackageManager).when(mContext).getPackageManager(); - } - - @After - public void tearDown() { - if (mMockitoSession != null) { - mMockitoSession.finishMocking(); - } - } - - @Test - public void testReceiverSkipsBroadcastIfDisabled() { - mockReceiverEnabled(false); - - mReceiver.onReceive(mContext, null); - - verify(mContext, never()).getPackageManager(); - verify(mContext, never()).sendBroadcast(any()); - } - - @Test - public void testReceiverSkipsBroadcastIfNoPackages() { - mockReceiverEnabled(true); - doReturn(List.of()).when(mPackageManager).getInstalledPackages(anyInt()); - - mReceiver.onReceive(mContext, null); - - verify(mContext, never()).sendBroadcast(any()); - verify(mPackageManager, never()).setComponentEnabledSetting(any(), anyInt(), anyInt()); - } - - @Test - public void testReceiverSkipsBroadcastIfNoPackagesMatchingAdServices() { - mockReceiverEnabled(true); - - PackageInfo one = new PackageInfo(); - one.packageName = "one"; - PackageInfo two = new PackageInfo(); - two.packageName = "external.adservices.invalid.api"; - doReturn(List.of(one, two)).when(mPackageManager).getInstalledPackages(anyInt()); - - mReceiver.onReceive(mContext, null); - - verify(mContext, never()).sendBroadcast(any()); - verify(mPackageManager, never()).setComponentEnabledSetting(any(), anyInt(), anyInt()); - } - - @Test - public void testReceiverResendsBroadcast() { - mockReceiverEnabled(true); - mockAdServicesPackageName(); - doReturn(false).when(mReceiver).isAtLeastT(); - mockExcludedDevices(""); - - mReceiver.onReceive(mContext, null); - - verifyBroadcastSent(); - } - - @Test - public void testReceiverDisablesItselfOnTPlusIfAdServicesDisabled() { - mockReceiverEnabled(true); - mockAdServicesPackageName(); - doReturn(true).when(mReceiver).isAtLeastT(); - doReturn(List.of()).when(mPackageManager).queryIntentActivities(any(), anyInt()); - - mReceiver.onReceive(mContext, null); - - verify(mPackageManager).setComponentEnabledSetting(any(), - eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), eq(0)); - verify(mContext, never()).sendBroadcast(any()); - } - - @Test - public void testReceiverDoesNotDisableItselfOnTPlusIfAdServicesEnabled() { - mockReceiverEnabled(true); - mockAdServicesPackageName(); - doReturn(true).when(mReceiver).isAtLeastT(); - mockExcludedDevices(""); - - ResolveInfo info = new ResolveInfo(); - info.activityInfo = new ActivityInfo(); - info.activityInfo.packageName = "test"; - info.activityInfo.name = "test2"; - doReturn(List.of(info)).when(mPackageManager).queryIntentActivities(any(), anyInt()); - - mReceiver.onReceive(mContext, null); - - verify(mPackageManager, never()).setComponentEnabledSetting(any(), anyInt(), anyInt()); - verifyBroadcastSent(); - } - - @Test - public void testReceiverShouldNotDisableItselfOnSMinus() { - mockReceiverEnabled(true); - mockAdServicesPackageName(); - doReturn(false).when(mReceiver).isAtLeastT(); - mockExcludedDevices(""); - - mReceiver.onReceive(mContext, null); - - verify(mPackageManager, never()).queryIntentActivities(any(), anyInt()); - verify(mPackageManager, never()).setComponentEnabledSetting(any(), anyInt(), anyInt()); - verifyBroadcastSent(); - } - - @Test - public void testReceiverSkipsBroadcastIfFingerprintExcludedExactly() { - mockReceiverEnabled(true); - mockAdServicesPackageName(); - doReturn(false).when(mReceiver).isAtLeastT(); - mockExcludedDevices(Build.FINGERPRINT); - - mReceiver.onReceive(mContext, null); - - verify(mContext, never()).sendBroadcast(any()); - } - - @Test - public void testReceiverSkipsBroadcastIfFingerprintExcludedPrefix() { - mockReceiverEnabled(true); - mockAdServicesPackageName(); - doReturn(false).when(mReceiver).isAtLeastT(); - - String currentBuild = Build.FINGERPRINT; - if (currentBuild.length() > 1) { - currentBuild = currentBuild.substring(0, currentBuild.length() - 2); - } - mockExcludedDevices(currentBuild); - - mReceiver.onReceive(mContext, null); - - verify(mContext, never()).sendBroadcast(any()); - } - - @Test - public void testReceiverSkipsBroadcastIfFingerprintExcludedTrim() { - mockReceiverEnabled(true); - mockAdServicesPackageName(); - doReturn(false).when(mReceiver).isAtLeastT(); - mockExcludedDevices(" " + Build.FINGERPRINT + " "); - - mReceiver.onReceive(mContext, null); - - verify(mContext, never()).sendBroadcast(any()); - } - - @Test - public void testReceiverSkipsBroadcastIfFingerprintExcludedInList() { - mockReceiverEnabled(true); - mockAdServicesPackageName(); - doReturn(false).when(mReceiver).isAtLeastT(); - mockExcludedDevices("one, " + Build.FINGERPRINT + ", two"); - - mReceiver.onReceive(mContext, null); - - verify(mContext, never()).sendBroadcast(any()); - } - - private void mockAdServicesPackageName() { - PackageInfo pkg = new PackageInfo(); - pkg.packageName = ADSERVICES_EXT_PACKAGE_NAME; - doReturn(List.of(pkg)).when(mPackageManager).getInstalledPackages(anyInt()); - } - - private void mockReceiverEnabled(boolean value) { - doReturn(value).when(mReceiver).isReceiverEnabled(); - } - - private void mockExcludedDevices(String value) { - doReturn(value).when(mReceiver).getExcludedFingerprints(); - } - - private void verifyBroadcastSent() { - ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); - verify(mContext).sendBroadcast(captor.capture()); - verify(mPackageManager, never()).setComponentEnabledSetting(any(), anyInt(), anyInt()); - - ComponentName componentName = captor.getValue().getComponent(); - assertThat(componentName.getPackageName()).isEqualTo(ADSERVICES_EXT_PACKAGE_NAME); - assertThat(componentName.getShortClassName()).isEqualTo( - "com.android.adservices.service.common.AdExtBootCompletedReceiver"); - } -} diff --git a/native/tests/Android.bp b/native/tests/Android.bp index ae8451f..b47a245 100644 --- a/native/tests/Android.bp +++ b/native/tests/Android.bp @@ -25,7 +25,46 @@ package { // Run this test using "atest libextservices_test -- --all-abi" or it will fail to run on 64 bit // devices. cc_test { - name: "libextservices_test", + name: "libextservices_test-tplus", + test_suites: [ + "general-tests", + "mts-extservices", + ], + + shared_libs: [ + "libandroid", + "liblog", + ], + static_libs: [ + "libbase", + "libextservices", + "libfft2d", + "libgmock_main", + "libgmock", + "libgtest", + ], + + cflags: [ + "-Wall", + "-Werror", + ], + + srcs: [ + "*.cpp", + ], + + data: [ + "test_data/*.raw", + ], + compile_multilib: "prefer32", + min_sdk_version: "33", + test_config: "AndroidTest-tplus.xml", +} + +// Run this test using "atest libextservices_test -- --all-abi" or it will fail to run on 64 bit +// devices. +cc_test { + name: "libextservices_test-sminus", test_suites: [ "general-tests", "mts-extservices", @@ -58,4 +97,5 @@ cc_test { ], compile_multilib: "prefer32", min_sdk_version: "30", + test_config: "AndroidTest-sminus.xml", } diff --git a/native/tests/AndroidTest-sminus.xml b/native/tests/AndroidTest-sminus.xml new file mode 100644 index 0000000..52e04f5 --- /dev/null +++ b/native/tests/AndroidTest-sminus.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2023 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. + --> +<configuration description="Config for libextservices_test-sminus"> + <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> + <option name="cleanup" value="true" /> + <option name="push" value="libextservices_test-sminus->/data/local/tmp/libextservices_test-sminus" /> + <option name="push" value="test_data->/data/local/tmp/test_data" /> + </target_preparer> + + <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.extservices.apex" /> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="mts" /> + + <test class="com.android.tradefed.testtype.GTest" > + <option name="native-test-device-path" value="/data/local/tmp" /> + <option name="module-name" value="libextservices_test-sminus" /> + </test> + + <!-- Prevent test from running on Android T+ --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.MaxSdkModuleController"> + <option name="max-sdk-level" value="32"/> + </object> + + <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <option name="mainline-module-package-name" value="com.google.android.extservices" /> + </object> +</configuration> diff --git a/native/tests/AndroidTest.xml b/native/tests/AndroidTest-tplus.xml index 5bc2ddf..179d511 100644 --- a/native/tests/AndroidTest.xml +++ b/native/tests/AndroidTest-tplus.xml @@ -14,22 +14,26 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<configuration description="Config for libextservices_test"> +<configuration description="Config for libextservices_test-tplus"> <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> <option name="cleanup" value="true" /> - <option name="push" value="libextservices_test->/data/local/tmp/libextservices_test" /> + <option name="push" value="libextservices_test-tplus->/data/local/tmp/libextservices_test-tplus" /> <option name="push" value="test_data->/data/local/tmp/test_data" /> </target_preparer> - <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.extservices.apex" /> + <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.extservices_tplus.apex" /> <option name="test-suite-tag" value="apct" /> <option name="test-suite-tag" value="mts" /> <test class="com.android.tradefed.testtype.GTest" > <option name="native-test-device-path" value="/data/local/tmp" /> - <option name="module-name" value="libextservices_test" /> + <option name="module-name" value="libextservices_test-tplus" /> </test> + <!-- Prevent tests from running on Android S- --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController"/> + <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> <option name="mainline-module-package-name" value="com.google.android.extservices" /> </object> |