package com.android.server.deviceconfig; import static com.android.server.deviceconfig.Flags.fixFlagStagingNotificationResourceFetching; 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.Optional; 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> aconfigFlags; public BootNotificationCreator(@NonNull Context context, Map> 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; } Optional resourcesPackageName = fixFlagStagingNotificationResourceFetching() ? ServiceResourcesHelper.get(context).getResourcesPackageName() : Optional.of(RESOURCES_PACKAGE); if (resourcesPackageName.isEmpty()) { Slog.w(TAG, "Unable to find resources package."); 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(resourcesPackageName.get(), 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; } }