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.NotificationChannel; 21 import android.app.NotificationManager; 22 import android.app.PendingIntent; 23 import android.content.BroadcastReceiver; 24 import android.content.ContentResolver; 25 import android.content.Context; 26 import android.content.DialogInterface; 27 import android.content.DialogInterface.OnClickListener; 28 import android.content.DialogInterface.OnDismissListener; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.media.AudioAttributes; 32 import android.net.Uri; 33 import android.os.AsyncTask; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.PowerManager; 37 import android.os.SystemClock; 38 import android.os.UserHandle; 39 import android.provider.Settings; 40 import android.support.annotation.VisibleForTesting; 41 import android.util.Slog; 42 43 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; 44 import com.android.internal.notification.SystemNotificationChannels; 45 import com.android.settingslib.Utils; 46 import com.android.systemui.R; 47 import com.android.systemui.SystemUI; 48 import com.android.systemui.statusbar.phone.StatusBar; 49 import com.android.systemui.statusbar.phone.SystemUIDialog; 50 import com.android.systemui.util.NotificationChannels; 51 52 import java.io.PrintWriter; 53 import java.text.NumberFormat; 54 55 public class PowerNotificationWarnings implements PowerUI.WarningsUI { 56 private static final String TAG = PowerUI.TAG + ".Notification"; 57 private static final boolean DEBUG = PowerUI.DEBUG; 58 59 private static final String TAG_BATTERY = "low_battery"; 60 private static final String TAG_TEMPERATURE = "high_temp"; 61 62 private static final int SHOWING_NOTHING = 0; 63 private static final int SHOWING_WARNING = 1; 64 private static final int SHOWING_INVALID_CHARGER = 3; 65 private static final String[] SHOWING_STRINGS = { 66 "SHOWING_NOTHING", 67 "SHOWING_WARNING", 68 "SHOWING_SAVER", 69 "SHOWING_INVALID_CHARGER", 70 }; 71 72 private static final String ACTION_SHOW_BATTERY_SETTINGS = "PNW.batterySettings"; 73 private static final String ACTION_START_SAVER = "PNW.startSaver"; 74 private static final String ACTION_DISMISSED_WARNING = "PNW.dismissedWarning"; 75 private static final String ACTION_CLICKED_TEMP_WARNING = "PNW.clickedTempWarning"; 76 private static final String ACTION_DISMISSED_TEMP_WARNING = "PNW.dismissedTempWarning"; 77 private static final String ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING = 78 "PNW.clickedThermalShutdownWarning"; 79 private static final String ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING = 80 "PNW.dismissedThermalShutdownWarning"; 81 82 private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder() 83 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 84 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 85 .build(); 86 87 private final Context mContext; 88 private final NotificationManager mNoMan; 89 private final PowerManager mPowerMan; 90 private final Handler mHandler = new Handler(Looper.getMainLooper()); 91 private final Receiver mReceiver = new Receiver(); 92 private final Intent mOpenBatterySettings = settings(Intent.ACTION_POWER_USAGE_SUMMARY); 93 94 private int mBatteryLevel; 95 private int mBucket; 96 private long mScreenOffTime; 97 private int mShowing; 98 99 private long mBucketDroppedNegativeTimeMs; 100 101 private boolean mWarning; 102 private boolean mPlaySound; 103 private boolean mInvalidCharger; 104 private SystemUIDialog mSaverConfirmation; 105 private boolean mHighTempWarning; 106 private SystemUIDialog mHighTempDialog; 107 private SystemUIDialog mThermalShutdownDialog; 108 109 public PowerNotificationWarnings(Context context) { 110 mContext = context; 111 mNoMan = mContext.getSystemService(NotificationManager.class); 112 mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 113 mReceiver.init(); 114 } 115 116 @Override 117 public void dump(PrintWriter pw) { 118 pw.print("mWarning="); pw.println(mWarning); 119 pw.print("mPlaySound="); pw.println(mPlaySound); 120 pw.print("mInvalidCharger="); pw.println(mInvalidCharger); 121 pw.print("mShowing="); pw.println(SHOWING_STRINGS[mShowing]); 122 pw.print("mSaverConfirmation="); pw.println(mSaverConfirmation != null ? "not null" : null); 123 pw.print("mHighTempWarning="); pw.println(mHighTempWarning); 124 pw.print("mHighTempDialog="); pw.println(mHighTempDialog != null ? "not null" : null); 125 pw.print("mThermalShutdownDialog="); 126 pw.println(mThermalShutdownDialog != null ? "not null" : null); 127 } 128 129 @Override 130 public void update(int batteryLevel, int bucket, long screenOffTime) { 131 mBatteryLevel = batteryLevel; 132 if (bucket >= 0) { 133 mBucketDroppedNegativeTimeMs = 0; 134 } else if (bucket < mBucket) { 135 mBucketDroppedNegativeTimeMs = System.currentTimeMillis(); 136 } 137 mBucket = bucket; 138 mScreenOffTime = screenOffTime; 139 } 140 141 private void updateNotification() { 142 if (DEBUG) Slog.d(TAG, "updateNotification mWarning=" + mWarning + " mPlaySound=" 143 + mPlaySound + " mInvalidCharger=" + mInvalidCharger); 144 if (mInvalidCharger) { 145 showInvalidChargerNotification(); 146 mShowing = SHOWING_INVALID_CHARGER; 147 } else if (mWarning) { 148 showWarningNotification(); 149 mShowing = SHOWING_WARNING; 150 } else { 151 mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, UserHandle.ALL); 152 mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, UserHandle.ALL); 153 mShowing = SHOWING_NOTHING; 154 } 155 } 156 157 private void showInvalidChargerNotification() { 158 final Notification.Builder nb = 159 new Notification.Builder(mContext, NotificationChannels.ALERTS) 160 .setSmallIcon(R.drawable.ic_power_low) 161 .setWhen(0) 162 .setShowWhen(false) 163 .setOngoing(true) 164 .setContentTitle(mContext.getString(R.string.invalid_charger_title)) 165 .setContentText(mContext.getString(R.string.invalid_charger_text)) 166 .setColor(mContext.getColor( 167 com.android.internal.R.color.system_notification_accent_color)); 168 SystemUI.overrideNotificationAppName(mContext, nb); 169 final Notification n = nb.build(); 170 mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, UserHandle.ALL); 171 mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, n, UserHandle.ALL); 172 } 173 174 private void showWarningNotification() { 175 final int textRes = R.string.battery_low_percent_format; 176 final String percentage = NumberFormat.getPercentInstance().format((double) mBatteryLevel / 100.0); 177 178 final Notification.Builder nb = 179 new Notification.Builder(mContext, NotificationChannels.BATTERY) 180 .setSmallIcon(R.drawable.ic_power_low) 181 // Bump the notification when the bucket dropped. 182 .setWhen(mBucketDroppedNegativeTimeMs) 183 .setShowWhen(false) 184 .setContentTitle(mContext.getString(R.string.battery_low_title)) 185 .setContentText(mContext.getString(textRes, percentage)) 186 .setOnlyAlertOnce(true) 187 .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING)) 188 .setVisibility(Notification.VISIBILITY_PUBLIC) 189 .setColor(Utils.getColorAttr(mContext, android.R.attr.colorError)); 190 if (hasBatterySettings()) { 191 nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SETTINGS)); 192 } 193 nb.addAction(0, 194 mContext.getString(R.string.battery_saver_start_action), 195 pendingBroadcast(ACTION_START_SAVER)); 196 nb.setOnlyAlertOnce(!mPlaySound); 197 mPlaySound = false; 198 SystemUI.overrideNotificationAppName(mContext, nb); 199 final Notification n = nb.build(); 200 mNoMan.cancelAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, UserHandle.ALL); 201 mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, n, UserHandle.ALL); 202 } 203 204 private PendingIntent pendingBroadcast(String action) { 205 return PendingIntent.getBroadcastAsUser(mContext, 206 0, new Intent(action), 0, UserHandle.CURRENT); 207 } 208 209 private static Intent settings(String action) { 210 return new Intent(action).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 211 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK 212 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 213 | Intent.FLAG_ACTIVITY_NO_HISTORY 214 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 215 } 216 217 @Override 218 public boolean isInvalidChargerWarningShowing() { 219 return mInvalidCharger; 220 } 221 222 @Override 223 public void dismissHighTemperatureWarning() { 224 if (!mHighTempWarning) { 225 return; 226 } 227 mHighTempWarning = false; 228 dismissHighTemperatureWarningInternal(); 229 } 230 231 /** 232 * Internal only version of {@link #dismissHighTemperatureWarning()} that simply dismisses 233 * the notification. As such, the notification will not show again until 234 * {@link #dismissHighTemperatureWarning()} is called. 235 */ 236 private void dismissHighTemperatureWarningInternal() { 237 mNoMan.cancelAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP, UserHandle.ALL); 238 } 239 240 @Override 241 public void showHighTemperatureWarning() { 242 if (mHighTempWarning) { 243 return; 244 } 245 mHighTempWarning = true; 246 final Notification.Builder nb = 247 new Notification.Builder(mContext, NotificationChannels.ALERTS) 248 .setSmallIcon(R.drawable.ic_device_thermostat_24) 249 .setWhen(0) 250 .setShowWhen(false) 251 .setContentTitle(mContext.getString(R.string.high_temp_title)) 252 .setContentText(mContext.getString(R.string.high_temp_notif_message)) 253 .setVisibility(Notification.VISIBILITY_PUBLIC) 254 .setContentIntent(pendingBroadcast(ACTION_CLICKED_TEMP_WARNING)) 255 .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_TEMP_WARNING)) 256 .setColor(Utils.getColorAttr(mContext, android.R.attr.colorError)); 257 SystemUI.overrideNotificationAppName(mContext, nb); 258 final Notification n = nb.build(); 259 mNoMan.notifyAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP, n, UserHandle.ALL); 260 } 261 262 private void showHighTemperatureDialog() { 263 if (mHighTempDialog != null) return; 264 final SystemUIDialog d = new SystemUIDialog(mContext); 265 d.setIconAttribute(android.R.attr.alertDialogIcon); 266 d.setTitle(R.string.high_temp_title); 267 d.setMessage(R.string.high_temp_dialog_message); 268 d.setPositiveButton(com.android.internal.R.string.ok, null); 269 d.setShowForAllUsers(true); 270 d.setOnDismissListener(dialog -> mHighTempDialog = null); 271 d.show(); 272 mHighTempDialog = d; 273 } 274 275 @VisibleForTesting 276 void dismissThermalShutdownWarning() { 277 mNoMan.cancelAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_THERMAL_SHUTDOWN, UserHandle.ALL); 278 } 279 280 private void showThermalShutdownDialog() { 281 if (mThermalShutdownDialog != null) return; 282 final SystemUIDialog d = new SystemUIDialog(mContext); 283 d.setIconAttribute(android.R.attr.alertDialogIcon); 284 d.setTitle(R.string.thermal_shutdown_title); 285 d.setMessage(R.string.thermal_shutdown_dialog_message); 286 d.setPositiveButton(com.android.internal.R.string.ok, null); 287 d.setShowForAllUsers(true); 288 d.setOnDismissListener(dialog -> mThermalShutdownDialog = null); 289 d.show(); 290 mThermalShutdownDialog = d; 291 } 292 293 @Override 294 public void showThermalShutdownWarning() { 295 final Notification.Builder nb = 296 new Notification.Builder(mContext, NotificationChannels.ALERTS) 297 .setSmallIcon(R.drawable.ic_device_thermostat_24) 298 .setWhen(0) 299 .setShowWhen(false) 300 .setContentTitle(mContext.getString(R.string.thermal_shutdown_title)) 301 .setContentText(mContext.getString(R.string.thermal_shutdown_message)) 302 .setVisibility(Notification.VISIBILITY_PUBLIC) 303 .setContentIntent(pendingBroadcast(ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING)) 304 .setDeleteIntent( 305 pendingBroadcast(ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING)) 306 .setColor(Utils.getColorAttr(mContext, android.R.attr.colorError)); 307 SystemUI.overrideNotificationAppName(mContext, nb); 308 final Notification n = nb.build(); 309 mNoMan.notifyAsUser( 310 TAG_TEMPERATURE, SystemMessage.NOTE_THERMAL_SHUTDOWN, n, UserHandle.ALL); 311 } 312 313 @Override 314 public void updateLowBatteryWarning() { 315 updateNotification(); 316 } 317 318 @Override 319 public void dismissLowBatteryWarning() { 320 if (DEBUG) Slog.d(TAG, "dismissing low battery warning: level=" + mBatteryLevel); 321 dismissLowBatteryNotification(); 322 } 323 324 private void dismissLowBatteryNotification() { 325 if (mWarning) Slog.i(TAG, "dismissing low battery notification"); 326 mWarning = false; 327 updateNotification(); 328 } 329 330 private boolean hasBatterySettings() { 331 return mOpenBatterySettings.resolveActivity(mContext.getPackageManager()) != null; 332 } 333 334 @Override 335 public void showLowBatteryWarning(boolean playSound) { 336 Slog.i(TAG, 337 "show low battery warning: level=" + mBatteryLevel 338 + " [" + mBucket + "] playSound=" + playSound); 339 mPlaySound = playSound; 340 mWarning = true; 341 updateNotification(); 342 } 343 344 @Override 345 public void dismissInvalidChargerWarning() { 346 dismissInvalidChargerNotification(); 347 } 348 349 private void dismissInvalidChargerNotification() { 350 if (mInvalidCharger) Slog.i(TAG, "dismissing invalid charger notification"); 351 mInvalidCharger = false; 352 updateNotification(); 353 } 354 355 @Override 356 public void showInvalidChargerWarning() { 357 mInvalidCharger = true; 358 updateNotification(); 359 } 360 361 @Override 362 public void userSwitched() { 363 updateNotification(); 364 } 365 366 private void showStartSaverConfirmation() { 367 if (mSaverConfirmation != null) return; 368 final SystemUIDialog d = new SystemUIDialog(mContext); 369 d.setTitle(R.string.battery_saver_confirmation_title); 370 d.setMessage(com.android.internal.R.string.battery_saver_description); 371 d.setNegativeButton(android.R.string.cancel, null); 372 d.setPositiveButton(R.string.battery_saver_confirmation_ok, mStartSaverMode); 373 d.setShowForAllUsers(true); 374 d.setOnDismissListener(new OnDismissListener() { 375 @Override 376 public void onDismiss(DialogInterface dialog) { 377 mSaverConfirmation = null; 378 } 379 }); 380 d.show(); 381 mSaverConfirmation = d; 382 } 383 384 private void setSaverMode(boolean mode) { 385 mPowerMan.setPowerSaveMode(mode); 386 } 387 388 private final class Receiver extends BroadcastReceiver { 389 390 public void init() { 391 IntentFilter filter = new IntentFilter(); 392 filter.addAction(ACTION_SHOW_BATTERY_SETTINGS); 393 filter.addAction(ACTION_START_SAVER); 394 filter.addAction(ACTION_DISMISSED_WARNING); 395 filter.addAction(ACTION_CLICKED_TEMP_WARNING); 396 filter.addAction(ACTION_DISMISSED_TEMP_WARNING); 397 filter.addAction(ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING); 398 filter.addAction(ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING); 399 mContext.registerReceiverAsUser(this, UserHandle.ALL, filter, 400 android.Manifest.permission.STATUS_BAR_SERVICE, mHandler); 401 } 402 403 @Override 404 public void onReceive(Context context, Intent intent) { 405 final String action = intent.getAction(); 406 Slog.i(TAG, "Received " + action); 407 if (action.equals(ACTION_SHOW_BATTERY_SETTINGS)) { 408 dismissLowBatteryNotification(); 409 mContext.startActivityAsUser(mOpenBatterySettings, UserHandle.CURRENT); 410 } else if (action.equals(ACTION_START_SAVER)) { 411 dismissLowBatteryNotification(); 412 showStartSaverConfirmation(); 413 } else if (action.equals(ACTION_DISMISSED_WARNING)) { 414 dismissLowBatteryWarning(); 415 } else if (ACTION_CLICKED_TEMP_WARNING.equals(action)) { 416 dismissHighTemperatureWarningInternal(); 417 showHighTemperatureDialog(); 418 } else if (ACTION_DISMISSED_TEMP_WARNING.equals(action)) { 419 dismissHighTemperatureWarningInternal(); 420 } else if (ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING.equals(action)) { 421 dismissThermalShutdownWarning(); 422 showThermalShutdownDialog(); 423 } else if (ACTION_DISMISSED_THERMAL_SHUTDOWN_WARNING.equals(action)) { 424 dismissThermalShutdownWarning(); 425 } 426 } 427 } 428 429 private final OnClickListener mStartSaverMode = new OnClickListener() { 430 @Override 431 public void onClick(DialogInterface dialog, int which) { 432 AsyncTask.execute(new Runnable() { 433 @Override 434 public void run() { 435 setSaverMode(true); 436 } 437 }); 438 } 439 }; 440 } 441