summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTed Bauer <tedbauer@google.com>2023-10-09 17:54:44 -0400
committerTed Bauer <tedbauer@google.com>2023-10-13 16:57:49 +0000
commit61e3b2a4ecefc7278e0d7f6ed8ab9c6aa5477d31 (patch)
tree882d05618b77a21d07b225b1d739f78d49021db9
parentbf73c93a2f602c1bf3c4fc9ab4ae539452d8f42e (diff)
downloadConfigInfrastructure-61e3b2a4ecefc7278e0d7f6ed8ab9c6aa5477d31.tar.gz
Add boot notification to ConfigInfra.
Bug: 298391955 Test: atest ConfigInfrastructureServiceUnitTests Change-Id: I9c1166b0107e8ea188a40ced71228fdab3ac6b0f
-rw-r--r--apex/Android.bp3
-rw-r--r--service/Android.bp19
-rw-r--r--service/ServiceResources/Android.bp36
-rw-r--r--service/ServiceResources/AndroidManifest.xml27
-rw-r--r--service/ServiceResources/res/drawable/ic_flag.xml10
-rw-r--r--service/ServiceResources/res/values/strings.xml7
-rw-r--r--service/flags.aconfig8
-rw-r--r--service/java/com/android/server/deviceconfig/BootNotificationCreator.java164
-rw-r--r--service/java/com/android/server/deviceconfig/DeviceConfigInit.java15
-rw-r--r--service/javatests/Android.bp1
-rw-r--r--service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java80
11 files changed, 369 insertions, 1 deletions
diff --git a/apex/Android.bp b/apex/Android.bp
index 243af7c..abed18f 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -94,6 +94,9 @@ apex {
min_sdk_version: "34",
key: "com.android.configinfrastructure.key",
certificate: ":com.android.configinfrastructure.certificate",
+ apps: [
+ "DeviceConfigServiceResources",
+ ],
}
sdk {
diff --git a/service/Android.bp b/service/Android.bp
index 88bc4eb..8d0fb79 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -33,9 +33,11 @@ java_sdk_library {
static_libs: [
"modules-utils-build",
"modules-utils-shell-command-handler",
+ "device_config_reboot_flags_java_lib",
],
libs: [
"framework-configinfrastructure.impl",
+ "DeviceConfigServiceResources",
],
min_sdk_version: "UpsideDownCake",
sdk_version: "system_server_current",
@@ -44,3 +46,20 @@ java_sdk_library {
"//packages/modules/ConfigInfrastructure/service/javatests",
],
}
+
+aconfig_declarations {
+ name: "device_config_reboot_flags",
+ package: "com.android.server.deviceconfig",
+ srcs: [
+ "flags.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "device_config_reboot_flags_java_lib",
+ min_sdk_version: "UpsideDownCake",
+ apex_available: [
+ "com.android.configinfrastructure",
+ ],
+ aconfig_declarations: "device_config_reboot_flags",
+}
diff --git a/service/ServiceResources/Android.bp b/service/ServiceResources/Android.bp
new file mode 100644
index 0000000..cd2dcc8
--- /dev/null
+++ b/service/ServiceResources/Android.bp
@@ -0,0 +1,36 @@
+//
+// Copyright (C) 2020 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.
+//
+
+// APK to hold all the wifi overlayable resources.
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+ name: "DeviceConfigServiceResources",
+ package_name: "com.android.server.deviceconfig.resources",
+ sdk_version: "system_current",
+ resource_dirs: [
+ "res",
+ ],
+ certificate: "platform",
+ min_sdk_version: "34",
+ // platform_apis: true,
+ export_package_resources: true,
+ apex_available: [
+ "com.android.configinfrastructure",
+ ],
+}
diff --git a/service/ServiceResources/AndroidManifest.xml b/service/ServiceResources/AndroidManifest.xml
new file mode 100644
index 0000000..4fc5e06
--- /dev/null
+++ b/service/ServiceResources/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2019 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 for resources APK -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.deviceconfig.resources"
+ coreApp="true"
+ android:versionCode="1"
+ android:versionName="R-initial">
+ <application>
+ </application>
+</manifest>
diff --git a/service/ServiceResources/res/drawable/ic_flag.xml b/service/ServiceResources/res/drawable/ic_flag.xml
new file mode 100644
index 0000000..db86d5d
--- /dev/null
+++ b/service/ServiceResources/res/drawable/ic_flag.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M5,21V4H14L14.4,6H20V16H13L12.6,14H7V21ZM12.5,10ZM14.65,14H18V8H12.75L12.35,6H7V12H14.25Z"/>
+</vector>
diff --git a/service/ServiceResources/res/values/strings.xml b/service/ServiceResources/res/values/strings.xml
new file mode 100644
index 0000000..53407e0
--- /dev/null
+++ b/service/ServiceResources/res/values/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Title text for a notification that instructs the user to reboot to apply new server flags to the device [CHAR LIMIT=NONE] -->
+ <string name="boot_notification_title">New flags available</string>
+ <!-- Content text for a notification that instructs the user to reboot to apply new server flags to the device [CHAR LIMIT=NONE] -->
+ <string name="boot_notification_content">Tap to reboot and apply new trunkfood flag values</string>
+</resources>
diff --git a/service/flags.aconfig b/service/flags.aconfig
new file mode 100644
index 0000000..6240af1
--- /dev/null
+++ b/service/flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.deviceconfig"
+
+flag {
+ name: "enable_reboot_notification"
+ namespace: "core_experiments_team_internal"
+ description: "If enabled, a notification appears when flags are staged to be applied on reboot."
+ bug: "296462695"
+}
diff --git a/service/java/com/android/server/deviceconfig/BootNotificationCreator.java b/service/java/com/android/server/deviceconfig/BootNotificationCreator.java
new file mode 100644
index 0000000..388fedc
--- /dev/null
+++ b/service/java/com/android/server/deviceconfig/BootNotificationCreator.java
@@ -0,0 +1,164 @@
+package com.android.server.deviceconfig;
+
+import android.annotation.NonNull;
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.IntentFilter;
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.PowerManager;
+import android.provider.DeviceConfig.OnPropertiesChangedListener;
+import android.provider.DeviceConfig.Properties;
+import android.util.Slog;
+import com.android.server.deviceconfig.resources.R;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import static android.app.NotificationManager.IMPORTANCE_HIGH;
+
+/**
+ * Creates notifications when flags are staged on the device.
+ *
+ * The notification alerts the user to reboot, to apply the staged flags.
+ *
+ * @hide
+ */
+class BootNotificationCreator implements OnPropertiesChangedListener {
+ private static final String TAG = "DeviceConfigBootNotificationCreator";
+
+ private static final String RESOURCES_PACKAGE =
+ "com.android.server.deviceconfig.resources";
+
+ private static final String REBOOT_REASON = "DeviceConfig";
+
+ private static final String ACTION_TRIGGER_HARD_REBOOT =
+ "com.android.server.deviceconfig.TRIGGER_HARD_REBOOT";
+ private static final String ACTION_POST_NOTIFICATION =
+ "com.android.server.deviceconfig.POST_NOTIFICATION";
+
+ private static final String CHANNEL_ID = "trunk-stable-flags";
+ private static final String CHANNEL_NAME = "Trunkfood flags";
+ private static final int NOTIFICATION_ID = 111555;
+
+ private NotificationManager notificationManager;
+ private PowerManager powerManager;
+ private AlarmManager alarmManager;
+
+ private Context context;
+
+ private static final int REBOOT_HOUR = 18;
+ private static final int REBOOT_MINUTE = 2;
+
+ public BootNotificationCreator(@NonNull Context context) {
+ this.context = context;
+
+ this.context.registerReceiver(
+ new HardRebootBroadcastReceiver(),
+ new IntentFilter(ACTION_TRIGGER_HARD_REBOOT),
+ Context.RECEIVER_EXPORTED);
+ this.context.registerReceiver(
+ new PostNotificationBroadcastReceiver(),
+ new IntentFilter(ACTION_POST_NOTIFICATION),
+ Context.RECEIVER_EXPORTED);
+ }
+
+ @Override
+ public void onPropertiesChanged(Properties properties) {
+ if (!tryInitializeDependenciesIfNeeded()) {
+ Slog.i(TAG, "not posting notif; service dependencies not ready");
+ return;
+ }
+
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ /* requestCode= */ 1,
+ new Intent(ACTION_POST_NOTIFICATION),
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ ZonedDateTime now = Instant
+ .ofEpochMilli(System.currentTimeMillis())
+ .atZone(ZoneId.systemDefault());
+
+ LocalDateTime currentTime = now.toLocalDateTime();
+ LocalDateTime postTime = now.toLocalDate().atTime(REBOOT_HOUR, REBOOT_MINUTE);
+
+ LocalDateTime scheduledPostTime =
+ currentTime.isBefore(postTime) ? postTime : postTime.plusDays(1);
+ long scheduledPostTimeLong = scheduledPostTime
+ .atZone(ZoneId.systemDefault())
+ .toInstant()
+ .toEpochMilli();
+
+ alarmManager.setExact(
+ AlarmManager.RTC_WAKEUP, scheduledPostTimeLong, pendingIntent);
+ }
+
+ private class PostNotificationBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(
+ context,
+ /* requestCode= */ 1,
+ new Intent(ACTION_TRIGGER_HARD_REBOOT),
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+ try {
+ Context resourcesContext = context.createPackageContext(RESOURCES_PACKAGE, 0);
+ Notification notification = new Notification.Builder(context, CHANNEL_ID)
+ .setContentText(resourcesContext.getString(R.string.boot_notification_content))
+ .setContentTitle(resourcesContext.getString(R.string.boot_notification_title))
+ .setSmallIcon(Icon.createWithResource(resourcesContext, R.drawable.ic_flag))
+ .setContentIntent(pendingIntent)
+ .build();
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ } catch (NameNotFoundException e) {
+ Slog.e(TAG, "failed to post boot notification", e);
+ }
+ }
+ }
+
+ private class HardRebootBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ powerManager.reboot(REBOOT_REASON);
+ }
+ }
+
+ /**
+ * If deps are not initialized yet, try to initialize them.
+ *
+ * @return true if the dependencies are newly or already initialized,
+ * or false if they are not ready yet
+ */
+ private boolean tryInitializeDependenciesIfNeeded() {
+ if (notificationManager == null) {
+ notificationManager = context.getSystemService(NotificationManager.class);
+ if (notificationManager != null) {
+ notificationManager.createNotificationChannel(
+ new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, IMPORTANCE_HIGH));
+ }
+ }
+
+ if (alarmManager == null) {
+ alarmManager = context.getSystemService(AlarmManager.class);
+ }
+
+ if (powerManager == null) {
+ powerManager = context.getSystemService(PowerManager.class);
+ }
+
+ return notificationManager != null
+ && alarmManager != null
+ && powerManager != null;
+ }
+}
diff --git a/service/java/com/android/server/deviceconfig/DeviceConfigInit.java b/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
index f9c7792..42cee70 100644
--- a/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
+++ b/service/java/com/android/server/deviceconfig/DeviceConfigInit.java
@@ -3,13 +3,18 @@ package com.android.server.deviceconfig;
import java.io.FileDescriptor;
import java.io.IOException;
+import android.content.Intent;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.content.Context;
+import android.os.AsyncTask;
import android.os.Binder;
import android.provider.DeviceConfig;
import android.provider.DeviceConfigManager;
import android.provider.UpdatableDeviceConfigServiceReadiness;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.content.ComponentName;
import android.provider.aidl.IDeviceConfigManager;
import android.util.Slog;
@@ -18,10 +23,13 @@ import com.android.modules.utils.build.SdkLevel;
import com.android.server.SystemService;
+import static com.android.server.deviceconfig.Flags.enableRebootNotification;
+
/** @hide */
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
public class DeviceConfigInit {
private static final String TAG = "DEVICE_CONFIG_INIT";
+ private static final String STAGED_NAMESPACE = "staged";
private DeviceConfigInit() {
// do not instantiate
@@ -50,7 +58,12 @@ public class DeviceConfigInit {
*/
@Override
public void onStart() {
- // no op
+ if (enableRebootNotification()) {
+ DeviceConfig.addOnPropertiesChangedListener(
+ STAGED_NAMESPACE,
+ AsyncTask.THREAD_POOL_EXECUTOR,
+ new BootNotificationCreator(getContext().getApplicationContext()));
+ }
}
private void applyBootstrapValues() {
diff --git a/service/javatests/Android.bp b/service/javatests/Android.bp
index 3ec4631..726c86e 100644
--- a/service/javatests/Android.bp
+++ b/service/javatests/Android.bp
@@ -40,6 +40,7 @@ android_test {
"modules-utils-build",
"service-configinfrastructure.impl",
"truth-prebuilt",
+ "mockito-target-minus-junit4",
],
libs: [
"android.test.base",
diff --git a/service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java b/service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java
new file mode 100644
index 0000000..394864d
--- /dev/null
+++ b/service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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 com.android.server.deviceconfig;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import android.content.Context;
+import java.util.HashMap;
+
+import android.content.ContextWrapper;
+import org.mockito.Mockito;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfig.Properties;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+import android.app.AlarmManager;
+
+import java.io.IOException;
+
+import org.junit.Test;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class BootNotificationCreatorTest {
+ Context mockContext;
+ AlarmManager mockAlarmManager;
+
+ BootNotificationCreator bootNotificationCreator;
+
+ @Before
+ public void setUp() {
+ mockAlarmManager = mock(AlarmManager.class);
+ mockContext = new ContextWrapper(getInstrumentation().getTargetContext()) {
+ @Override
+ public Object getSystemService(String name) {
+ if (name.equals(Context.ALARM_SERVICE)) {
+ return mockAlarmManager;
+ } else {
+ return super.getSystemService(name);
+ }
+ }
+ };
+ bootNotificationCreator = new BootNotificationCreator(mockContext);
+ }
+
+ @Test
+ public void testNotificationScheduledWhenFlagStaged() {
+ HashMap<String, String> flags = new HashMap();
+ flags.put("test", "flag");
+ Properties properties = new Properties("staged", flags);
+
+ bootNotificationCreator.onPropertiesChanged(properties);
+
+ Mockito.verify(mockAlarmManager).setExact(anyInt(), anyLong(), any());
+ }
+}