1 package com.android.server.deviceconfig; 2 3 import static com.android.server.deviceconfig.Flags.fixFlagStagingNotificationResourceFetching; 4 5 import android.annotation.NonNull; 6 import android.app.AlarmManager; 7 import android.app.Notification; 8 import android.app.Notification.Action; 9 import android.app.NotificationChannel; 10 import android.app.NotificationManager; 11 import android.app.PendingIntent; 12 import android.content.IntentFilter; 13 import android.content.BroadcastReceiver; 14 import android.content.Intent; 15 import android.content.pm.PackageManager.NameNotFoundException; 16 import android.content.Context; 17 import android.graphics.drawable.Icon; 18 import android.os.PowerManager; 19 import android.provider.DeviceConfig.OnPropertiesChangedListener; 20 import android.provider.DeviceConfig.Properties; 21 import android.util.Slog; 22 import com.android.server.deviceconfig.resources.R; 23 import java.time.Instant; 24 import java.time.LocalDateTime; 25 import java.time.ZoneId; 26 import java.time.ZonedDateTime; 27 import java.io.IOException; 28 import java.util.Optional; 29 import java.util.Map; 30 import java.util.Set; 31 32 import static android.app.NotificationManager.IMPORTANCE_HIGH; 33 import static java.time.temporal.ChronoUnit.SECONDS; 34 35 /** 36 * Creates notifications when aconfig flags are staged on the device. 37 * 38 * The notification alerts the user to reboot, to apply the staged flags. 39 * 40 * @hide 41 */ 42 class BootNotificationCreator implements OnPropertiesChangedListener { 43 private static final String TAG = "DeviceConfigBootNotificationCreator"; 44 45 private static final String RESOURCES_PACKAGE = 46 "com.android.server.deviceconfig.resources"; 47 48 private static final String REBOOT_REASON = "DeviceConfig"; 49 50 private static final String ACTION_TRIGGER_HARD_REBOOT = 51 "com.android.server.deviceconfig.TRIGGER_HARD_REBOOT"; 52 private static final String ACTION_POST_NOTIFICATION = 53 "com.android.server.deviceconfig.POST_NOTIFICATION"; 54 55 private static final String CHANNEL_ID = "trunk-stable-flags"; 56 private static final String CHANNEL_NAME = "Trunkfood flags"; 57 private static final int NOTIFICATION_ID = 111555; 58 59 private NotificationManager notificationManager; 60 private PowerManager powerManager; 61 private AlarmManager alarmManager; 62 63 private Context context; 64 65 private static final int REBOOT_HOUR = 10; 66 private static final int REBOOT_MINUTE = 0; 67 private static final int MIN_SECONDS_TO_SHOW_NOTIF = 86400; 68 69 private LocalDateTime lastReboot; 70 71 private Map<String, Set<String>> aconfigFlags; 72 BootNotificationCreator(@onNull Context context, Map<String, Set<String>> aconfigFlags)73 public BootNotificationCreator(@NonNull Context context, 74 Map<String, Set<String>> aconfigFlags) { 75 this.context = context; 76 this.aconfigFlags = aconfigFlags; 77 78 this.context.registerReceiver( 79 new HardRebootBroadcastReceiver(), 80 new IntentFilter(ACTION_TRIGGER_HARD_REBOOT), 81 Context.RECEIVER_EXPORTED); 82 this.context.registerReceiver( 83 new PostNotificationBroadcastReceiver(), 84 new IntentFilter(ACTION_POST_NOTIFICATION), 85 Context.RECEIVER_EXPORTED); 86 87 this.lastReboot = LocalDateTime.now(ZoneId.systemDefault()); 88 } 89 90 @Override onPropertiesChanged(Properties properties)91 public void onPropertiesChanged(Properties properties) { 92 if (!containsAconfigChanges(properties)) { 93 return; 94 } 95 96 if (!tryInitializeDependenciesIfNeeded()) { 97 Slog.i(TAG, "not posting notif; service dependencies not ready"); 98 return; 99 } 100 101 PendingIntent pendingIntent = 102 PendingIntent.getBroadcast( 103 context, 104 /* requestCode= */ 1, 105 new Intent(ACTION_POST_NOTIFICATION), 106 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 107 108 ZonedDateTime now = Instant 109 .ofEpochMilli(System.currentTimeMillis()) 110 .atZone(ZoneId.systemDefault()); 111 112 LocalDateTime currentTime = now.toLocalDateTime(); 113 LocalDateTime postTime = now.toLocalDate().atTime(REBOOT_HOUR, REBOOT_MINUTE); 114 115 LocalDateTime scheduledPostTime = 116 currentTime.isBefore(postTime) ? postTime : postTime.plusDays(1); 117 long scheduledPostTimeLong = scheduledPostTime 118 .atZone(ZoneId.systemDefault()) 119 .toInstant() 120 .toEpochMilli(); 121 122 alarmManager.setExact( 123 AlarmManager.RTC_WAKEUP, scheduledPostTimeLong, pendingIntent); 124 } 125 containsAconfigChanges(Properties properties)126 private boolean containsAconfigChanges(Properties properties) { 127 for (String namespaceAndFlag : properties.getKeyset()) { 128 int firstStarIndex = namespaceAndFlag.indexOf("*"); 129 if (firstStarIndex == -1 || firstStarIndex == 0 130 || firstStarIndex == namespaceAndFlag.length() - 1) { 131 Slog.w(TAG, "detected malformed staged flag: " + namespaceAndFlag); 132 continue; 133 } 134 135 String namespace = namespaceAndFlag.substring(0, firstStarIndex); 136 String flag = namespaceAndFlag.substring(firstStarIndex + 1); 137 138 if (aconfigFlags.get(namespace) != null && aconfigFlags.get(namespace).contains(flag)) { 139 return true; 140 } 141 } 142 return false; 143 } 144 145 private class PostNotificationBroadcastReceiver extends BroadcastReceiver { 146 @Override onReceive(Context context, Intent intent)147 public void onReceive(Context context, Intent intent) { 148 LocalDateTime now = LocalDateTime.now(ZoneId.systemDefault()); 149 150 if (lastReboot.until(now, SECONDS) < MIN_SECONDS_TO_SHOW_NOTIF) { 151 Slog.w(TAG, "not enough time passed, punting"); 152 tryAgainIn24Hours(now); 153 return; 154 } 155 156 Optional<String> resourcesPackageName = 157 fixFlagStagingNotificationResourceFetching() 158 ? ServiceResourcesHelper.get(context).getResourcesPackageName() 159 : Optional.of(RESOURCES_PACKAGE); 160 if (resourcesPackageName.isEmpty()) { 161 Slog.w(TAG, "Unable to find resources package."); 162 return; 163 } 164 165 PendingIntent pendingIntent = 166 PendingIntent.getBroadcast( 167 context, 168 /* requestCode= */ 1, 169 new Intent(ACTION_TRIGGER_HARD_REBOOT), 170 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 171 172 try { 173 Context resourcesContext = 174 context.createPackageContext(resourcesPackageName.get(), 0); 175 Action action = new Action.Builder( 176 Icon.createWithResource(resourcesContext, R.drawable.ic_restart), 177 resourcesContext.getString(R.string.boot_notification_action_text), 178 pendingIntent).build(); 179 Notification notification = new Notification.Builder(context, CHANNEL_ID) 180 .setContentText(resourcesContext.getString(R.string.boot_notification_content)) 181 .setContentTitle(resourcesContext.getString(R.string.boot_notification_title)) 182 .setSmallIcon(Icon.createWithResource(resourcesContext, R.drawable.ic_flag)) 183 .addAction(action) 184 .build(); 185 notificationManager.notify(NOTIFICATION_ID, notification); 186 } catch (NameNotFoundException e) { 187 Slog.e(TAG, "failed to post boot notification", e); 188 } 189 } 190 tryAgainIn24Hours(LocalDateTime currentTime)191 private void tryAgainIn24Hours(LocalDateTime currentTime) { 192 PendingIntent pendingIntent = 193 PendingIntent.getBroadcast( 194 context, 195 /* requestCode= */ 1, 196 new Intent(ACTION_POST_NOTIFICATION), 197 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 198 199 LocalDateTime postTime = 200 currentTime.toLocalDate().atTime(REBOOT_HOUR, REBOOT_MINUTE).plusDays(1); 201 long scheduledPostTimeLong = postTime 202 .atZone(ZoneId.systemDefault()) 203 .toInstant() 204 .toEpochMilli(); 205 alarmManager.setExact( 206 AlarmManager.RTC_WAKEUP, scheduledPostTimeLong, pendingIntent); 207 } 208 } 209 210 private class HardRebootBroadcastReceiver extends BroadcastReceiver { 211 @Override onReceive(Context context, Intent intent)212 public void onReceive(Context context, Intent intent) { 213 powerManager.reboot(REBOOT_REASON); 214 } 215 } 216 217 /** 218 * If deps are not initialized yet, try to initialize them. 219 * 220 * @return true if the dependencies are newly or already initialized, 221 * or false if they are not ready yet 222 */ tryInitializeDependenciesIfNeeded()223 private boolean tryInitializeDependenciesIfNeeded() { 224 if (notificationManager == null) { 225 notificationManager = context.getSystemService(NotificationManager.class); 226 if (notificationManager != null) { 227 notificationManager.createNotificationChannel( 228 new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, IMPORTANCE_HIGH)); 229 } 230 } 231 232 if (alarmManager == null) { 233 alarmManager = context.getSystemService(AlarmManager.class); 234 } 235 236 if (powerManager == null) { 237 powerManager = context.getSystemService(PowerManager.class); 238 } 239 240 return notificationManager != null 241 && alarmManager != null 242 && powerManager != null; 243 } 244 } 245