1 /* 2 * Copyright (C) 2014 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.systemui.power; 18 19 import static android.app.PendingIntent.FLAG_IMMUTABLE; 20 21 import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_CONFIRMATION; 22 import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_LOW_WARNING; 23 import static com.android.settingslib.fuelgauge.BatterySaverLogging.SaverManualEnabledReason; 24 25 import android.app.Dialog; 26 import android.app.KeyguardManager; 27 import android.app.Notification; 28 import android.app.NotificationManager; 29 import android.app.PendingIntent; 30 import android.content.ActivityNotFoundException; 31 import android.content.BroadcastReceiver; 32 import android.content.ContentResolver; 33 import android.content.Context; 34 import android.content.DialogInterface; 35 import android.content.Intent; 36 import android.content.IntentFilter; 37 import android.media.AudioAttributes; 38 import android.net.Uri; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.os.Looper; 42 import android.os.PowerManager; 43 import android.os.UserHandle; 44 import android.provider.Settings; 45 import android.provider.Settings.Global; 46 import android.provider.Settings.Secure; 47 import android.text.Annotation; 48 import android.text.Layout; 49 import android.text.SpannableString; 50 import android.text.SpannableStringBuilder; 51 import android.text.TextPaint; 52 import android.text.TextUtils; 53 import android.text.method.LinkMovementMethod; 54 import android.text.style.URLSpan; 55 import android.util.Log; 56 import android.util.Slog; 57 import android.view.View; 58 import android.view.WindowManager; 59 60 import androidx.annotation.VisibleForTesting; 61 62 import com.android.internal.jank.InteractionJankMonitor; 63 import com.android.internal.logging.UiEventLogger; 64 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; 65 import com.android.settingslib.Utils; 66 import com.android.settingslib.fuelgauge.BatterySaverUtils; 67 import com.android.systemui.SystemUIApplication; 68 import com.android.systemui.animation.DialogCuj; 69 import com.android.systemui.animation.DialogTransitionAnimator; 70 import com.android.systemui.animation.Expandable; 71 import com.android.systemui.broadcast.BroadcastSender; 72 import com.android.systemui.dagger.SysUISingleton; 73 import com.android.systemui.plugins.ActivityStarter; 74 import com.android.systemui.res.R; 75 import com.android.systemui.settings.UserTracker; 76 import com.android.systemui.statusbar.phone.SystemUIDialog; 77 import com.android.systemui.statusbar.policy.BatteryController; 78 import com.android.systemui.util.NotificationChannels; 79 import com.android.systemui.volume.Events; 80 81 import dagger.Lazy; 82 83 import java.io.PrintWriter; 84 import java.lang.ref.WeakReference; 85 import java.text.NumberFormat; 86 import java.util.Locale; 87 import java.util.Objects; 88 89 import javax.inject.Inject; 90 91 /** 92 */ 93 @SysUISingleton 94 public class PowerNotificationWarnings implements PowerUI.WarningsUI { 95 96 private static final String TAG = PowerUI.TAG + ".Notification"; 97 private static final boolean DEBUG = PowerUI.DEBUG; 98 99 private static final String TAG_BATTERY = "low_battery"; 100 private static final String TAG_TEMPERATURE = "high_temp"; 101 private static final String TAG_AUTO_SAVER = "auto_saver"; 102 103 private static final String INTERACTION_JANK_TAG = "start_power_saver"; 104 105 private static final int SHOWING_NOTHING = 0; 106 private static final int SHOWING_WARNING = 1; 107 private static final int SHOWING_INVALID_CHARGER = 3; 108 private static final int SHOWING_AUTO_SAVER_SUGGESTION = 4; 109 private static final String[] SHOWING_STRINGS = { 110 "SHOWING_NOTHING", 111 "SHOWING_WARNING", 112 "SHOWING_SAVER", 113 "SHOWING_INVALID_CHARGER", 114 "SHOWING_AUTO_SAVER_SUGGESTION", 115 }; 116 117 private static final String ACTION_SHOW_BATTERY_SAVER_SETTINGS = "PNW.batterySaverSettings"; 118 private static final String ACTION_START_SAVER = "PNW.startSaver"; 119 private static final String ACTION_DISMISSED_WARNING = "PNW.dismissedWarning"; 120 private static final String ACTION_CLICKED_TEMP_WARNING = "PNW.clickedTempWarning"; 121 private static final String ACTION_DISMISSED_TEMP_WARNING = "PNW.dismissedTempWarning"; 122 private static final String ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING = 123 "PNW.clickedThermalShutdownWarning"; 124 private static final String ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING = 125 "PNW.dismissedThermalShutdownWarning"; 126 private static final String ACTION_SHOW_START_SAVER_CONFIRMATION = 127 BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION; 128 private static final String ACTION_SHOW_AUTO_SAVER_SUGGESTION = 129 BatterySaverUtils.ACTION_SHOW_AUTO_SAVER_SUGGESTION; 130 private static final String ACTION_DISMISS_AUTO_SAVER_SUGGESTION = 131 "PNW.dismissAutoSaverSuggestion"; 132 133 private static final String ACTION_ENABLE_AUTO_SAVER = 134 "PNW.enableAutoSaver"; 135 private static final String ACTION_AUTO_SAVER_NO_THANKS = 136 "PNW.autoSaverNoThanks"; 137 138 private static final String EXTRA_SCHEDULED_BY_PERCENTAGE = 139 "extra_scheduled_by_percentage"; 140 public static final String BATTERY_SAVER_SCHEDULE_SCREEN_INTENT_ACTION = 141 "com.android.settings.BATTERY_SAVER_SCHEDULE_SETTINGS"; 142 143 private static final String BATTERY_SAVER_DESCRIPTION_URL_KEY = "url"; 144 145 private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder() 146 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 147 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 148 .build(); 149 public static final String EXTRA_CONFIRM_ONLY = "extra_confirm_only"; 150 151 private final Context mContext; 152 private final SystemUIDialog.Factory mSystemUIDialogFactory; 153 private final NotificationManager mNoMan; 154 private final PowerManager mPowerMan; 155 private final KeyguardManager mKeyguard; 156 private final Handler mHandler = new Handler(Looper.getMainLooper()); 157 private final Receiver mReceiver = new Receiver(); 158 private final Intent mOpenBatterySettings = settings(Intent.ACTION_POWER_USAGE_SUMMARY); 159 private final Intent mOpenBatterySaverSettings = 160 settings(Settings.ACTION_BATTERY_SAVER_SETTINGS); 161 private final boolean mUseExtraSaverConfirmation; 162 163 private int mBatteryLevel; 164 private int mBucket; 165 private long mScreenOffTime; 166 private int mShowing; 167 168 private long mWarningTriggerTimeMs; 169 private boolean mWarning; 170 private boolean mShowAutoSaverSuggestion; 171 private boolean mPlaySound; 172 private boolean mInvalidCharger; 173 private SystemUIDialog mSaverConfirmation; 174 private SystemUIDialog mSaverEnabledConfirmation; 175 private boolean mHighTempWarning; 176 private SystemUIDialog mHighTempDialog; 177 private SystemUIDialog mThermalShutdownDialog; 178 @VisibleForTesting SystemUIDialog mUsbHighTempDialog; 179 private BatteryStateSnapshot mCurrentBatterySnapshot; 180 private ActivityStarter mActivityStarter; 181 private final BroadcastSender mBroadcastSender; 182 private final UiEventLogger mUiEventLogger; 183 private final UserTracker mUserTracker; 184 private final Lazy<BatteryController> mBatteryControllerLazy; 185 private final DialogTransitionAnimator mDialogTransitionAnimator; 186 187 /** 188 */ 189 @Inject PowerNotificationWarnings( Context context, ActivityStarter activityStarter, BroadcastSender broadcastSender, Lazy<BatteryController> batteryControllerLazy, DialogTransitionAnimator dialogTransitionAnimator, UiEventLogger uiEventLogger, UserTracker userTracker, SystemUIDialog.Factory systemUIDialogFactory)190 public PowerNotificationWarnings( 191 Context context, 192 ActivityStarter activityStarter, 193 BroadcastSender broadcastSender, 194 Lazy<BatteryController> batteryControllerLazy, 195 DialogTransitionAnimator dialogTransitionAnimator, 196 UiEventLogger uiEventLogger, 197 UserTracker userTracker, 198 SystemUIDialog.Factory systemUIDialogFactory) { 199 mContext = context; 200 mSystemUIDialogFactory = systemUIDialogFactory; 201 mNoMan = mContext.getSystemService(NotificationManager.class); 202 mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 203 mKeyguard = mContext.getSystemService(KeyguardManager.class); 204 mReceiver.init(); 205 mActivityStarter = activityStarter; 206 mBroadcastSender = broadcastSender; 207 mBatteryControllerLazy = batteryControllerLazy; 208 mDialogTransitionAnimator = dialogTransitionAnimator; 209 mUiEventLogger = uiEventLogger; 210 mUserTracker = userTracker; 211 mUseExtraSaverConfirmation = 212 mContext.getResources().getBoolean(R.bool.config_extra_battery_saver_confirmation); 213 } 214 215 @Override dump(PrintWriter pw)216 public void dump(PrintWriter pw) { 217 pw.print("mWarning="); pw.println(mWarning); 218 pw.print("mPlaySound="); pw.println(mPlaySound); 219 pw.print("mInvalidCharger="); pw.println(mInvalidCharger); 220 pw.print("mShowing="); pw.println(SHOWING_STRINGS[mShowing]); 221 pw.print("mSaverConfirmation="); pw.println(mSaverConfirmation != null ? "not null" : null); 222 pw.print("mSaverEnabledConfirmation="); 223 pw.print("mHighTempWarning="); pw.println(mHighTempWarning); 224 pw.print("mHighTempDialog="); pw.println(mHighTempDialog != null ? "not null" : null); 225 pw.print("mThermalShutdownDialog="); 226 pw.println(mThermalShutdownDialog != null ? "not null" : null); 227 pw.print("mUsbHighTempDialog="); 228 pw.println(mUsbHighTempDialog != null ? "not null" : null); 229 } 230 getLowBatteryAutoTriggerDefaultLevel()231 private int getLowBatteryAutoTriggerDefaultLevel() { 232 return mContext.getResources().getInteger( 233 com.android.internal.R.integer.config_lowBatteryAutoTriggerDefaultLevel); 234 } 235 236 @Override update(int batteryLevel, int bucket, long screenOffTime)237 public void update(int batteryLevel, int bucket, long screenOffTime) { 238 mBatteryLevel = batteryLevel; 239 if (bucket >= 0) { 240 mWarningTriggerTimeMs = 0; 241 } else if (bucket < mBucket) { 242 mWarningTriggerTimeMs = System.currentTimeMillis(); 243 } 244 mBucket = bucket; 245 mScreenOffTime = screenOffTime; 246 } 247 248 @Override updateSnapshot(BatteryStateSnapshot snapshot)249 public void updateSnapshot(BatteryStateSnapshot snapshot) { 250 mCurrentBatterySnapshot = snapshot; 251 } 252 updateNotification()253 private void updateNotification() { 254 if (DEBUG) Slog.d(TAG, "updateNotification mWarning=" + mWarning + " mPlaySound=" 255 + mPlaySound + " mInvalidCharger=" + mInvalidCharger); 256 if (mInvalidCharger) { 257 showInvalidChargerNotification(); 258 mShowing = SHOWING_INVALID_CHARGER; 259 } else if (mWarning) { 260 showWarningNotification(); 261 mShowing = SHOWING_WARNING; 262 } else if (mShowAutoSaverSuggestion) { 263 // Once we showed the notification, don't show it again until it goes SHOWING_NOTHING. 264 // This shouldn't be needed, because we have a delete intent on this notification 265 // so when it's dismissed we should notice it and clear mShowAutoSaverSuggestion, 266 // However we double check here just in case the dismiss intent broadcast is delayed. 267 if (mShowing != SHOWING_AUTO_SAVER_SUGGESTION) { 268 showAutoSaverSuggestionNotification(); 269 } 270 mShowing = SHOWING_AUTO_SAVER_SUGGESTION; 271 } else { 272 mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, UserHandle.ALL); 273 mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, UserHandle.ALL); 274 mNoMan.cancelAsUser(TAG_AUTO_SAVER, 275 SystemMessage.NOTE_AUTO_SAVER_SUGGESTION, UserHandle.ALL); 276 mShowing = SHOWING_NOTHING; 277 } 278 } 279 showInvalidChargerNotification()280 private void showInvalidChargerNotification() { 281 final Notification.Builder nb = 282 new Notification.Builder(mContext, NotificationChannels.ALERTS) 283 .setSmallIcon(R.drawable.ic_power_low) 284 .setWhen(0) 285 .setShowWhen(false) 286 .setOngoing(true) 287 .setContentTitle(mContext.getString(R.string.invalid_charger_title)) 288 .setContentText(mContext.getString(R.string.invalid_charger_text)) 289 .setColor(mContext.getColor( 290 com.android.internal.R.color.system_notification_accent_color)); 291 SystemUIApplication.overrideNotificationAppName(mContext, nb, false); 292 final Notification n = nb.build(); 293 mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, UserHandle.ALL); 294 mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, n, UserHandle.ALL); 295 } 296 showWarningNotification()297 protected void showWarningNotification() { 298 if (isScheduledByPercentage()) { 299 return; 300 } 301 302 final String percentage = NumberFormat.getPercentInstance() 303 .format((double) mCurrentBatterySnapshot.getBatteryLevel() / 100.0); 304 final String title = mContext.getString(R.string.battery_low_title); 305 final String contentText = mContext.getString( 306 R.string.battery_low_description, percentage); 307 308 final Notification.Builder nb = 309 new Notification.Builder(mContext, NotificationChannels.BATTERY) 310 .setSmallIcon(R.drawable.ic_power_low) 311 // Bump the notification when the bucket dropped. 312 .setWhen(mWarningTriggerTimeMs) 313 .setShowWhen(false) 314 .setContentText(contentText) 315 .setContentTitle(title) 316 .setOnlyAlertOnce(true) 317 .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING)) 318 .setStyle(new Notification.BigTextStyle().bigText(contentText)) 319 .setVisibility(Notification.VISIBILITY_PUBLIC); 320 if (hasBatterySettings()) { 321 nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SAVER_SETTINGS)); 322 } 323 // Make the notification red if the percentage goes below a certain amount or the time 324 // remaining estimate is disabled 325 if (!mCurrentBatterySnapshot.isHybrid() || mBucket < -1 326 || mCurrentBatterySnapshot.getTimeRemainingMillis() 327 < mCurrentBatterySnapshot.getSevereThresholdMillis()) { 328 nb.setColor(Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorError)); 329 } 330 331 if (!mPowerMan.isPowerSaveMode()) { 332 nb.addAction(0, mContext.getString(R.string.battery_saver_dismiss_action), 333 pendingBroadcast(ACTION_DISMISSED_WARNING)); 334 nb.addAction(0, 335 mContext.getString(R.string.battery_saver_start_action), 336 pendingBroadcast(ACTION_START_SAVER)); 337 } 338 nb.setOnlyAlertOnce(!mPlaySound); 339 mPlaySound = false; 340 SystemUIApplication.overrideNotificationAppName(mContext, nb, false); 341 final Notification n = nb.build(); 342 mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, UserHandle.ALL); 343 mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, n, UserHandle.ALL); 344 } 345 346 /** 347 * Checking battery saver schedule mode is set as "Based on percentage" or not. 348 * 349 * return {@code true} if scheduled by percentage. 350 */ isScheduledByPercentage()351 private boolean isScheduledByPercentage() { 352 final ContentResolver resolver = mContext.getContentResolver(); 353 final int mode = Settings.Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, 354 PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); 355 356 // Return false if battery saver mode trigger percentage is less than 0, which means it is 357 // set as "Based on routine" mode, otherwise it will be "Based on percentage" mode. 358 return mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE 359 && Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0) > 0; 360 } 361 showAutoSaverSuggestionNotification()362 private void showAutoSaverSuggestionNotification() { 363 final CharSequence message = mContext.getString(R.string.auto_saver_text); 364 final Notification.Builder nb = 365 new Notification.Builder(mContext, NotificationChannels.HINTS) 366 .setSmallIcon(R.drawable.ic_power_saver) 367 .setWhen(0) 368 .setShowWhen(false) 369 .setContentTitle(mContext.getString(R.string.auto_saver_title)) 370 .setStyle(new Notification.BigTextStyle().bigText(message)) 371 .setContentText(message); 372 nb.setContentIntent(pendingBroadcast(ACTION_ENABLE_AUTO_SAVER)); 373 nb.setDeleteIntent(pendingBroadcast(ACTION_DISMISS_AUTO_SAVER_SUGGESTION)); 374 nb.addAction(0, 375 mContext.getString(R.string.no_auto_saver_action), 376 pendingBroadcast(ACTION_AUTO_SAVER_NO_THANKS)); 377 378 SystemUIApplication.overrideNotificationAppName(mContext, nb, false); 379 380 final Notification n = nb.build(); 381 mNoMan.notifyAsUser( 382 TAG_AUTO_SAVER, SystemMessage.NOTE_AUTO_SAVER_SUGGESTION, n, UserHandle.ALL); 383 } 384 pendingBroadcast(String action)385 private PendingIntent pendingBroadcast(String action) { 386 return PendingIntent.getBroadcastAsUser( 387 mContext, 388 0 /* request code */, 389 new Intent(action) 390 .setPackage(mContext.getPackageName()) 391 .setFlags(Intent.FLAG_RECEIVER_FOREGROUND), 392 FLAG_IMMUTABLE /* flags */, 393 UserHandle.CURRENT); 394 } 395 settings(String action)396 private static Intent settings(String action) { 397 return new Intent(action).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 398 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK 399 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 400 | Intent.FLAG_ACTIVITY_NO_HISTORY 401 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 402 } 403 404 @Override isInvalidChargerWarningShowing()405 public boolean isInvalidChargerWarningShowing() { 406 return mInvalidCharger; 407 } 408 409 @Override dismissHighTemperatureWarning()410 public void dismissHighTemperatureWarning() { 411 if (!mHighTempWarning) { 412 return; 413 } 414 dismissHighTemperatureWarningInternal(); 415 } 416 417 /** 418 * Internal only version of {@link #dismissHighTemperatureWarning()} that simply dismisses 419 * the notification. As such, the notification will not show again until 420 * {@link #dismissHighTemperatureWarning()} is called. 421 */ dismissHighTemperatureWarningInternal()422 private void dismissHighTemperatureWarningInternal() { 423 mNoMan.cancelAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP, UserHandle.ALL); 424 mHighTempWarning = false; 425 } 426 427 @Override showHighTemperatureWarning()428 public void showHighTemperatureWarning() { 429 if (mHighTempWarning) { 430 return; 431 } 432 mHighTempWarning = true; 433 final String message = mContext.getString(R.string.high_temp_notif_message); 434 final Notification.Builder nb = 435 new Notification.Builder(mContext, NotificationChannels.ALERTS) 436 .setSmallIcon(R.drawable.ic_device_thermostat_24) 437 .setWhen(0) 438 .setShowWhen(false) 439 .setContentTitle(mContext.getString(R.string.high_temp_title)) 440 .setContentText(message) 441 .setStyle(new Notification.BigTextStyle().bigText(message)) 442 .setVisibility(Notification.VISIBILITY_PUBLIC) 443 .setContentIntent(pendingBroadcast(ACTION_CLICKED_TEMP_WARNING)) 444 .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_TEMP_WARNING)) 445 .setColor(Utils.getColorAttrDefaultColor(mContext, 446 android.R.attr.colorError)); 447 SystemUIApplication.overrideNotificationAppName(mContext, nb, false); 448 final Notification n = nb.build(); 449 mNoMan.notifyAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP, n, UserHandle.ALL); 450 } 451 showHighTemperatureDialog()452 private void showHighTemperatureDialog() { 453 if (mHighTempDialog != null) return; 454 final SystemUIDialog d = mSystemUIDialogFactory.create(); 455 d.setIconAttribute(android.R.attr.alertDialogIcon); 456 d.setTitle(R.string.high_temp_title); 457 d.setMessage(R.string.high_temp_dialog_message); 458 d.setPositiveButton(com.android.internal.R.string.ok, null); 459 d.setShowForAllUsers(true); 460 d.setOnDismissListener(dialog -> mHighTempDialog = null); 461 final String url = mContext.getString(R.string.high_temp_dialog_help_url); 462 if (!url.isEmpty()) { 463 d.setNeutralButton(R.string.high_temp_dialog_help_text, 464 new DialogInterface.OnClickListener() { 465 @Override 466 public void onClick(DialogInterface dialog, int which) { 467 final Intent helpIntent = 468 new Intent(Intent.ACTION_VIEW) 469 .setData(Uri.parse(url)) 470 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 471 mActivityStarter.startActivity(helpIntent, 472 true /* dismissShade */, resultCode -> { 473 mHighTempDialog = null; 474 }); 475 } 476 }); 477 } 478 d.show(); 479 mHighTempDialog = d; 480 } 481 482 @VisibleForTesting dismissThermalShutdownWarning()483 void dismissThermalShutdownWarning() { 484 mNoMan.cancelAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_THERMAL_SHUTDOWN, UserHandle.ALL); 485 } 486 showThermalShutdownDialog()487 private void showThermalShutdownDialog() { 488 if (mThermalShutdownDialog != null) return; 489 final SystemUIDialog d = mSystemUIDialogFactory.create(); 490 d.setIconAttribute(android.R.attr.alertDialogIcon); 491 d.setTitle(R.string.thermal_shutdown_title); 492 d.setMessage(R.string.thermal_shutdown_dialog_message); 493 d.setPositiveButton(com.android.internal.R.string.ok, null); 494 d.setShowForAllUsers(true); 495 d.setOnDismissListener(dialog -> mThermalShutdownDialog = null); 496 final String url = mContext.getString(R.string.thermal_shutdown_dialog_help_url); 497 if (!url.isEmpty()) { 498 d.setNeutralButton(R.string.thermal_shutdown_dialog_help_text, 499 new DialogInterface.OnClickListener() { 500 @Override 501 public void onClick(DialogInterface dialog, int which) { 502 final Intent helpIntent = 503 new Intent(Intent.ACTION_VIEW) 504 .setData(Uri.parse(url)) 505 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 506 mActivityStarter.startActivity(helpIntent, 507 true /* dismissShade */, resultCode -> { 508 mThermalShutdownDialog = null; 509 }); 510 } 511 }); 512 } 513 d.show(); 514 mThermalShutdownDialog = d; 515 } 516 517 @Override showThermalShutdownWarning()518 public void showThermalShutdownWarning() { 519 final String message = mContext.getString(R.string.thermal_shutdown_message); 520 final Notification.Builder nb = 521 new Notification.Builder(mContext, NotificationChannels.ALERTS) 522 .setSmallIcon(R.drawable.ic_device_thermostat_24) 523 .setWhen(0) 524 .setShowWhen(false) 525 .setContentTitle(mContext.getString(R.string.thermal_shutdown_title)) 526 .setContentText(message) 527 .setStyle(new Notification.BigTextStyle().bigText(message)) 528 .setVisibility(Notification.VISIBILITY_PUBLIC) 529 .setContentIntent(pendingBroadcast(ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING)) 530 .setDeleteIntent( 531 pendingBroadcast(ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING)) 532 .setColor(Utils.getColorAttrDefaultColor(mContext, 533 android.R.attr.colorError)); 534 SystemUIApplication.overrideNotificationAppName(mContext, nb, false); 535 final Notification n = nb.build(); 536 mNoMan.notifyAsUser( 537 TAG_TEMPERATURE, SystemMessage.NOTE_THERMAL_SHUTDOWN, n, UserHandle.ALL); 538 } 539 540 @Override showUsbHighTemperatureAlarm()541 public void showUsbHighTemperatureAlarm() { 542 mHandler.post(() -> showUsbHighTemperatureAlarmInternal()); 543 } 544 showUsbHighTemperatureAlarmInternal()545 private void showUsbHighTemperatureAlarmInternal() { 546 if (mUsbHighTempDialog != null) { 547 return; 548 } 549 550 final SystemUIDialog d = new SystemUIDialog(mContext, R.style.Theme_SystemUI_Dialog_Alert); 551 d.setCancelable(false); 552 d.setIconAttribute(android.R.attr.alertDialogIcon); 553 d.setTitle(R.string.high_temp_alarm_title); 554 d.setShowForAllUsers(true); 555 d.setMessage(mContext.getString(R.string.high_temp_alarm_notify_message, "")); 556 d.setPositiveButton((com.android.internal.R.string.ok), 557 (dialogInterface, which) -> mUsbHighTempDialog = null); 558 d.setNegativeButton((R.string.high_temp_alarm_help_care_steps), 559 (dialogInterface, which) -> { 560 final String contextString = mContext.getString( 561 R.string.high_temp_alarm_help_url); 562 final Intent helpIntent = new Intent(); 563 helpIntent.setClassName("com.android.settings", 564 "com.android.settings.HelpTrampoline"); 565 helpIntent.putExtra(Intent.EXTRA_TEXT, contextString); 566 mActivityStarter.startActivity(helpIntent, 567 true /* dismissShade */, resultCode -> { 568 mUsbHighTempDialog = null; 569 }); 570 }); 571 d.setOnDismissListener(dialogInterface -> { 572 mUsbHighTempDialog = null; 573 Events.writeEvent(Events.EVENT_DISMISS_USB_OVERHEAT_ALARM, 574 Events.DISMISS_REASON_USB_OVERHEAD_ALARM_CHANGED, 575 mKeyguard.isKeyguardLocked()); 576 }); 577 d.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 578 | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 579 d.show(); 580 mUsbHighTempDialog = d; 581 582 Events.writeEvent(Events.EVENT_SHOW_USB_OVERHEAT_ALARM, 583 Events.SHOW_REASON_USB_OVERHEAD_ALARM_CHANGED, 584 mKeyguard.isKeyguardLocked()); 585 } 586 587 @Override updateLowBatteryWarning()588 public void updateLowBatteryWarning() { 589 updateNotification(); 590 } 591 592 @Override dismissLowBatteryWarning()593 public void dismissLowBatteryWarning() { 594 if (DEBUG) Slog.d(TAG, "dismissing low battery warning: level=" + mBatteryLevel); 595 dismissLowBatteryNotification(); 596 } 597 dismissLowBatteryNotification()598 private void dismissLowBatteryNotification() { 599 if (mWarning) Slog.i(TAG, "dismissing low battery notification"); 600 mWarning = false; 601 updateNotification(); 602 } 603 hasBatterySettings()604 private boolean hasBatterySettings() { 605 return mOpenBatterySettings.resolveActivity(mContext.getPackageManager()) != null; 606 } 607 608 @Override showLowBatteryWarning(boolean playSound)609 public void showLowBatteryWarning(boolean playSound) { 610 Slog.i(TAG, 611 "show low battery warning: level=" + mBatteryLevel 612 + " [" + mBucket + "] playSound=" + playSound); 613 logEvent(BatteryWarningEvents.LowBatteryWarningEvent.LOW_BATTERY_NOTIFICATION); 614 mPlaySound = playSound; 615 mWarning = true; 616 updateNotification(); 617 } 618 619 @Override dismissInvalidChargerWarning()620 public void dismissInvalidChargerWarning() { 621 dismissInvalidChargerNotification(); 622 } 623 dismissInvalidChargerNotification()624 private void dismissInvalidChargerNotification() { 625 if (mInvalidCharger) Slog.i(TAG, "dismissing invalid charger notification"); 626 mInvalidCharger = false; 627 updateNotification(); 628 } 629 630 @Override showInvalidChargerWarning()631 public void showInvalidChargerWarning() { 632 mInvalidCharger = true; 633 updateNotification(); 634 } 635 showAutoSaverSuggestion()636 private void showAutoSaverSuggestion() { 637 mShowAutoSaverSuggestion = true; 638 updateNotification(); 639 } 640 dismissAutoSaverSuggestion()641 private void dismissAutoSaverSuggestion() { 642 mShowAutoSaverSuggestion = false; 643 updateNotification(); 644 } 645 646 @Override userSwitched()647 public void userSwitched() { 648 updateNotification(); 649 } 650 showStartSaverConfirmation(Bundle extras)651 private void showStartSaverConfirmation(Bundle extras) { 652 if (mSaverConfirmation != null || mUseExtraSaverConfirmation) return; 653 final SystemUIDialog d = mSystemUIDialogFactory.create(); 654 final boolean confirmOnly = extras.getBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY); 655 final int batterySaverTriggerMode = 656 extras.getInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER, 657 PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); 658 final int batterySaverTriggerLevel = 659 extras.getInt(BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL, 0); 660 d.setMessage(getBatterySaverDescription()); 661 662 // Sad hack for http://b/78261259 and http://b/78298335. Otherwise "Battery" may be split 663 // into "Bat-tery". 664 if (isEnglishLocale()) { 665 d.setMessageHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE); 666 } 667 // We need to set LinkMovementMethod to make the link clickable. 668 d.setMessageMovementMethod(LinkMovementMethod.getInstance()); 669 670 if (confirmOnly) { 671 d.setTitle(R.string.battery_saver_confirmation_title_generic); 672 d.setPositiveButton(com.android.internal.R.string.confirm_battery_saver, 673 (dialog, which) -> { 674 final ContentResolver resolver = mContext.getContentResolver(); 675 Settings.Global.putInt( 676 resolver, 677 Global.AUTOMATIC_POWER_SAVE_MODE, 678 batterySaverTriggerMode); 679 Settings.Global.putInt( 680 resolver, 681 Global.LOW_POWER_MODE_TRIGGER_LEVEL, 682 batterySaverTriggerLevel); 683 Secure.putIntForUser( 684 resolver, 685 Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 686 1, mUserTracker.getUserId()); 687 Secure.putIntForUser( 688 resolver, 689 Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, 690 1, mUserTracker.getUserId()); 691 }); 692 } else { 693 d.setTitle(R.string.battery_saver_confirmation_title); 694 d.setPositiveButton(R.string.battery_saver_confirmation_ok, 695 (dialog, which) -> { 696 setSaverMode(true, false, SAVER_ENABLED_CONFIRMATION); 697 logEvent(BatteryWarningEvents.LowBatteryWarningEvent.SAVER_CONFIRM_OK); 698 }); 699 d.setNegativeButton(android.R.string.cancel, (dialog, which) -> 700 logEvent(BatteryWarningEvents.LowBatteryWarningEvent.SAVER_CONFIRM_CANCEL)); 701 } 702 d.setShowForAllUsers(true); 703 d.setOnDismissListener((dialog) -> { 704 mSaverConfirmation = null; 705 logEvent(BatteryWarningEvents.LowBatteryWarningEvent.SAVER_CONFIRM_DISMISS); 706 }); 707 WeakReference<Expandable> ref = 708 mBatteryControllerLazy.get().getLastPowerSaverStartExpandable(); 709 if (ref != null && ref.get() != null) { 710 DialogTransitionAnimator.Controller controller = ref.get().dialogTransitionController( 711 new DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, 712 INTERACTION_JANK_TAG)); 713 if (controller != null) { 714 mDialogTransitionAnimator.show(d, controller); 715 } else { 716 d.show(); 717 } 718 } else { 719 d.show(); 720 } 721 logEvent(BatteryWarningEvents.LowBatteryWarningEvent.SAVER_CONFIRM_DIALOG); 722 mSaverConfirmation = d; 723 mBatteryControllerLazy.get().clearLastPowerSaverStartExpandable(); 724 } 725 726 @VisibleForTesting getSaverConfirmationDialog()727 Dialog getSaverConfirmationDialog() { 728 return mSaverConfirmation; 729 } 730 isEnglishLocale()731 private boolean isEnglishLocale() { 732 return Objects.equals(Locale.getDefault().getLanguage(), 733 Locale.ENGLISH.getLanguage()); 734 } 735 736 /** 737 * Generates the message for the "want to start battery saver?" dialog with a "learn more" link. 738 */ getBatterySaverDescription()739 private CharSequence getBatterySaverDescription() { 740 final String learnMoreUrl = mContext.getText( 741 R.string.help_uri_battery_saver_learn_more_link_target).toString(); 742 743 // If there's no link, use the string with no "learn more". 744 if (TextUtils.isEmpty(learnMoreUrl)) { 745 return mContext.getText(R.string.battery_low_intro); 746 } 747 748 // If we have a link, use the string with the "learn more" link. 749 final CharSequence rawText = mContext.getText( 750 com.android.internal.R.string.battery_saver_description_with_learn_more); 751 final SpannableString message = new SpannableString(rawText); 752 final SpannableStringBuilder builder = new SpannableStringBuilder(message); 753 754 // Look for the "learn more" part of the string, and set a URL span on it. 755 // We use a customized URLSpan to add FLAG_RECEIVER_FOREGROUND to the intent, and 756 // also to close the dialog. 757 for (Annotation annotation : message.getSpans(0, message.length(), Annotation.class)) { 758 final String key = annotation.getValue(); 759 760 if (!BATTERY_SAVER_DESCRIPTION_URL_KEY.equals(key)) { 761 continue; 762 } 763 final int start = message.getSpanStart(annotation); 764 final int end = message.getSpanEnd(annotation); 765 766 // Replace the "learn more" with a custom URL span, with 767 // - No underline. 768 // - When clicked, close the dialog and the notification shade. 769 final URLSpan urlSpan = new URLSpan(learnMoreUrl) { 770 @Override 771 public void updateDrawState(TextPaint ds) { 772 super.updateDrawState(ds); 773 ds.setUnderlineText(false); 774 } 775 776 @Override 777 public void onClick(View widget) { 778 // Close the parent dialog. 779 if (mSaverConfirmation != null) { 780 mSaverConfirmation.dismiss(); 781 } 782 // Also close the notification shade, if it's open. 783 mBroadcastSender.closeSystemDialogs(); 784 785 final Uri uri = Uri.parse(getURL()); 786 Context context = widget.getContext(); 787 Intent intent = new Intent(Intent.ACTION_VIEW, uri) 788 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 789 try { 790 context.startActivity(intent); 791 } catch (ActivityNotFoundException e) { 792 Log.w(TAG, "Activity was not found for intent, " + intent.toString()); 793 } 794 } 795 }; 796 builder.setSpan(urlSpan, start, end, message.getSpanFlags(urlSpan)); 797 } 798 return builder; 799 } 800 setSaverMode(boolean mode, boolean needFirstTimeWarning, @SaverManualEnabledReason int reason)801 private void setSaverMode(boolean mode, boolean needFirstTimeWarning, 802 @SaverManualEnabledReason int reason) { 803 BatterySaverUtils.setPowerSaveMode(mContext, mode, needFirstTimeWarning, reason); 804 } 805 startBatterySaverSchedulePage()806 private void startBatterySaverSchedulePage() { 807 Intent intent = new Intent(BATTERY_SAVER_SCHEDULE_SCREEN_INTENT_ACTION); 808 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 809 mActivityStarter.startActivity(intent, true /* dismissShade */); 810 } 811 logEvent(BatteryWarningEvents.LowBatteryWarningEvent event)812 private void logEvent(BatteryWarningEvents.LowBatteryWarningEvent event) { 813 if (mUiEventLogger != null) { 814 mUiEventLogger.log(event); 815 } 816 } 817 818 private final class Receiver extends BroadcastReceiver { 819 init()820 public void init() { 821 IntentFilter filter = new IntentFilter(); 822 filter.addAction(ACTION_SHOW_BATTERY_SAVER_SETTINGS); 823 filter.addAction(ACTION_START_SAVER); 824 filter.addAction(ACTION_DISMISSED_WARNING); 825 filter.addAction(ACTION_CLICKED_TEMP_WARNING); 826 filter.addAction(ACTION_DISMISSED_TEMP_WARNING); 827 filter.addAction(ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING); 828 filter.addAction(ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING); 829 filter.addAction(ACTION_SHOW_START_SAVER_CONFIRMATION); 830 filter.addAction(ACTION_SHOW_AUTO_SAVER_SUGGESTION); 831 filter.addAction(ACTION_ENABLE_AUTO_SAVER); 832 filter.addAction(ACTION_AUTO_SAVER_NO_THANKS); 833 filter.addAction(ACTION_DISMISS_AUTO_SAVER_SUGGESTION); 834 mContext.registerReceiverAsUser(this, UserHandle.ALL, filter, 835 android.Manifest.permission.DEVICE_POWER, mHandler, Context.RECEIVER_EXPORTED); 836 } 837 838 @Override onReceive(Context context, Intent intent)839 public void onReceive(Context context, Intent intent) { 840 final String action = intent.getAction(); 841 Slog.i(TAG, "Received " + action); 842 if (action.equals(ACTION_SHOW_BATTERY_SAVER_SETTINGS)) { 843 logEvent(BatteryWarningEvents 844 .LowBatteryWarningEvent.LOW_BATTERY_NOTIFICATION_SETTINGS); 845 dismissLowBatteryNotification(); 846 mContext.startActivityAsUser(mOpenBatterySaverSettings, 847 mUserTracker.getUserHandle()); 848 } else if (action.equals(ACTION_START_SAVER)) { 849 logEvent(BatteryWarningEvents 850 .LowBatteryWarningEvent.LOW_BATTERY_NOTIFICATION_TURN_ON); 851 setSaverMode(true, true, SAVER_ENABLED_LOW_WARNING); 852 dismissLowBatteryNotification(); 853 } else if (action.equals(ACTION_SHOW_START_SAVER_CONFIRMATION)) { 854 dismissLowBatteryNotification(); 855 showStartSaverConfirmation(intent.getExtras()); 856 } else if (action.equals(ACTION_DISMISSED_WARNING)) { 857 logEvent(BatteryWarningEvents 858 .LowBatteryWarningEvent.LOW_BATTERY_NOTIFICATION_CANCEL); 859 dismissLowBatteryWarning(); 860 } else if (ACTION_CLICKED_TEMP_WARNING.equals(action)) { 861 dismissHighTemperatureWarningInternal(); 862 showHighTemperatureDialog(); 863 } else if (ACTION_DISMISSED_TEMP_WARNING.equals(action)) { 864 dismissHighTemperatureWarningInternal(); 865 } else if (ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING.equals(action)) { 866 dismissThermalShutdownWarning(); 867 showThermalShutdownDialog(); 868 } else if (ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING.equals(action)) { 869 dismissThermalShutdownWarning(); 870 } else if (ACTION_SHOW_AUTO_SAVER_SUGGESTION.equals(action)) { 871 showAutoSaverSuggestion(); 872 } else if (ACTION_DISMISS_AUTO_SAVER_SUGGESTION.equals(action)) { 873 dismissAutoSaverSuggestion(); 874 } else if (ACTION_ENABLE_AUTO_SAVER.equals(action)) { 875 dismissAutoSaverSuggestion(); 876 startBatterySaverSchedulePage(); 877 } else if (ACTION_AUTO_SAVER_NO_THANKS.equals(action)) { 878 dismissAutoSaverSuggestion(); 879 BatterySaverUtils.suppressAutoBatterySaver(context); 880 } 881 } 882 } 883 }