diff options
author | Ted Bauer <tedbauer@google.com> | 2023-10-09 17:54:44 -0400 |
---|---|---|
committer | Ted Bauer <tedbauer@google.com> | 2023-10-13 16:57:49 +0000 |
commit | 61e3b2a4ecefc7278e0d7f6ed8ab9c6aa5477d31 (patch) | |
tree | 882d05618b77a21d07b225b1d739f78d49021db9 | |
parent | bf73c93a2f602c1bf3c4fc9ab4ae539452d8f42e (diff) | |
download | ConfigInfrastructure-61e3b2a4ecefc7278e0d7f6ed8ab9c6aa5477d31.tar.gz |
Add boot notification to ConfigInfra.
Bug: 298391955
Test: atest ConfigInfrastructureServiceUnitTests
Change-Id: I9c1166b0107e8ea188a40ced71228fdab3ac6b0f
-rw-r--r-- | apex/Android.bp | 3 | ||||
-rw-r--r-- | service/Android.bp | 19 | ||||
-rw-r--r-- | service/ServiceResources/Android.bp | 36 | ||||
-rw-r--r-- | service/ServiceResources/AndroidManifest.xml | 27 | ||||
-rw-r--r-- | service/ServiceResources/res/drawable/ic_flag.xml | 10 | ||||
-rw-r--r-- | service/ServiceResources/res/values/strings.xml | 7 | ||||
-rw-r--r-- | service/flags.aconfig | 8 | ||||
-rw-r--r-- | service/java/com/android/server/deviceconfig/BootNotificationCreator.java | 164 | ||||
-rw-r--r-- | service/java/com/android/server/deviceconfig/DeviceConfigInit.java | 15 | ||||
-rw-r--r-- | service/javatests/Android.bp | 1 | ||||
-rw-r--r-- | service/javatests/src/com/android/server/deviceconfig/BootNotificationCreatorTest.java | 80 |
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()); + } +} |