diff options
Diffstat (limited to 'service/java/com/android/server/deviceconfig/BootNotificationCreator.java')
-rw-r--r-- | service/java/com/android/server/deviceconfig/BootNotificationCreator.java | 231 |
1 files changed, 231 insertions, 0 deletions
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..d29f317 --- /dev/null +++ b/service/java/com/android/server/deviceconfig/BootNotificationCreator.java @@ -0,0 +1,231 @@ +package com.android.server.deviceconfig; + +import android.annotation.NonNull; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.Notification.Action; +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 java.io.IOException; +import java.util.Map; +import java.util.Set; + +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static java.time.temporal.ChronoUnit.SECONDS; + +/** + * Creates notifications when aconfig 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 = 10; + private static final int REBOOT_MINUTE = 0; + private static final int MIN_SECONDS_TO_SHOW_NOTIF = 86400; + + private LocalDateTime lastReboot; + + private Map<String, Set<String>> aconfigFlags; + + public BootNotificationCreator(@NonNull Context context, + Map<String, Set<String>> aconfigFlags) { + this.context = context; + this.aconfigFlags = aconfigFlags; + + 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); + + this.lastReboot = LocalDateTime.now(ZoneId.systemDefault()); + } + + @Override + public void onPropertiesChanged(Properties properties) { + if (!containsAconfigChanges(properties)) { + return; + } + + 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 boolean containsAconfigChanges(Properties properties) { + for (String namespaceAndFlag : properties.getKeyset()) { + int firstStarIndex = namespaceAndFlag.indexOf("*"); + if (firstStarIndex == -1 || firstStarIndex == 0 + || firstStarIndex == namespaceAndFlag.length() - 1) { + Slog.w(TAG, "detected malformed staged flag: " + namespaceAndFlag); + continue; + } + + String namespace = namespaceAndFlag.substring(0, firstStarIndex); + String flag = namespaceAndFlag.substring(firstStarIndex + 1); + + if (aconfigFlags.get(namespace) != null && aconfigFlags.get(namespace).contains(flag)) { + return true; + } + } + return false; + } + + private class PostNotificationBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault()); + + if (lastReboot.until(now, SECONDS) < MIN_SECONDS_TO_SHOW_NOTIF) { + Slog.w(TAG, "not enough time passed, punting"); + tryAgainIn24Hours(now); + return; + } + + 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); + Action action = new Action.Builder( + Icon.createWithResource(resourcesContext, R.drawable.ic_restart), + resourcesContext.getString(R.string.boot_notification_action_text), + pendingIntent).build(); + 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)) + .addAction(action) + .build(); + notificationManager.notify(NOTIFICATION_ID, notification); + } catch (NameNotFoundException e) { + Slog.e(TAG, "failed to post boot notification", e); + } + } + + private void tryAgainIn24Hours(LocalDateTime currentTime) { + PendingIntent pendingIntent = + PendingIntent.getBroadcast( + context, + /* requestCode= */ 1, + new Intent(ACTION_POST_NOTIFICATION), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + LocalDateTime postTime = + currentTime.toLocalDate().atTime(REBOOT_HOUR, REBOOT_MINUTE).plusDays(1); + long scheduledPostTimeLong = postTime + .atZone(ZoneId.systemDefault()) + .toInstant() + .toEpochMilli(); + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, scheduledPostTimeLong, pendingIntent); + } + } + + 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; + } +} |