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