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