Snap for 11219529 from a8b7fc7f57ea68153a9fc8b5e0c9cfa6c8e38820 to mainline-tzdata4-release
Change-Id: I5c12599d741306a562d6008482a8bb24ad955faa
diff --git a/Android.bp b/Android.bp
index 668be85..2f2f9d9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -34,14 +34,17 @@
srcs: [
"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: "AndroidManifest.xml",
-
static_libs: [
"androidx.annotation_annotation",
"androidx.autofill_autofill",
@@ -63,6 +66,7 @@
name: "ExtServices",
sdk_version: "module_current",
min_sdk_version: "30",
+ manifest: "AndroidManifest.xml",
optimize: {
optimize: true,
proguard_compatibility: false,
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 5426117..354ea51 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -36,7 +36,11 @@
<!-- 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" />
+ <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"
@@ -157,6 +161,13 @@
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
+ <!-- Boot completed receiver sends privileged startup broadcast. -->
+ <receiver android:name=".common.BootCompletedReceiver"
+ android:enabled="@bool/enableBootCompletedReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED"/>
+ </intent-filter>
+ </receiver>
</application>
-
</manifest>
diff --git a/EmptyManifest.xml b/EmptyManifest.xml
new file mode 100644
index 0000000..4c6b790
--- /dev/null
+++ b/EmptyManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.ext.services">
+</manifest>
diff --git a/java/res/values-v31/bools.xml b/java/res/values-v31/bools.xml
new file mode 100644
index 0000000..f39c88f
--- /dev/null
+++ b/java/res/values-v31/bools.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <bool name="enableBootCompletedReceiver">true</bool>
+</resources>
diff --git a/java/res/values/bools.xml b/java/res/values/bools.xml
new file mode 100644
index 0000000..d21ed12
--- /dev/null
+++ b/java/res/values/bools.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <bool name="enableBootCompletedReceiver">false</bool>
+</resources>
diff --git a/java/src/android/ext/services/common/BootCompletedReceiver.java b/java/src/android/ext/services/common/BootCompletedReceiver.java
new file mode 100644
index 0000000..af28dd7
--- /dev/null
+++ b/java/src/android/ext/services/common/BootCompletedReceiver.java
@@ -0,0 +1,160 @@
+/*
+ * 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/src/android/ext/services/common/BootCompletedReceiverTest.java b/java/tests/src/android/ext/services/common/BootCompletedReceiverTest.java
new file mode 100644
index 0000000..8a58a9f
--- /dev/null
+++ b/java/tests/src/android/ext/services/common/BootCompletedReceiverTest.java
@@ -0,0 +1,257 @@
+/*
+ * 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");
+ }
+}