aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2024-03-06 09:30:08 -0800
committerXin Li <delphij@google.com>2024-03-06 09:30:08 -0800
commit209031ab4c42cbf931729533666a571295d6747e (patch)
treeff0874151c073d757c20d0196a2bb3599efa7f0d
parentb1e3041c540e6397ac86ae6a5eeb8958c423fc1d (diff)
parent14beeea07284a8f611fb73e81e37e03f86e01724 (diff)
downloadExtServices-main.tar.gz
Merge Android 14 QPR2 to AOSP mainHEADmastermain
Bug: 319669529 Merged-In: Ifecdd1cd8d47fc2d71aecd13b8155cb976a96ba3 Change-Id: I0836ddc9b10b1742c01128edf305c5bb05169742
-rw-r--r--Android.bp48
-rw-r--r--AndroidManifest.xml17
-rw-r--r--EmptyManifest.xml2
-rw-r--r--OWNERS5
-rw-r--r--TEST_MAPPING26
-rw-r--r--apex/Android.bp25
-rw-r--r--apex/permissions/Android.bp14
-rw-r--r--apex/permissions/android.ext_sminus.services.xml (renamed from apex/permissions/android.ext.services.xml)0
-rw-r--r--apex/permissions/android.ext_tplus.services.xml24
-rw-r--r--apex/tplus_apex_manifest.json7
-rw-r--r--java/res/values-v33/bools.xml (renamed from java/res/values-v31/bools.xml)5
-rw-r--r--java/res/values/bools.xml8
-rw-r--r--java/src/android/ext/services/common/AdServicesAppsearchDeleteJob.java455
-rw-r--r--java/src/android/ext/services/common/AdServicesFilesCleanupBootCompleteReceiver.java212
-rw-r--r--java/src/android/ext/services/common/AdservicesPhFlags.java153
-rw-r--r--java/src/android/ext/services/common/BootCompletedReceiver.java160
-rw-r--r--java/tests/Android.bp41
-rw-r--r--java/tests/AndroidTest-sminus.xml40
-rw-r--r--java/tests/AndroidTest-tplus.xml (renamed from java/tests/AndroidTest.xml)8
-rw-r--r--java/tests/hosttests/Android.bp59
-rw-r--r--java/tests/hosttests/AndroidTest-sminus.xml47
-rw-r--r--java/tests/hosttests/AndroidTest-tplus.xml41
-rw-r--r--java/tests/hosttests/src/android/ext/services/hosttests/AdServicesFilesCleanupBootCompleteReceiverHostTest.java255
-rw-r--r--java/tests/src/android/ext/services/common/AdServicesAppsearchDeleteJobTest.java955
-rw-r--r--java/tests/src/android/ext/services/common/AdServicesFilesCleanupBootCompleteReceiverTest.java351
-rw-r--r--java/tests/src/android/ext/services/common/BootCompletedReceiverTest.java257
-rw-r--r--native/tests/Android.bp42
-rw-r--r--native/tests/AndroidTest-sminus.xml42
-rw-r--r--native/tests/AndroidTest-tplus.xml (renamed from native/tests/AndroidTest.xml)12
29 files changed, 2848 insertions, 463 deletions
diff --git a/Android.bp b/Android.bp
index 11bd96d..a5bf21a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -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.
diff --git a/OWNERS b/OWNERS
index c62882a..2ce48f7 100644
--- a/OWNERS
+++ b/OWNERS
@@ -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>