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