1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.devicestate; 18 19 import static android.provider.Settings.ACTION_BATTERY_SAVER_SETTINGS; 20 21 import android.annotation.DrawableRes; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.app.Notification; 25 import android.app.NotificationChannel; 26 import android.app.NotificationManager; 27 import android.app.PendingIntent; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.pm.ApplicationInfo; 33 import android.content.pm.PackageManager; 34 import android.hardware.devicestate.DeviceStateManager; 35 import android.os.Handler; 36 import android.util.Slog; 37 import android.util.SparseArray; 38 39 import com.android.internal.R; 40 import com.android.internal.annotations.GuardedBy; 41 import com.android.internal.annotations.VisibleForTesting; 42 43 import java.util.Locale; 44 45 /** 46 * Manages the user-visible device state notifications. 47 */ 48 class DeviceStateNotificationController extends BroadcastReceiver { 49 private static final String TAG = "DeviceStateNotificationController"; 50 51 @VisibleForTesting static final String INTENT_ACTION_CANCEL_STATE = 52 "com.android.server.devicestate.INTENT_ACTION_CANCEL_STATE"; 53 @VisibleForTesting static final int NOTIFICATION_ID = 1; 54 @VisibleForTesting static final String CHANNEL_ID = "DeviceStateManager"; 55 @VisibleForTesting static final String NOTIFICATION_TAG = "DeviceStateManager"; 56 57 private final Context mContext; 58 private final Handler mHandler; 59 private final NotificationManager mNotificationManager; 60 private final PackageManager mPackageManager; 61 62 // The callback when a device state is requested to be canceled. 63 private final Runnable mCancelStateRunnable; 64 65 private final NotificationInfoProvider mNotificationInfoProvider; 66 DeviceStateNotificationController(@onNull Context context, @NonNull Handler handler, @NonNull Runnable cancelStateRunnable)67 DeviceStateNotificationController(@NonNull Context context, @NonNull Handler handler, 68 @NonNull Runnable cancelStateRunnable) { 69 this(context, handler, cancelStateRunnable, new NotificationInfoProvider(context), 70 context.getPackageManager(), context.getSystemService(NotificationManager.class)); 71 } 72 73 @VisibleForTesting DeviceStateNotificationController( @onNull Context context, @NonNull Handler handler, @NonNull Runnable cancelStateRunnable, @NonNull NotificationInfoProvider notificationInfoProvider, @NonNull PackageManager packageManager, @NonNull NotificationManager notificationManager)74 DeviceStateNotificationController( 75 @NonNull Context context, @NonNull Handler handler, 76 @NonNull Runnable cancelStateRunnable, 77 @NonNull NotificationInfoProvider notificationInfoProvider, 78 @NonNull PackageManager packageManager, 79 @NonNull NotificationManager notificationManager) { 80 mContext = context; 81 mHandler = handler; 82 mCancelStateRunnable = cancelStateRunnable; 83 mNotificationInfoProvider = notificationInfoProvider; 84 mPackageManager = packageManager; 85 mNotificationManager = notificationManager; 86 mContext.registerReceiver( 87 this, 88 new IntentFilter(INTENT_ACTION_CANCEL_STATE), 89 android.Manifest.permission.CONTROL_DEVICE_STATE, 90 mHandler, 91 Context.RECEIVER_NOT_EXPORTED); 92 } 93 94 /** 95 * Displays the ongoing notification indicating that the device state is active. Does nothing if 96 * the state does not have an active notification. 97 * 98 * @param state the active device state identifier. 99 * @param requestingAppUid the uid of the requesting app used to retrieve the app name. 100 */ showStateActiveNotificationIfNeeded(int state, int requestingAppUid)101 void showStateActiveNotificationIfNeeded(int state, int requestingAppUid) { 102 NotificationInfo info = getNotificationInfos().get(state); 103 if (info == null || !info.hasActiveNotification()) { 104 return; 105 } 106 String requesterApplicationLabel = getApplicationLabel(requestingAppUid); 107 if (requesterApplicationLabel != null) { 108 final Intent intent = new Intent(INTENT_ACTION_CANCEL_STATE) 109 .setPackage(mContext.getPackageName()); 110 final PendingIntent pendingIntent = PendingIntent.getBroadcast( 111 mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE); 112 113 showNotification( 114 info.name, info.activeNotificationTitle, 115 String.format(info.activeNotificationContent, requesterApplicationLabel), 116 true /* ongoing */, R.drawable.ic_dual_screen, 117 pendingIntent, 118 mContext.getString(R.string.device_state_notification_turn_off_button) 119 ); 120 } else { 121 Slog.e(TAG, "Cannot determine the requesting app name when showing state active " 122 + "notification. uid=" + requestingAppUid + ", state=" + state); 123 } 124 } 125 126 /** 127 * Displays the notification indicating that the device state is canceled due to thermal 128 * critical condition. Does nothing if the state does not have a thermal critical notification. 129 * 130 * @param state the identifier of the device state being canceled. 131 */ showThermalCriticalNotificationIfNeeded(int state)132 void showThermalCriticalNotificationIfNeeded(int state) { 133 NotificationInfo info = getNotificationInfos().get(state); 134 if (info == null || !info.hasThermalCriticalNotification()) { 135 return; 136 } 137 showNotification( 138 info.name, info.thermalCriticalNotificationTitle, 139 info.thermalCriticalNotificationContent, false /* ongoing */, 140 R.drawable.ic_thermostat, 141 null /* pendingIntent */, 142 null /* actionText */ 143 ); 144 } 145 146 /** 147 * Displays the notification indicating that the device state is canceled due to power 148 * save mode being enabled. Does nothing if the state does not have a power save mode 149 * notification. 150 * 151 * @param state the identifier of the device state being canceled. 152 */ showPowerSaveNotificationIfNeeded(int state)153 void showPowerSaveNotificationIfNeeded(int state) { 154 NotificationInfo info = getNotificationInfos().get(state); 155 if (info == null || !info.hasPowerSaveModeNotification()) { 156 return; 157 } 158 final Intent intent = new Intent(ACTION_BATTERY_SAVER_SETTINGS); 159 final PendingIntent pendingIntent = PendingIntent.getActivity( 160 mContext, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE); 161 showNotification( 162 info.name, info.powerSaveModeNotificationTitle, 163 info.powerSaveModeNotificationContent, false /* ongoing */, 164 R.drawable.ic_thermostat, 165 pendingIntent, 166 mContext.getString(R.string.device_state_notification_settings_button) 167 ); 168 } 169 170 /** 171 * Cancels the notification of the corresponding device state. 172 * 173 * @param state the device state identifier. 174 */ cancelNotification(int state)175 void cancelNotification(int state) { 176 if (getNotificationInfos().get(state) == null) { 177 return; 178 } 179 mHandler.post(() -> mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID)); 180 } 181 182 @Override onReceive(@onNull Context context, @Nullable Intent intent)183 public void onReceive(@NonNull Context context, @Nullable Intent intent) { 184 if (intent != null) { 185 if (INTENT_ACTION_CANCEL_STATE.equals(intent.getAction())) { 186 mCancelStateRunnable.run(); 187 } 188 } 189 } 190 191 /** 192 * Displays a notification with the specified name, title, and content. 193 * 194 * @param name the name of the notification. 195 * @param title the title of the notification. 196 * @param content the content of the notification. 197 * @param ongoing if true, display an ongoing (sticky) notification with a turn off button. 198 */ showNotification( @onNull String name, @NonNull String title, @NonNull String content, boolean ongoing, @DrawableRes int iconRes, @Nullable PendingIntent pendingIntent, @Nullable String actionText)199 private void showNotification( 200 @NonNull String name, @NonNull String title, @NonNull String content, boolean ongoing, 201 @DrawableRes int iconRes, 202 @Nullable PendingIntent pendingIntent, @Nullable String actionText) { 203 final NotificationChannel channel = new NotificationChannel( 204 CHANNEL_ID, name, NotificationManager.IMPORTANCE_HIGH); 205 final Notification.Builder builder = new Notification.Builder(mContext, CHANNEL_ID) 206 .setSmallIcon(iconRes) 207 .setContentTitle(title) 208 .setContentText(content) 209 .setSubText(name) 210 .setLocalOnly(true) 211 .setOngoing(ongoing) 212 .setCategory(Notification.CATEGORY_SYSTEM); 213 214 if (pendingIntent != null && actionText != null) { 215 final Notification.Action action = new Notification.Action.Builder( 216 null /* icon */, 217 actionText, 218 pendingIntent) 219 .build(); 220 builder.addAction(action); 221 } 222 223 mHandler.post(() -> { 224 mNotificationManager.createNotificationChannel(channel); 225 mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build()); 226 }); 227 } 228 getNotificationInfos()229 private SparseArray<NotificationInfo> getNotificationInfos() { 230 Locale locale = mContext.getResources().getConfiguration().getLocales().get(0); 231 return mNotificationInfoProvider.getNotificationInfos(locale); 232 } 233 234 @VisibleForTesting 235 public static class NotificationInfoProvider { 236 @NonNull 237 private final Context mContext; 238 private final Object mLock = new Object(); 239 240 @GuardedBy("mLock") 241 @Nullable 242 private SparseArray<NotificationInfo> mCachedNotificationInfos; 243 244 @GuardedBy("mLock") 245 @Nullable 246 @VisibleForTesting 247 Locale mCachedLocale; 248 NotificationInfoProvider(@onNull Context context)249 NotificationInfoProvider(@NonNull Context context) { 250 mContext = context; 251 } 252 253 /** 254 * Loads the resources for the notifications. The device state identifiers and strings are 255 * stored in arrays. All the string arrays must have the same length and same order as the 256 * identifier array. 257 */ 258 @NonNull getNotificationInfos(@onNull Locale locale)259 public SparseArray<NotificationInfo> getNotificationInfos(@NonNull Locale locale) { 260 synchronized (mLock) { 261 if (!locale.equals(mCachedLocale)) { 262 refreshNotificationInfos(locale); 263 } 264 return mCachedNotificationInfos; 265 } 266 } 267 268 269 @VisibleForTesting getCachedLocale()270 Locale getCachedLocale() { 271 synchronized (mLock) { 272 return mCachedLocale; 273 } 274 } 275 276 @VisibleForTesting refreshNotificationInfos(Locale locale)277 public void refreshNotificationInfos(Locale locale) { 278 synchronized (mLock) { 279 mCachedLocale = locale; 280 mCachedNotificationInfos = loadNotificationInfos(); 281 } 282 } 283 284 @VisibleForTesting loadNotificationInfos()285 public SparseArray<NotificationInfo> loadNotificationInfos() { 286 final SparseArray<NotificationInfo> notificationInfos = new SparseArray<>(); 287 288 final int[] stateIdentifiers = 289 mContext.getResources().getIntArray( 290 R.array.device_state_notification_state_identifiers); 291 final String[] names = 292 mContext.getResources().getStringArray(R.array.device_state_notification_names); 293 final String[] activeNotificationTitles = 294 mContext.getResources().getStringArray( 295 R.array.device_state_notification_active_titles); 296 final String[] activeNotificationContents = 297 mContext.getResources().getStringArray( 298 R.array.device_state_notification_active_contents); 299 final String[] thermalCriticalNotificationTitles = 300 mContext.getResources().getStringArray( 301 R.array.device_state_notification_thermal_titles); 302 final String[] thermalCriticalNotificationContents = 303 mContext.getResources().getStringArray( 304 R.array.device_state_notification_thermal_contents); 305 final String[] powerSaveModeNotificationTitles = 306 mContext.getResources().getStringArray( 307 R.array.device_state_notification_power_save_titles); 308 final String[] powerSaveModeNotificationContents = 309 mContext.getResources().getStringArray( 310 R.array.device_state_notification_power_save_contents); 311 312 if (stateIdentifiers.length != names.length 313 || stateIdentifiers.length != activeNotificationTitles.length 314 || stateIdentifiers.length != activeNotificationContents.length 315 || stateIdentifiers.length != thermalCriticalNotificationTitles.length 316 || stateIdentifiers.length != thermalCriticalNotificationContents.length 317 || stateIdentifiers.length != powerSaveModeNotificationTitles.length 318 || stateIdentifiers.length != powerSaveModeNotificationContents.length 319 ) { 320 throw new IllegalStateException( 321 "The length of state identifiers and notification texts must match!"); 322 } 323 324 for (int i = 0; i < stateIdentifiers.length; i++) { 325 int identifier = stateIdentifiers[i]; 326 if (identifier == DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER) { 327 continue; 328 } 329 330 notificationInfos.put( 331 identifier, 332 new NotificationInfo( 333 names[i], 334 activeNotificationTitles[i], 335 activeNotificationContents[i], 336 thermalCriticalNotificationTitles[i], 337 thermalCriticalNotificationContents[i], 338 powerSaveModeNotificationTitles[i], 339 powerSaveModeNotificationContents[i]) 340 ); 341 } 342 return notificationInfos; 343 } 344 } 345 346 /** 347 * A helper function to get app name (label) using the app uid. 348 * 349 * @param uid the uid of the app. 350 * @return app name (label) if found, or null otherwise. 351 */ 352 @Nullable getApplicationLabel(int uid)353 private String getApplicationLabel(int uid) { 354 String packageName = mPackageManager.getNameForUid(uid); 355 try { 356 ApplicationInfo appInfo = mPackageManager.getApplicationInfo( 357 packageName, PackageManager.ApplicationInfoFlags.of(0)); 358 return appInfo.loadLabel(mPackageManager).toString(); 359 } catch (PackageManager.NameNotFoundException e) { 360 return null; 361 } 362 } 363 364 /** 365 * A data class storing string resources of the notification of a device state. 366 */ 367 @VisibleForTesting 368 static class NotificationInfo { 369 public final String name; 370 public final String activeNotificationTitle; 371 public final String activeNotificationContent; 372 public final String thermalCriticalNotificationTitle; 373 public final String thermalCriticalNotificationContent; 374 public final String powerSaveModeNotificationTitle; 375 public final String powerSaveModeNotificationContent; 376 NotificationInfo(String name, String activeNotificationTitle, String activeNotificationContent, String thermalCriticalNotificationTitle, String thermalCriticalNotificationContent, String powerSaveModeNotificationTitle, String powerSaveModeNotificationContent)377 NotificationInfo(String name, String activeNotificationTitle, 378 String activeNotificationContent, String thermalCriticalNotificationTitle, 379 String thermalCriticalNotificationContent, String powerSaveModeNotificationTitle, 380 String powerSaveModeNotificationContent) { 381 382 this.name = name; 383 this.activeNotificationTitle = activeNotificationTitle; 384 this.activeNotificationContent = activeNotificationContent; 385 this.thermalCriticalNotificationTitle = thermalCriticalNotificationTitle; 386 this.thermalCriticalNotificationContent = thermalCriticalNotificationContent; 387 this.powerSaveModeNotificationTitle = powerSaveModeNotificationTitle; 388 this.powerSaveModeNotificationContent = powerSaveModeNotificationContent; 389 } 390 hasActiveNotification()391 boolean hasActiveNotification() { 392 return activeNotificationTitle != null && activeNotificationTitle.length() > 0; 393 } 394 hasThermalCriticalNotification()395 boolean hasThermalCriticalNotification() { 396 return thermalCriticalNotificationTitle != null 397 && thermalCriticalNotificationTitle.length() > 0; 398 } 399 hasPowerSaveModeNotification()400 boolean hasPowerSaveModeNotification() { 401 return powerSaveModeNotificationTitle != null 402 && powerSaveModeNotificationTitle.length() > 0; 403 } 404 } 405 } 406