• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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