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