From 32ed3d673ef460ab1f2fddd798e6682d4592aa39 Mon Sep 17 00:00:00 2001 From: Bill Lin Date: Tue, 2 Oct 2018 18:10:09 +0800 Subject: [PATCH] DO NOT MERGE Implement USB High Temperature warning dialog(1/N) When device USB adapter got electric short issue and overheating. Popup overheat alarm dialog, with beep sound and vibration(optional) to notify user unplug USB cable, this dialog will keep showing at foreground until user click OK button or see care steps. Test: manual adjust threshold to 35 degrees and trigger alarm Test: atest SystemUITests Test: atest PowerUITest Test: atest PowerNotificationWarningsTest Test: atest OverheatAlarmControllerTest Test: atest OverheatAlarmDialogTest Change-Id: I35e4269b2f3f2108043e2756d0f5a53f54d86e7d Fix: b/117138158 --- .../res/layout/overheat_dialog_content.xml | 39 +++ .../res/layout/overheat_dialog_title.xml | 46 ++++ packages/SystemUI/res/raw/overheat_alarm.ogg | Bin 0 -> 54993 bytes packages/SystemUI/res/values/config.xml | 14 + packages/SystemUI/res/values/dimens.xml | 7 + packages/SystemUI/res/values/strings.xml | 8 + .../power/OverheatAlarmController.java | 158 +++++++++++ .../systemui/power/OverheatAlarmDialog.java | 181 +++++++++++++ .../power/PowerNotificationWarnings.java | 112 +++++++- .../com/android/systemui/power/PowerUI.java | 249 ++++++++++++++---- .../com/android/systemui/volume/Events.java | 23 +- packages/SystemUI/tests/AndroidManifest.xml | 1 + .../power/OverheatAlarmControllerTest.java | 87 ++++++ .../power/OverheatAlarmDialogTest.java | 130 +++++++++ .../power/PowerNotificationWarningsTest.java | 160 ++++++++++- .../android/systemui/power/PowerUITest.java | 96 ++++++- proto/src/metrics_constants.proto | 5 + 17 files changed, 1255 insertions(+), 61 deletions(-) create mode 100644 packages/SystemUI/res/layout/overheat_dialog_content.xml create mode 100644 packages/SystemUI/res/layout/overheat_dialog_title.xml create mode 100644 packages/SystemUI/res/raw/overheat_alarm.ogg create mode 100644 packages/SystemUI/src/com/android/systemui/power/OverheatAlarmController.java create mode 100644 packages/SystemUI/src/com/android/systemui/power/OverheatAlarmDialog.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/power/OverheatAlarmControllerTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/power/OverheatAlarmDialogTest.java diff --git a/packages/SystemUI/res/layout/overheat_dialog_content.xml b/packages/SystemUI/res/layout/overheat_dialog_content.xml new file mode 100644 index 0000000000000..d78272fe6abc9 --- /dev/null +++ b/packages/SystemUI/res/layout/overheat_dialog_content.xml @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/packages/SystemUI/res/layout/overheat_dialog_title.xml b/packages/SystemUI/res/layout/overheat_dialog_title.xml new file mode 100644 index 0000000000000..65a512ae085be --- /dev/null +++ b/packages/SystemUI/res/layout/overheat_dialog_title.xml @@ -0,0 +1,46 @@ + + + + + + + diff --git a/packages/SystemUI/res/raw/overheat_alarm.ogg b/packages/SystemUI/res/raw/overheat_alarm.ogg new file mode 100644 index 0000000000000000000000000000000000000000..5624f42a5c500ee9ddd14a331494427d98a943e1 GIT binary patch literal 54993 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 + + false + 0 + + 0 + -1 + + 60 + 2 + + 5 + diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 79e1fae98c216..f5f198d442575 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1023,4 +1023,11 @@ 12dp + + + 24dp + + + 28dp + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 18f378e5919b1..114fbe4daaf7e 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2067,6 +2067,14 @@ Some features limited while phone cools down Your phone will automatically try to cool down. You can still use your phone, but it may run slower.\n\nOnce your phone has cooled down, it will run normally. + + Unplug charger + + There\u2019s an issue charging this device. Unplug the power adapter and take care as the cable may be warm. + + See care steps + + help_uri_usb_warm Left shortcut diff --git a/packages/SystemUI/src/com/android/systemui/power/OverheatAlarmController.java b/packages/SystemUI/src/com/android/systemui/power/OverheatAlarmController.java new file mode 100644 index 0000000000000..db15909196c7d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/power/OverheatAlarmController.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.power; + +import static android.content.Context.VIBRATOR_SERVICE; + +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; + +import android.content.ContentResolver; +import android.content.Context; +import android.media.AudioAttributes; +import android.net.Uri; +import android.os.Handler; +import android.os.VibrationEffect; +import android.os.Vibrator; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.R; +import com.android.systemui.media.NotificationPlayer; + +/** + * A Controller handle beep sound, vibration and TTS depend on state of OverheatAlarmDialog. + */ +public class OverheatAlarmController { + private static final String TAG = OverheatAlarmController.class.getSimpleName(); + + private static final int VIBRATION_INTERVAL = 2000; + private static final long[] VIBRATION_PATTERN = new long[]{0, 400, 200, 400, 200, 400, 200}; + + private static OverheatAlarmController sInstance; + + private final Vibrator mVibrator; + + private NotificationPlayer mPlayer; + private VibrationEffect mVibrationEffect; + + private boolean mShouldVibrate; + + /** + * The constructor only used to create singleton sInstance. + */ + private OverheatAlarmController(Context context) { + mVibrator = (Vibrator) context.getSystemService(VIBRATOR_SERVICE); + } + + /** + * Get singleton OverheatAlarmController instance. + */ + public static OverheatAlarmController getInstance(Context context) { + if (context == null) { + throw new IllegalArgumentException(); + } + if (sInstance == null) { + sInstance = new OverheatAlarmController(context); + } + return sInstance; + } + + /** + * Starting alarm beep sound and vibration. + */ + @VisibleForTesting + public void startAlarm(Context context) { + if (mPlayer != null) { + return; + } + playSound(context); + startVibrate(); + } + + /** + * Stop alarming beep sound, vibrating, and TTS if initialized. + */ + @VisibleForTesting + public void stopAlarm() { + stopPlayer(); + stopVibrate(); + } + + @VisibleForTesting + protected void playSound(Context context) { + Uri uri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(context.getBasePackageName()) + .appendPath(Integer.toString(R.raw.overheat_alarm)).build(); + + if (mPlayer == null) { + mPlayer = new NotificationPlayer(TAG); + } + mPlayer.setUsesWakeLock(context); + mPlayer.play(context, uri, true /* looping */, getAlertAudioAttributes()); + } + + @VisibleForTesting + protected void stopPlayer() { + if (mPlayer != null) { + mPlayer.stop(); + mPlayer = null; + } + } + + @VisibleForTesting + protected void startVibrate() { + mShouldVibrate = true; + if (mVibrationEffect == null) { + mVibrationEffect = VibrationEffect.createWaveform(VIBRATION_PATTERN, -1); + } + performVibrate(); + } + + @VisibleForTesting + protected void performVibrate() { + if (mShouldVibrate && mVibrator != null) { + mVibrator.vibrate(mVibrationEffect, getAlertAudioAttributes()); + Handler.getMain().sendMessageDelayed( + obtainMessage(OverheatAlarmController::performVibrate, this), + VIBRATION_INTERVAL); + } + } + + @VisibleForTesting + protected void stopVibrate() { + if (mVibrator != null) { + mVibrator.cancel(); + } + mShouldVibrate = false; + } + + /** + * Build AudioAttributes for mPlayer(NotificationPlayer) and vibrator + * Use the alarm channel so it can vibrate in DnD mode, unless alarms are + * specifically disabled in DnD. + */ + private static AudioAttributes getAlertAudioAttributes() { + AudioAttributes.Builder builder = new AudioAttributes.Builder(); + builder.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION); + builder.setUsage(AudioAttributes.USAGE_ALARM); + // Set FLAG_BYPASS_INTERRUPTION_POLICY and FLAG_BYPASS_MUTE so that it enables + // audio in any DnD mode, even in total silence DnD mode (requires MODIFY_PHONE_STATE). + builder.setFlags( + AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY | AudioAttributes.FLAG_BYPASS_MUTE); + return builder.build(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/power/OverheatAlarmDialog.java b/packages/SystemUI/src/com/android/systemui/power/OverheatAlarmDialog.java new file mode 100644 index 0000000000000..1ecec51162661 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/power/OverheatAlarmDialog.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.power; + +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.UserHandle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.TextView; + +import com.android.internal.annotations.VisibleForTesting; + +import com.android.systemui.R; + +/** + * The alarm dialog shown when the device overheats. + * When the temperature exceeds a threshold, we're showing this dialog to notify the user. + * Once the dialog shows, a sound and vibration will be played until the user touches the dialog. + */ +public class OverheatAlarmDialog extends AlertDialog { + private final OverheatDialogDelegate mOverheatDialogDelegate; + private final View mContentView, mTitleView; + + private static boolean sHasUserInteracted; + + private OverheatAlarmDialog.PowerEventReceiver mPowerEventReceiver; + + /** + * OverheatAlarmDialog should appear over system panels and keyguard. + */ + public OverheatAlarmDialog(Context context) { + super(context, R.style.Theme_SystemUI_Dialog_Alert); + mOverheatDialogDelegate = new OverheatDialogDelegate(); + + // Setup custom views, the purpose of set custom title and message is inject + // AccessibilityDelegate to solve beep sound and talk back mix problem + final LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + mContentView = inflater.inflate(R.layout.overheat_dialog_content, null); + mTitleView = inflater.inflate(R.layout.overheat_dialog_title, null); + setView(mContentView); + setCustomTitle(mTitleView); + + setupDialog(); + } + + @Override + public void dismiss() { + sHasUserInteracted = false; + getContext().unregisterReceiver(mPowerEventReceiver); + super.dismiss(); + } + + private void setupDialog() { + getWindow().getAttributes().privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; + getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + // Register ACTION_SCREEN_OFF for power Key event. + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + mPowerEventReceiver = new OverheatAlarmDialog.PowerEventReceiver(); + getContext().registerReceiverAsUser(mPowerEventReceiver, UserHandle.CURRENT, filter, null, + null); + } + + @Override + public void setTitle(CharSequence title) { + if (mTitleView == null) { + super.setTitle(title); + return; + } + final TextView titleTextView = mTitleView.findViewById(R.id.alertTitle); + if (titleTextView != null) { + titleTextView.setText(title); + titleTextView.setAccessibilityDelegate(mOverheatDialogDelegate); + } + } + + @Override + public void setMessage(CharSequence message) { + if (mContentView == null) { + super.setMessage(message); + return; + } + final TextView messageView = mContentView.findViewById(android.R.id.message); + if (messageView != null) { + messageView.setAccessibilityDelegate(mOverheatDialogDelegate); + messageView.requestAccessibilityFocus(); + messageView.setText(message); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (!sHasUserInteracted) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_BACK: + notifyAlarmBeepSoundChange(); + sHasUserInteracted = true; + break; + } + } + return super.dispatchKeyEvent(event); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + // Stop beep sound when touch alarm dialog. + if (!sHasUserInteracted) { + if (ev.getAction() == MotionEvent.ACTION_DOWN + || ev.getAction() == MotionEvent.ACTION_OUTSIDE) { + notifyAlarmBeepSoundChange(); + sHasUserInteracted = true; + } + } + return super.dispatchTouchEvent(ev); + } + + @VisibleForTesting + protected void notifyAlarmBeepSoundChange() { + this.getContext().sendBroadcast(new Intent(Intent.ACTION_ALARM_CHANGED).setPackage( + this.getContext().getPackageName()) + .setFlags(Intent.FLAG_RECEIVER_FOREGROUND)); + } + + private final class PowerEventReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!sHasUserInteracted) { + notifyAlarmBeepSoundChange(); + sHasUserInteracted = true; + } + } + } + + /** + * Implement AccessibilityDelegate to stop beep sound while title or message view get + * accessibility focus, in case the alarm beep sound mix up talk back description. + */ + private final class OverheatDialogDelegate extends View.AccessibilityDelegate { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS && !sHasUserInteracted) { + notifyAlarmBeepSoundChange(); + sHasUserInteracted = true; + } + return super.performAccessibilityAction(host, action, args); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index 288f058b529ad..c32a4435fa72f 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -16,6 +16,10 @@ package com.android.systemui.power; +import static android.content.DialogInterface.BUTTON_NEGATIVE; +import static android.content.DialogInterface.BUTTON_POSITIVE; + +import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -47,10 +51,13 @@ import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.settingslib.Utils; import com.android.settingslib.fuelgauge.BatterySaverUtils; import com.android.settingslib.utils.PowerUtil; +import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.SystemUI; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.NotificationChannels; +import com.android.systemui.volume.Events; import java.io.PrintWriter; import java.text.NumberFormat; @@ -111,6 +118,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private final Context mContext; private final NotificationManager mNoMan; private final PowerManager mPowerMan; + private final KeyguardManager mKeyguard; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final Receiver mReceiver = new Receiver(); private final Intent mOpenBatterySettings = settings(Intent.ACTION_POWER_USAGE_SUMMARY); @@ -134,25 +142,37 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private boolean mHighTempWarning; private SystemUIDialog mHighTempDialog; private SystemUIDialog mThermalShutdownDialog; + @VisibleForTesting + protected OverheatAlarmDialog mOverheatAlarmDialog; public PowerNotificationWarnings(Context context) { mContext = context; mNoMan = mContext.getSystemService(NotificationManager.class); mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mKeyguard = mContext.getSystemService(KeyguardManager.class); mReceiver.init(); } @Override public void dump(PrintWriter pw) { - pw.print("mWarning="); pw.println(mWarning); - pw.print("mPlaySound="); pw.println(mPlaySound); - pw.print("mInvalidCharger="); pw.println(mInvalidCharger); - pw.print("mShowing="); pw.println(SHOWING_STRINGS[mShowing]); - pw.print("mSaverConfirmation="); pw.println(mSaverConfirmation != null ? "not null" : null); + pw.print("mWarning="); + pw.println(mWarning); + pw.print("mPlaySound="); + pw.println(mPlaySound); + pw.print("mInvalidCharger="); + pw.println(mInvalidCharger); + pw.print("mShowing="); + pw.println(SHOWING_STRINGS[mShowing]); + pw.print("mSaverConfirmation="); + pw.println(mSaverConfirmation != null ? "not null" : null); pw.print("mSaverEnabledConfirmation="); pw.println(mSaverEnabledConfirmation != null ? "not null" : null); - pw.print("mHighTempWarning="); pw.println(mHighTempWarning); - pw.print("mHighTempDialog="); pw.println(mHighTempDialog != null ? "not null" : null); + pw.print("mHighTempWarning="); + pw.println(mHighTempWarning); + pw.print("mHighTempDialog="); + pw.println(mHighTempDialog != null ? "not null" : null); + pw.print("mOverheatAlarmDialog="); + pw.println(mOverheatAlarmDialog != null ? "not null" : null); pw.print("mThermalShutdownDialog="); pw.println(mThermalShutdownDialog != null ? "not null" : null); } @@ -370,6 +390,81 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { mNoMan.notifyAsUser(TAG_TEMPERATURE, SystemMessage.NOTE_HIGH_TEMP, n, UserHandle.ALL); } + /** + * PowerUI detect thermal overheat, notify to popup alarm dialog. + * Alarm with beep sound, showing overheat alarm dialog until user click OK or link of help. + * Do not auto dismiss even temperature drop. + * + * @param overheat true if device overheat, temperature >= threshold. + * false if device temperature <= threshold tolerance after overheat + * alarmed. + * @param shouldBeepSound true alarm beep sound until user interactive with device + * false showing alarm dialog only + */ + @Override + public void notifyHighTemperatureAlarm(boolean overheat, boolean shouldBeepSound) { + // Overheat and non-null dialog are XOR(exclusive or) relationship + if (overheat ^ (mOverheatAlarmDialog != null)) { + // b/120188825 Since notifyHighTemperatureAlarm() could be triggered by + // non-ui thread such as ThermalEventListener.notifyThrottling() + mHandler.post(() -> setOverheatAlarmDialogShowing(overheat)); + setAlarmShouldSound(shouldBeepSound); + } + } + + /** + * Showing overheat alarm dialog until user click OK button or link of help to dismiss + * + * @param shouldShow whether to show overheat alarm dialog. + */ + protected void setOverheatAlarmDialogShowing(boolean shouldShow) { + if (shouldShow && mOverheatAlarmDialog == null) { + OverheatAlarmDialog d = new OverheatAlarmDialog(mContext); + d.setCancelable(false); + d.setButton(BUTTON_NEGATIVE, + mContext.getString(R.string.high_temp_alarm_help_care_steps), + (dialogInterface, i) -> { + final String contextString = mContext.getString( + R.string.high_temp_alarm_help_url); + final Intent helpIntent = new Intent(); + helpIntent.setClassName("com.android.settings", + "com.android.settings.HelpTrampoline"); + helpIntent.putExtra(Intent.EXTRA_TEXT, contextString); + Dependency.get(ActivityStarter.class).startActivity(helpIntent, + true /* dismissShade */, resultCode -> { + mOverheatAlarmDialog = null; + }); + }); + d.setButton(BUTTON_POSITIVE, mContext.getString(com.android.internal.R.string.ok), + (dialogInterface, i) -> mOverheatAlarmDialog = null); + d.setOnDismissListener(dialogInterface -> { + mOverheatAlarmDialog = null; + Events.writeEvent(mContext, Events.EVENT_DISMISS_OVERHEAT_ALARM, + Events.DISMISS_REASON_DONE_CLICKED, + mKeyguard.isKeyguardLocked()); + }); + d.show(); + Events.writeEvent(mContext, Events.EVENT_SHOW_OVERHEAT_ALARM, + Events.SHOW_REASON_OVERHEAD_ALARM_CHANGED, + mKeyguard.isKeyguardLocked()); + mOverheatAlarmDialog = d; + } + } + + /** + * Whether to alarm beep sound when overheat dialog showing. + * + * @param shouldSound whether to alarm beep sound. + */ + protected void setAlarmShouldSound(boolean shouldSound) { + Log.d(TAG, "setAlarmShouldSound, " + shouldSound); + if (shouldSound) { + OverheatAlarmController.getInstance(mContext).startAlarm(mContext); + } else { + OverheatAlarmController.getInstance(mContext).stopAlarm(); + } + } + private void showHighTemperatureDialog() { if (mHighTempDialog != null) return; final SystemUIDialog d = new SystemUIDialog(mContext); @@ -643,6 +738,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { filter.addAction(ACTION_ENABLE_AUTO_SAVER); filter.addAction(ACTION_AUTO_SAVER_NO_THANKS); filter.addAction(ACTION_DISMISS_AUTO_SAVER_SUGGESTION); + filter.addAction(Intent.ACTION_ALARM_CHANGED); mContext.registerReceiverAsUser(this, UserHandle.ALL, filter, android.Manifest.permission.DEVICE_POWER, mHandler); } @@ -682,6 +778,8 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { } else if (ACTION_AUTO_SAVER_NO_THANKS.equals(action)) { dismissAutoSaverSuggestion(); BatterySaverUtils.suppressAutoBatterySaver(context); + } else if (Intent.ACTION_ALARM_CHANGED.equals(action)) { + setAlarmShouldSound(false /* mHasUserInteracted */); } } } diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index 0b9067e5dc1ff..80ebb6ee9acdc 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -54,12 +54,15 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.time.Duration; import java.util.Arrays; +import java.util.Locale; public class PowerUI extends SystemUI { static final String TAG = "PowerUI"; static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final long TEMPERATURE_INTERVAL = 30 * DateUtils.SECOND_IN_MILLIS; private static final long TEMPERATURE_LOGGING_INTERVAL = DateUtils.HOUR_IN_MILLIS; + private static final int TEMPERATURE_OVERHEAT_WARNING = 0; + private static final int TEMPERATURE_OVERHEAT_ALARM = 1; private static final int MAX_RECENT_TEMPS = 125; // TEMPERATURE_LOGGING_INTERVAL plus a buffer static final long THREE_HOURS_IN_MILLIS = DateUtils.HOUR_IN_MILLIS * 3; private static final int CHARGE_CYCLE_PERCENT_RESET = 45; @@ -80,15 +83,22 @@ public class PowerUI extends SystemUI { private Estimate mLastEstimate; private boolean mLowWarningShownThisChargeCycle; private boolean mSevereWarningShownThisChargeCycle; + private boolean mEnableTemperatureWarning; + private boolean mEnableTemperatureAlarm; + private boolean mIsOverheatAlarming; private int mLowBatteryAlertCloseLevel; private final int[] mLowBatteryReminderLevels = new int[2]; private long mScreenOffTime = -1; - private float mThresholdTemp; - private float[] mRecentTemps = new float[MAX_RECENT_TEMPS]; - private int mNumTemps; + private float mThresholdWarningTemp; + private float mThresholdAlarmTemp; + private float mThresholdAlarmTempTolerance; + private float[] mRecentSkinTemps = new float[MAX_RECENT_TEMPS]; + private float[] mRecentAlarmTemps = new float[MAX_RECENT_TEMPS]; + private int mWarningNumTemps; + private int mAlarmNumTemps; private long mNextLogTime; private IThermalService mThermalService; @@ -98,7 +108,7 @@ public class PowerUI extends SystemUI { // by using the same instance (method references are not guaranteed to be the same object // We create a method reference here so that we are guaranteed that we can remove a callback // each time they are created). - private final Runnable mUpdateTempCallback = this::updateTemperatureWarning; + private final Runnable mUpdateTempCallback = this::updateTemperature; public void start() { mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); @@ -126,7 +136,7 @@ public class PowerUI extends SystemUI { // to the temperature being too high. showThermalShutdownDialog(); - initTemperatureWarning(); + initTemperature(); } @Override @@ -135,7 +145,7 @@ public class PowerUI extends SystemUI { // Safe to modify mLastConfiguration here as it's only updated by the main thread (here). if ((mLastConfiguration.updateFrom(newConfig) & mask) != 0) { - mHandler.post(this::initTemperatureWarning); + mHandler.post(this::initTemperature); } } @@ -193,6 +203,7 @@ public class PowerUI extends SystemUI { filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_SCREEN_ON); filter.addAction(Intent.ACTION_USER_SWITCHED); + filter.addAction(Intent.ACTION_POWER_DISCONNECTED); mContext.registerReceiver(this, filter, null, mHandler); } @@ -258,6 +269,8 @@ public class PowerUI extends SystemUI { mScreenOffTime = -1; } else if (Intent.ACTION_USER_SWITCHED.equals(action)) { mWarnings.userSwitched(); + } else if (Intent.ACTION_POWER_DISCONNECTED.equals(action)) { + updateTemperature(); } else { Slog.w(TAG, "unknown intent: " + intent); } @@ -364,18 +377,51 @@ public class PowerUI extends SystemUI { return canShowWarning || canShowSevereWarning; } - private void initTemperatureWarning() { - ContentResolver resolver = mContext.getContentResolver(); - Resources resources = mContext.getResources(); - if (Settings.Global.getInt(resolver, Settings.Global.SHOW_TEMPERATURE_WARNING, - resources.getInteger(R.integer.config_showTemperatureWarning)) == 0) { + private void initTemperature() { + initTemperatureWarning(); + initTemperatureAlarm(); + if (mEnableTemperatureWarning || mEnableTemperatureAlarm) { + bindThermalService(); + } + } + + private void initTemperatureAlarm() { + mEnableTemperatureAlarm = mContext.getResources().getInteger( + R.integer.config_showTemperatureAlarm) != 0; + if (!mEnableTemperatureAlarm) { return; } - mThresholdTemp = Settings.Global.getFloat(resolver, Settings.Global.WARNING_TEMPERATURE, + float[] throttlingTemps = mHardwarePropertiesManager.getDeviceTemperatures( + HardwarePropertiesManager.DEVICE_TEMPERATURE_SKIN, + HardwarePropertiesManager.TEMPERATURE_THROTTLING); + if (throttlingTemps == null || throttlingTemps.length < TEMPERATURE_OVERHEAT_ALARM + 1) { + mThresholdAlarmTemp = mContext.getResources().getInteger( + R.integer.config_alarmTemperature); + } else { + mThresholdAlarmTemp = throttlingTemps[TEMPERATURE_OVERHEAT_ALARM]; + } + mThresholdAlarmTempTolerance = mThresholdAlarmTemp - mContext.getResources().getInteger( + R.integer.config_alarmTemperatureTolerance); + Log.d(TAG, "mThresholdAlarmTemp=" + mThresholdAlarmTemp + ", mThresholdAlarmTempTolerance=" + + mThresholdAlarmTempTolerance); + } + + private void initTemperatureWarning() { + ContentResolver resolver = mContext.getContentResolver(); + Resources resources = mContext.getResources(); + mEnableTemperatureWarning = Settings.Global.getInt(resolver, + Settings.Global.SHOW_TEMPERATURE_WARNING, + resources.getInteger(R.integer.config_showTemperatureWarning)) != 0; + if (!mEnableTemperatureWarning) { + return; + } + + mThresholdWarningTemp = Settings.Global.getFloat(resolver, + Settings.Global.WARNING_TEMPERATURE, resources.getInteger(R.integer.config_warningTemperature)); - if (mThresholdTemp < 0f) { + if (mThresholdWarningTemp < 0f) { // Get the shutdown temperature, adjust for warning tolerance. float[] throttlingTemps = mHardwarePropertiesManager.getDeviceTemperatures( HardwarePropertiesManager.DEVICE_TEMPERATURE_SKIN, @@ -385,10 +431,12 @@ public class PowerUI extends SystemUI { || throttlingTemps[0] == HardwarePropertiesManager.UNDEFINED_TEMPERATURE) { return; } - mThresholdTemp = throttlingTemps[0] - - resources.getInteger(R.integer.config_warningTemperatureTolerance); + mThresholdWarningTemp = throttlingTemps[0] - resources.getInteger( + R.integer.config_warningTemperatureTolerance); } + } + private void bindThermalService() { if (mThermalService == null) { // Enable push notifications of throttling from vendor thermal // management subsystem via thermalservice, in addition to our @@ -416,7 +464,7 @@ public class PowerUI extends SystemUI { mHandler.removeCallbacks(mUpdateTempCallback); // We have passed all of the checks, start checking the temp - updateTemperatureWarning(); + updateTemperature(); } private void showThermalShutdownDialog() { @@ -426,38 +474,110 @@ public class PowerUI extends SystemUI { } } + /** + * Update temperature depend on config, there are type types of messages by design + * TEMPERATURE_OVERHEAT_WARNING Send generic notification to notify user + * TEMPERATURE_OVERHEAT_ALARM popup emergency Dialog for user + */ @VisibleForTesting - protected void updateTemperatureWarning() { + protected void updateTemperature() { + if (!mEnableTemperatureWarning && !mEnableTemperatureAlarm) { + return; + } + float[] temps = mHardwarePropertiesManager.getDeviceTemperatures( HardwarePropertiesManager.DEVICE_TEMPERATURE_SKIN, HardwarePropertiesManager.TEMPERATURE_CURRENT); + if (temps == null) { + Log.e(TAG, "Can't query current temperature value from HardProps SKIN type"); + return; + } + + final float[] temps_compat; + if (temps.length < TEMPERATURE_OVERHEAT_ALARM + 1) { + temps_compat = new float[] { temps[0], temps[0] }; + } else { + temps_compat = temps; + } + + if (mEnableTemperatureWarning) { + updateTemperatureWarning(temps_compat); + logTemperatureStats(TEMPERATURE_OVERHEAT_WARNING); + } + + if (mEnableTemperatureAlarm) { + updateTemperatureAlarm(temps_compat); + logTemperatureStats(TEMPERATURE_OVERHEAT_ALARM); + } + + mHandler.postDelayed(mUpdateTempCallback, TEMPERATURE_INTERVAL); + } + + /** + * Update legacy overheat warning notification from skin temperatures + * + * @param temps the array include two types of value from DEVICE_TEMPERATURE_SKIN + * this function only obtain the value of temps[TEMPERATURE_OVERHEAT_WARNING] + */ + @VisibleForTesting + protected void updateTemperatureWarning(float[] temps) { if (temps.length != 0) { - float temp = temps[0]; - mRecentTemps[mNumTemps++] = temp; + float temp = temps[TEMPERATURE_OVERHEAT_WARNING]; + mRecentSkinTemps[mWarningNumTemps++] = temp; StatusBar statusBar = getComponent(StatusBar.class); if (statusBar != null && !statusBar.isDeviceInVrMode() - && temp >= mThresholdTemp) { - logAtTemperatureThreshold(temp); + && temp >= mThresholdWarningTemp) { + logAtTemperatureThreshold(TEMPERATURE_OVERHEAT_WARNING, temp, + mThresholdWarningTemp); mWarnings.showHighTemperatureWarning(); } else { mWarnings.dismissHighTemperatureWarning(); } } - - logTemperatureStats(); - - mHandler.postDelayed(mUpdateTempCallback, TEMPERATURE_INTERVAL); } - private void logAtTemperatureThreshold(float temp) { + /** + * Update overheat alarm from skin temperatures + * OEM can config alarm with beep sound by config_alarmTemperatureBeepSound + * + * @param temps the array include two types of value from DEVICE_TEMPERATURE_SKIN + * this function only obtain the value of temps[TEMPERATURE_OVERHEAT_ALARM] + */ + @VisibleForTesting + protected void updateTemperatureAlarm(float[] temps) { + if (temps.length != 0) { + final float temp = temps[TEMPERATURE_OVERHEAT_ALARM]; + final boolean shouldBeepSound = mContext.getResources().getBoolean( + R.bool.config_alarmTemperatureBeepSound); + mRecentAlarmTemps[mAlarmNumTemps++] = temp; + if (temp >= mThresholdAlarmTemp && !mIsOverheatAlarming) { + mWarnings.notifyHighTemperatureAlarm(true /* overheat */, shouldBeepSound); + mIsOverheatAlarming = true; + } else if (temp <= mThresholdAlarmTempTolerance && mIsOverheatAlarming) { + mWarnings.notifyHighTemperatureAlarm(false /* overheat */, false /* beepSound */); + mIsOverheatAlarming = false; + } + logAtTemperatureThreshold(TEMPERATURE_OVERHEAT_ALARM, temp, mThresholdAlarmTemp); + } + } + + private void logAtTemperatureThreshold(int type, float temp, float thresholdTemp) { StringBuilder sb = new StringBuilder(); sb.append("currentTemp=").append(temp) - .append(",thresholdTemp=").append(mThresholdTemp) + .append(",overheatType=").append(type) + .append(",isOverheatAlarm=").append(mIsOverheatAlarming) + .append(",thresholdTemp=").append(thresholdTemp) .append(",batteryStatus=").append(mBatteryStatus) .append(",recentTemps="); - for (int i = 0; i < mNumTemps; i++) { - sb.append(mRecentTemps[i]).append(','); + if (type == TEMPERATURE_OVERHEAT_WARNING) { + for (int i = 0; i < mWarningNumTemps; i++) { + sb.append(mRecentSkinTemps[i]).append(','); + } + } else { + for (int i = 0; i < mAlarmNumTemps; i++) { + sb.append(mRecentAlarmTemps[i]).append(','); + } } Slog.i(TAG, sb.toString()); } @@ -466,16 +586,20 @@ public class PowerUI extends SystemUI { * Calculates and logs min, max, and average * {@link HardwarePropertiesManager#DEVICE_TEMPERATURE_SKIN} over the past * {@link #TEMPERATURE_LOGGING_INTERVAL}. + * @param type TEMPERATURE_OVERHEAT_WARNING Send generic notification to notify user + * TEMPERATURE_OVERHEAT_ALARM Popup emergency Dialog for user */ - private void logTemperatureStats() { - if (mNextLogTime > System.currentTimeMillis() && mNumTemps != MAX_RECENT_TEMPS) { + private void logTemperatureStats(int type) { + int numTemp = type == TEMPERATURE_OVERHEAT_ALARM ? mAlarmNumTemps : mWarningNumTemps; + if (mNextLogTime > System.currentTimeMillis() && numTemp != MAX_RECENT_TEMPS) { return; } - - if (mNumTemps > 0) { - float sum = mRecentTemps[0], min = mRecentTemps[0], max = mRecentTemps[0]; - for (int i = 1; i < mNumTemps; i++) { - float temp = mRecentTemps[i]; + float[] recentTemps = + type == TEMPERATURE_OVERHEAT_ALARM ? mRecentAlarmTemps : mRecentSkinTemps; + if (numTemp > 0) { + float sum = recentTemps[0], min = recentTemps[0], max = recentTemps[0]; + for (int i = 1; i < numTemp; i++) { + float temp = recentTemps[i]; sum += temp; if (temp > max) { max = temp; @@ -485,14 +609,22 @@ public class PowerUI extends SystemUI { } } - float avg = sum / mNumTemps; - Slog.i(TAG, "avg=" + avg + ",min=" + min + ",max=" + max); - MetricsLogger.histogram(mContext, "device_skin_temp_avg", (int) avg); - MetricsLogger.histogram(mContext, "device_skin_temp_min", (int) min); - MetricsLogger.histogram(mContext, "device_skin_temp_max", (int) max); + float avg = sum / numTemp; + Slog.i(TAG, "Type=" + type + ",avg=" + avg + ",min=" + min + ",max=" + max); + String t = type == TEMPERATURE_OVERHEAT_WARNING ? "skin" : "alarm"; + MetricsLogger.histogram(mContext, + String.format(Locale.ENGLISH, "device_%1$s_temp_avg", t), (int) avg); + MetricsLogger.histogram(mContext, + String.format(Locale.ENGLISH, "device_%1$s_temp_min", t), (int) min); + MetricsLogger.histogram(mContext, + String.format(Locale.ENGLISH, "device_%1$s_temp_max", t), (int) max); } setNextLogTime(); - mNumTemps = 0; + if (type == TEMPERATURE_OVERHEAT_ALARM) { + mAlarmNumTemps = 0; + } else { + mWarningNumTemps = 0; + } } private void setNextLogTime() { @@ -525,8 +657,10 @@ public class PowerUI extends SystemUI { Settings.Global.LOW_BATTERY_SOUND_TIMEOUT, 0)); pw.print("bucket: "); pw.println(Integer.toString(findBatteryLevelBucket(mBatteryLevel))); - pw.print("mThresholdTemp="); - pw.println(Float.toString(mThresholdTemp)); + pw.print("mThresholdWarningTemp="); + pw.println(Float.toString(mThresholdWarningTemp)); + pw.print("mThresholdAlarmTemp="); + pw.println(Float.toString(mThresholdAlarmTemp)); pw.print("mNextLogTime="); pw.println(Long.toString(mNextLogTime)); mWarnings.dump(pw); @@ -534,18 +668,41 @@ public class PowerUI extends SystemUI { public interface WarningsUI { void update(int batteryLevel, int bucket, long screenOffTime); + void updateEstimate(Estimate estimate); + void updateThresholds(long lowThreshold, long severeThreshold); + void dismissLowBatteryWarning(); + void showLowBatteryWarning(boolean playSound); + void dismissInvalidChargerWarning(); + void showInvalidChargerWarning(); + void updateLowBatteryWarning(); + boolean isInvalidChargerWarningShowing(); + void dismissHighTemperatureWarning(); + void showHighTemperatureWarning(); + + /** + * PowerUI detect thermal overheat, notify to popup alert dialog strongly. + * Alarm with beep sound with un-dismissible dialog until device cool down below threshold, + * then popup another dismissible dialog to user. + * + * @param overheat whether device temperature over threshold + * @param beepSound should beep sound once overheat + */ + void notifyHighTemperatureAlarm(boolean overheat, boolean beepSound); + void showThermalShutdownWarning(); + void dump(PrintWriter pw); + void userSwitched(); } @@ -554,9 +711,9 @@ public class PowerUI extends SystemUI { @Override public void notifyThrottling(boolean isThrottling, Temperature temp) { // Trigger an update of the temperature warning. Only one // callback can be enabled at a time, so remove any existing - // callback; updateTemperatureWarning will schedule another one. + // callback; updateTemperature will schedule another one. mHandler.removeCallbacks(mUpdateTempCallback); - updateTemperatureWarning(); + updateTemperature(); } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/Events.java b/packages/SystemUI/src/com/android/systemui/volume/Events.java index ca55e1f23d206..96e8310a1c625 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/Events.java +++ b/packages/SystemUI/src/com/android/systemui/volume/Events.java @@ -53,6 +53,8 @@ public class Events { public static final int EVENT_TOUCH_LEVEL_DONE = 16; // (stream|int) (level|bool) public static final int EVENT_ZEN_CONFIG_CHANGED = 17; // (allow/disallow|string) public static final int EVENT_RINGER_TOGGLE = 18; // (ringer_mode) + public static final int EVENT_SHOW_OVERHEAT_ALARM = 19; // (reason|int) (keyguard|bool) + public static final int EVENT_DISMISS_OVERHEAT_ALARM = 20; // (reason|int) (keyguard|bool) private static final String[] EVENT_TAGS = { "show_dialog", @@ -73,7 +75,9 @@ public class Events { "mute_changed", "touch_level_done", "zen_mode_config_changed", - "ringer_toggle" + "ringer_toggle", + "show_overheat_alarm", + "dismiss_overheat_alarm" }; public static final int DISMISS_REASON_UNKNOWN = 0; @@ -100,10 +104,12 @@ public class Events { public static final int SHOW_REASON_UNKNOWN = 0; public static final int SHOW_REASON_VOLUME_CHANGED = 1; public static final int SHOW_REASON_REMOTE_VOLUME_CHANGED = 2; + public static final int SHOW_REASON_OVERHEAD_ALARM_CHANGED = 3; public static final String[] SHOW_REASONS = { "unknown", "volume_changed", - "remote_volume_changed" + "remote_volume_changed", + "overheat_alarm_changed" }; public static final int ICON_STATE_UNKNOWN = 0; @@ -181,6 +187,19 @@ public class Events { case EVENT_SUPPRESSOR_CHANGED: sb.append(list[0]).append(' ').append(list[1]); break; + case EVENT_SHOW_OVERHEAT_ALARM: + MetricsLogger.visible(context, MetricsEvent.POWER_OVERHEAT_ALARM); + MetricsLogger.histogram(context, "show_overheat_alarm", + (Boolean) list[1] ? 1 : 0); + sb.append(SHOW_REASONS[(Integer) list[0]]).append(" keyguard=").append(list[1]); + break; + case EVENT_DISMISS_OVERHEAT_ALARM: + MetricsLogger.hidden(context, MetricsEvent.POWER_OVERHEAT_ALARM); + MetricsLogger.histogram(context, "dismiss_overheat_alarm", + (Boolean) list[1] ? 1 : 0); + sb.append(DISMISS_REASONS[(Integer) list[0]]).append(" keyguard=").append( + list[1]); + break; default: sb.append(Arrays.asList(list)); break; diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index 1be83229cf22a..c40d72d483a00 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -49,6 +49,7 @@ + diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/OverheatAlarmControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/OverheatAlarmControllerTest.java new file mode 100644 index 0000000000000..651d2b7af7db3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/power/OverheatAlarmControllerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.power; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper.RunWithLooper; + +import com.android.systemui.SysuiTestCase; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +@SmallTest +public class OverheatAlarmControllerTest extends SysuiTestCase{ + OverheatAlarmController mOverheatAlarmController; + OverheatAlarmController mSpyOverheatAlarmController; + + @Before + public void setUp() { + mOverheatAlarmController = OverheatAlarmController.getInstance(mContext); + mSpyOverheatAlarmController = spy(mOverheatAlarmController); + } + + @After + public void tearDown() { + mSpyOverheatAlarmController.stopAlarm(); + } + + @Test + public void testGetInstance() { + assertThat(mOverheatAlarmController).isNotNull(); + } + + @Test + public void testStartAlarm_PlaySound() { + mSpyOverheatAlarmController.startAlarm(mContext); + verify(mSpyOverheatAlarmController).playSound(mContext); + } + + @Test + public void testStartAlarm_InitVibrate() { + mSpyOverheatAlarmController.startAlarm(mContext); + verify(mSpyOverheatAlarmController).startVibrate(); + } + + @Test + public void testStartAlarm_StartVibrate() { + mSpyOverheatAlarmController.startAlarm(mContext); + verify(mSpyOverheatAlarmController).performVibrate(); + } + + @Test + public void testStopAlarm_StopPlayer() { + mSpyOverheatAlarmController.stopAlarm(); + verify(mSpyOverheatAlarmController).stopPlayer(); + } + + @Test + public void testStopAlarm_StopVibrate() { + mSpyOverheatAlarmController.stopAlarm(); + verify(mSpyOverheatAlarmController).stopVibrate(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/OverheatAlarmDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/OverheatAlarmDialogTest.java new file mode 100644 index 0000000000000..04d3163f05742 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/power/OverheatAlarmDialogTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.power; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.os.SystemClock; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper.RunWithLooper; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.WindowManager; + +import com.android.systemui.SysuiTestCase; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + + +@RunWith(AndroidTestingRunner.class) +@RunWithLooper(setAsMainLooper = true) +@SmallTest +public class OverheatAlarmDialogTest extends SysuiTestCase { + + private OverheatAlarmDialog mDialog, mSpyDialog; + + @Before + public void setup() { + mDialog = new OverheatAlarmDialog(mContext); + mSpyDialog = spy(mDialog); + } + + @After + public void tearDown() { + mSpyDialog = mDialog = null; + } + + @Test + public void testFlagShowForAllUsers() { + assertThat((mDialog.getWindow().getAttributes().privateFlags + & WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS) != 0).isTrue(); + } + + @Test + public void testFlagShowWhenLocked() { + assertThat((mDialog.getWindow().getAttributes().flags + & WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) != 0).isTrue(); + } + + @Test + public void testFlagTurnScreenOn() { + assertThat((mDialog.getWindow().getAttributes().flags + & WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) != 0).isTrue(); + } + + @Test + public void testFlagKeepScreenOn() { + assertThat((mDialog.getWindow().getAttributes().flags + & WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0).isTrue(); + } + + @Test + public void testTouchOutsideDialog_NotifyAlarmBeepSoundIntent_ShouldStopBeepSound() { + final long currentTime = SystemClock.uptimeMillis(); + mSpyDialog.show(); + MotionEvent ev = createMotionEvent(MotionEvent.ACTION_DOWN, currentTime, 0, 0); + mSpyDialog.dispatchTouchEvent(ev); + + verify(mSpyDialog, atLeastOnce()).notifyAlarmBeepSoundChange(); + mSpyDialog.dismiss(); + } + + @Test + public void testPressBackKey_NotifyAlarmBeepSoundIntent_ShouldStopBeepSound() { + mSpyDialog.show(); + KeyEvent ev = new KeyEvent(0, 0, MotionEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK, 0, + 0); + mSpyDialog.dispatchKeyEvent(ev); + + verify(mSpyDialog, atLeastOnce()).notifyAlarmBeepSoundChange(); + mSpyDialog.dismiss(); + } + + @Test + public void testPressVolumeUp_NotifyAlarmBeepSoundIntent_ShouldStopBeepSound() { + mSpyDialog.show(); + KeyEvent ev = new KeyEvent(0, 0, MotionEvent.ACTION_DOWN, + KeyEvent.KEYCODE_VOLUME_UP, 0, 0); + mSpyDialog.dispatchKeyEvent(ev); + + verify(mSpyDialog, atLeastOnce()).notifyAlarmBeepSoundChange(); + mSpyDialog.dismiss(); + } + + @Test + public void testPressVolumeDown_NotifyAlarmBeepSoundIntent_ShouldStopBeepSound() { + mSpyDialog.show(); + KeyEvent ev = new KeyEvent(0, 0, MotionEvent.ACTION_DOWN, + KeyEvent.KEYCODE_VOLUME_DOWN, 0, 0); + mSpyDialog.dispatchKeyEvent(ev); + + verify(mSpyDialog, atLeastOnce()).notifyAlarmBeepSoundChange(); + mSpyDialog.dismiss(); + } + + private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) { + return MotionEvent.obtain(0, eventTime, action, x, y, 0); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java index bf6cc53aa5e90..d37c2852bc1f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java @@ -16,9 +16,8 @@ package com.android.systemui.power; -import static android.test.MoreAsserts.assertNotEqual; +import static com.google.common.truth.Truth.assertThat; -import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; @@ -26,6 +25,8 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -38,7 +39,7 @@ import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.systemui.SysuiTestCase; import com.android.systemui.util.NotificationChannels; -import java.util.concurrent.TimeUnit; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,13 +52,22 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { public static final String FORMATTED_45M = "0h 45m"; public static final String FORMATTED_HOUR = "1h 0m"; private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); - private PowerNotificationWarnings mPowerNotificationWarnings; + private PowerNotificationWarnings mPowerNotificationWarnings, mSpyPowerNotificationWarnings; @Before public void setUp() throws Exception { // Test Instance. mContext.addMockSystemService(NotificationManager.class, mMockNotificationManager); mPowerNotificationWarnings = new PowerNotificationWarnings(mContext); + mSpyPowerNotificationWarnings = spy(mPowerNotificationWarnings); + } + + @After + public void tearDown() throws Exception { + if (mSpyPowerNotificationWarnings.mOverheatAlarmDialog != null) { + mSpyPowerNotificationWarnings.mOverheatAlarmDialog.dismiss(); + mSpyPowerNotificationWarnings.mOverheatAlarmDialog = null; + } } @Test @@ -151,4 +161,146 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { verify(mMockNotificationManager, times(1)).cancelAsUser(anyString(), eq(SystemMessage.NOTE_THERMAL_SHUTDOWN), any()); } + + @Test + public void testSetOverheatAlarmDialog_Overheat_ShouldShowing() { + final boolean overheat = true; + final boolean shouldBeepSound = false; + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + shouldBeepSound)); + waitForIdleSync(); + verify(mSpyPowerNotificationWarnings, times(1)).setOverheatAlarmDialogShowing(overheat); + verify(mSpyPowerNotificationWarnings, times(1)).setAlarmShouldSound(shouldBeepSound); + } + + @Test + public void testSetOverheatAlarmDialog_Overheat_ShouldShowingWithBeepSound() { + final boolean overheat = true; + final boolean shouldBeepSound = true; + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + shouldBeepSound)); + waitForIdleSync(); + verify(mSpyPowerNotificationWarnings, times(1)).setOverheatAlarmDialogShowing(overheat); + verify(mSpyPowerNotificationWarnings, times(1)).setAlarmShouldSound(shouldBeepSound); + } + + @Test + public void testSetOverheatAlarmDialog_NotOverheat_ShouldNotShowing() { + final boolean overheat = false; + final boolean shouldBeepSound = false; + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + shouldBeepSound)); + waitForIdleSync(); + verify(mSpyPowerNotificationWarnings, never()).setOverheatAlarmDialogShowing(overheat); + verify(mSpyPowerNotificationWarnings, never()).setAlarmShouldSound(shouldBeepSound); + } + + @Test + public void testSetOverheatAlarmDialog_NotOverheat_ShouldNotAlarmBeepSound() { + final boolean overheat = false; + final boolean configBeepSound = true; + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + configBeepSound)); + waitForIdleSync(); + verify(mSpyPowerNotificationWarnings, never()).setOverheatAlarmDialogShowing(overheat); + verify(mSpyPowerNotificationWarnings, never()).setAlarmShouldSound(configBeepSound); + } + + @Test + public void testSetAlarmShouldSound_OverheatDrop_ShouldNotSound() { + final boolean overheat = true; + final boolean shouldBeepSound = true; + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + shouldBeepSound)); + waitForIdleSync(); + // First time overheat, show overheat alarm dialog with alarm beep sound + verify(mSpyPowerNotificationWarnings, times(1)).setOverheatAlarmDialogShowing(overheat); + verify(mSpyPowerNotificationWarnings, times(1)).setAlarmShouldSound(shouldBeepSound); + + // After disconnected cable or temperature drop + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(!overheat, + !shouldBeepSound)); + waitForIdleSync(); + verify(mSpyPowerNotificationWarnings, times(1)).setOverheatAlarmDialogShowing(!overheat); + verify(mSpyPowerNotificationWarnings, times(1)).setAlarmShouldSound(!shouldBeepSound); + } + + @Test + public void testSetAlarmShouldSound_Overheat_Twice_ShouldShowOverheatDialogAgain() { + final boolean overheat = true; + final boolean shouldBeepSound = true; + // First time overheat, show mAlarmDialog and alarm beep sound + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + shouldBeepSound)); + waitForIdleSync(); + verify(mSpyPowerNotificationWarnings, times(1)).setOverheatAlarmDialogShowing(overheat); + verify(mSpyPowerNotificationWarnings, times(1)).setAlarmShouldSound(shouldBeepSound); + + // After disconnected cable or temperature drop, stop beep sound + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(!overheat, + !shouldBeepSound)); + waitForIdleSync(); + verify(mSpyPowerNotificationWarnings, times(1)).setOverheatAlarmDialogShowing(!overheat); + verify(mSpyPowerNotificationWarnings, times(1)).setAlarmShouldSound(!shouldBeepSound); + + // Overheat again, ensure the previous dialog do not auto-dismiss + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + shouldBeepSound)); + waitForIdleSync(); + verify(mSpyPowerNotificationWarnings, times(1)).setOverheatAlarmDialogShowing(overheat); + verify(mSpyPowerNotificationWarnings, times(1)).setAlarmShouldSound(shouldBeepSound); + } + + @Test + public void testOverheatAlarmDialogShowing() { + final boolean overheat = true; + final boolean shouldBeepSound = false; + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + shouldBeepSound)); + waitForIdleSync(); + assertThat(mSpyPowerNotificationWarnings.mOverheatAlarmDialog).isNotNull(); + } + + @Test + public void testOverheatAlarmDialogShowingWithBeepSound() { + final boolean overheat = true; + final boolean shouldBeepSound = true; + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + shouldBeepSound)); + waitForIdleSync(); + assertThat(mSpyPowerNotificationWarnings.mOverheatAlarmDialog).isNotNull(); + } + + @Test + public void testOverheatAlarmDialogNotShowing() { + final boolean overheat = false; + final boolean shouldBeepSound = false; + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + shouldBeepSound)); + waitForIdleSync(); + assertThat(mSpyPowerNotificationWarnings.mOverheatAlarmDialog).isNull(); + } + + @Test + public void testOverheatAlarmDialogNotShowingWithBeepSound() { + final boolean overheat = false; + final boolean shouldBeepSound = true; + mContext.getMainThreadHandler().post( + () -> mSpyPowerNotificationWarnings.notifyHighTemperatureAlarm(overheat, + shouldBeepSound)); + waitForIdleSync(); + assertThat(mSpyPowerNotificationWarnings.mOverheatAlarmDialog).isNull(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java index a9d49f91e44ea..054bdee9fd97d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java @@ -17,6 +17,7 @@ package com.android.systemui.power; import static android.os.HardwarePropertiesManager.DEVICE_TEMPERATURE_SKIN; import static android.os.HardwarePropertiesManager.TEMPERATURE_CURRENT; import static android.os.HardwarePropertiesManager.TEMPERATURE_SHUTDOWN; +import static android.os.HardwarePropertiesManager.TEMPERATURE_THROTTLING; import static android.provider.Settings.Global.SHOW_TEMPERATURE_WARNING; import static junit.framework.Assert.assertFalse; @@ -69,6 +70,7 @@ public class PowerUITest extends SysuiTestCase { private static final long ABOVE_CHARGE_CYCLE_THRESHOLD = Duration.ofHours(8).toMillis(); private static final int OLD_BATTERY_LEVEL_NINE = 9; private static final int OLD_BATTERY_LEVEL_10 = 10; + private static final int DEFAULT_OVERHEAT_ALARM_THRESHOLD = 58; private HardwarePropertiesManager mHardProps; private WarningsUI mMockWarnings; private PowerUI mPowerUI; @@ -86,6 +88,7 @@ public class PowerUITest extends SysuiTestCase { mContext.addMockSystemService(Context.HARDWARE_PROPERTIES_SERVICE, mHardProps); mContext.addMockSystemService(Context.POWER_SERVICE, mPowerManager); + setUnderThreshold(); createPowerUi(); } @@ -153,10 +156,94 @@ public class PowerUITest extends SysuiTestCase { verify(mMockWarnings, never()).showHighTemperatureWarning(); setCurrentTemp(56); // Above threshold. - mPowerUI.updateTemperatureWarning(); + mPowerUI.updateTemperature(); verify(mMockWarnings).showHighTemperatureWarning(); } + @Test + public void testNoConfig_noAlarms() { + setOverThreshold(); + final Boolean overheat = false; + final Boolean shouldBeepSound = false; + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride(R.integer.config_showTemperatureWarning, 0); + resources.addOverride(R.integer.config_alarmTemperature, 55); + resources.addOverride(R.bool.config_alarmTemperatureBeepSound, shouldBeepSound); + + mPowerUI.start(); + verify(mMockWarnings, never()).notifyHighTemperatureAlarm(overheat, shouldBeepSound); + } + + @Test + public void testConfig_noAlarms() { + setUnderThreshold(); + final Boolean overheat = false; + final Boolean shouldBeepSound = false; + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride(R.integer.config_showTemperatureAlarm, 1); + resources.addOverride(R.integer.config_alarmTemperature, 58); + resources.addOverride(R.bool.config_alarmTemperatureBeepSound, shouldBeepSound); + + mPowerUI.start(); + verify(mMockWarnings, never()).notifyHighTemperatureAlarm(overheat, shouldBeepSound); + } + + @Test + public void testConfig_alarms() { + setOverThreshold(); + final Boolean overheat = true; + final Boolean shouldBeepSound = false; + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride(R.integer.config_showTemperatureAlarm, 1); + resources.addOverride(R.integer.config_alarmTemperature, 58); + resources.addOverride(R.bool.config_alarmTemperatureBeepSound, shouldBeepSound); + + mPowerUI.start(); + verify(mMockWarnings).notifyHighTemperatureAlarm(overheat, shouldBeepSound); + } + + @Test + public void testConfig_alarmsWithBeepSound() { + setOverThreshold(); + final Boolean overheat = true; + final Boolean shouldBeepSound = true; + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride(R.integer.config_showTemperatureAlarm, 1); + resources.addOverride(R.integer.config_alarmTemperature, 58); + resources.addOverride(R.bool.config_alarmTemperatureBeepSound, shouldBeepSound); + + mPowerUI.start(); + verify(mMockWarnings).notifyHighTemperatureAlarm(overheat, shouldBeepSound); + } + + @Test + public void testHardPropsThrottlingThreshold_alarms() { + setThrottlingThreshold(DEFAULT_OVERHEAT_ALARM_THRESHOLD); + setOverThreshold(); + final Boolean overheat = true; + final Boolean shouldBeepSound = false; + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride(R.integer.config_showTemperatureAlarm, 1); + resources.addOverride(R.bool.config_alarmTemperatureBeepSound, shouldBeepSound); + + mPowerUI.start(); + verify(mMockWarnings).notifyHighTemperatureAlarm(overheat, shouldBeepSound); + } + + @Test + public void testHardPropsThrottlingThreshold_noAlarms() { + setThrottlingThreshold(DEFAULT_OVERHEAT_ALARM_THRESHOLD); + setUnderThreshold(); + final Boolean overheat = false; + final Boolean shouldBeepSound = false; + TestableResources resources = mContext.getOrCreateTestableResources(); + resources.addOverride(R.integer.config_showTemperatureAlarm, 1); + resources.addOverride(R.bool.config_alarmTemperatureBeepSound, shouldBeepSound); + + mPowerUI.start(); + verify(mMockWarnings, never()).notifyHighTemperatureAlarm(overheat, shouldBeepSound); + } + @Test public void testShouldShowLowBatteryWarning_showHybridOnly_overrideThresholdHigh_returnsNoShow() { when(mEnhancedEstimates.isHybridNotificationEnabled()).thenReturn(true); @@ -495,7 +582,12 @@ public class PowerUITest extends SysuiTestCase { private void setCurrentTemp(float temp) { when(mHardProps.getDeviceTemperatures(DEVICE_TEMPERATURE_SKIN, TEMPERATURE_CURRENT)) - .thenReturn(new float[] { temp }); + .thenReturn(new float[] { temp, temp }); + } + + private void setThrottlingThreshold(float temp) { + when(mHardProps.getDeviceTemperatures(DEVICE_TEMPERATURE_SKIN, TEMPERATURE_THROTTLING)) + .thenReturn(new float[] { temp, temp }); } private void setOverThreshold() { diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto index d79d833f22336..4ebf9aa005d89 100644 --- a/proto/src/metrics_constants.proto +++ b/proto/src/metrics_constants.proto @@ -6117,6 +6117,11 @@ message MetricsEvent { // OS: P FIELD_AUTOFILL_SESSION_ID = 1456; + // FIELD: Device USB overheat alarm trigger. + // CATEGORY: GLOBAL_SYSTEM_UI + // OS: P + POWER_OVERHEAT_ALARM = 1457; + // NOTIFICATION_SINCE_INTERRUPTION_MILLIS added to P // NOTIFICATION_INTERRUPTION added to P