diff options
17 files changed, 1251 insertions, 57 deletions
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 000000000000..d78272fe6abc --- /dev/null +++ b/packages/SystemUI/res/layout/overheat_dialog_content.xml @@ -0,0 +1,39 @@ +<!-- + 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. +--> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/contentPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp"> + + <ScrollView + android:id="@+id/scrollView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="@dimen/alarm_dialog_panel_padding" + android:paddingEnd="?android:attr/dialogPreferredPadding" + android:paddingStart="?android:attr/dialogPreferredPadding" + android:clipToPadding="false"> + + <TextView + android:id="@android:id/message" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/high_temp_alarm_notify_message" + style="@android:style/TextAppearance.Material.Subhead"/> + </ScrollView> +</FrameLayout> 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 000000000000..65a512ae085b --- /dev/null +++ b/packages/SystemUI/res/layout/overheat_dialog_title.xml @@ -0,0 +1,46 @@ +<!-- + 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. +--> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/title_template" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical|start" + android:paddingStart="?android:attr/dialogPreferredPadding" + android:paddingEnd="?android:attr/dialogPreferredPadding" + android:paddingTop="@dimen/alarm_dialog_panel_padding"> + + <ImageView + android:id="@android:id/icon" + android:layout_width="32dip" + android:layout_height="32dip" + android:layout_marginEnd="8dip" + android:scaleType="fitCenter" + android:tint="?android:attr/colorError" + android:src="?android:attr/alertDialogIcon"/> + + <com.android.internal.widget.DialogTitle + android:id="@+id/alertTitle" + android:singleLine="true" + android:ellipsize="end" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:text="@string/high_temp_alarm_title" + android:importantForAccessibility="noHideDescendants" + style="?android:attr/windowTitleStyle"/> +</LinearLayout> diff --git a/packages/SystemUI/res/raw/overheat_alarm.ogg b/packages/SystemUI/res/raw/overheat_alarm.ogg Binary files differnew file mode 100644 index 000000000000..5624f42a5c50 --- /dev/null +++ b/packages/SystemUI/res/raw/overheat_alarm.ogg diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index a68ba9b2b411..317fe31f9b00 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -368,17 +368,31 @@ <bool name="quick_settings_show_full_alarm">false</bool> + <!-- Whether or not beep sound should be when overheat --> + <bool name="config_alarmTemperatureBeepSound">false</bool> + <!-- Whether to show a warning notification when the device reaches a certain temperature. --> <integer name="config_showTemperatureWarning">0</integer> + <!-- Whether to show a alarm dialog when device of usb cable reaches a certain temperature. --> + <integer name="config_showTemperatureAlarm">0</integer> + <!-- Temp at which to show a warning notification if config_showTemperatureWarning is true. If < 0, uses the skin temperature sensor shutdown value from HardwarePropertiesManager#getDeviceTemperatures - config_warningTemperatureTolerance. --> <integer name="config_warningTemperature">-1</integer> + <!-- Temp at which to show a alarm dialog if config_showTemperatureAlarm is true. + If < 0, uses the skin temperature sensor shutdown value of index[1] from + HardwarePropertiesManager#getDeviceTemperatures --> + <integer name="config_alarmTemperature">60</integer> + <!-- Fudge factor for how much below the shutdown temp to show the warning. --> <integer name="config_warningTemperatureTolerance">2</integer> + <!-- Fudge factor for how much below the overheat temp to dismiss alarm. --> + <integer name="config_alarmTemperatureTolerance">5</integer> + <!-- Accessibility actions --> <item type="id" name="action_split_task_to_left" /> <item type="id" name="action_split_task_to_right" /> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 79e1fae98c21..f5f198d44257 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1023,4 +1023,11 @@ <!-- How much we expand the touchable region of the status bar below the notch to catch touches that just start below the notch. --> <dimen name="display_cutout_touchable_region_size">12dp</dimen> + + <!-- Padding for overheat alarm dialog message of content --> + <dimen name="alarm_dialog_panel_padding">24dp</dimen> + + <!-- Padding for overheat alarm dialog message of content --> + <dimen name="alarm_dialog_message_space">28dp</dimen> + </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 18f378e5919b..114fbe4daaf7 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2067,6 +2067,14 @@ <string name="high_temp_notif_message">Some features limited while phone cools down</string> <!-- Text body for dialog alerting user that their phone has reached a certain temperature and may start to slow down in order to cool down. [CHAR LIMIT=300] --> <string name="high_temp_dialog_message">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.</string> + <!-- Title for alarm dialog alerting user the usb adapter has reached a certain temperature that should disconnect charging cable immediately. [CHAR LIMIT=30] --> + <string name="high_temp_alarm_title">Unplug charger</string> + <!-- Text body for dialog alerting user the usb adapter has reached a certain temperature that should disconnect charging cable immediately. [CHAR LIMIT=300] --> + <string name="high_temp_alarm_notify_message">There\u2019s an issue charging this device. Unplug the power adapter and take care as the cable may be warm.</string> + <!-- Text link for user to see more detail about overheat alarm. [CHAR LIMIT=300] --> + <string name="high_temp_alarm_help_care_steps">See care steps</string> + <!-- Help link of context for usb overheat web page --> + <string name="high_temp_alarm_help_url" translatable="false">help_uri_usb_warm</string> <!-- SysUI Tuner: Button to select lock screen shortcut [CHAR LIMIT=60] --> <string name="lockscreen_shortcut_left">Left shortcut</string> 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 000000000000..db15909196c7 --- /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 000000000000..1ecec5116266 --- /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 288f058b529a..c32a4435fa72 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 0b9067e5dc1f..80ebb6ee9acd 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 initTemperature() { + initTemperatureWarning(); + initTemperatureAlarm(); + if (mEnableTemperatureWarning || mEnableTemperatureAlarm) { + bindThermalService(); + } + } + + private void initTemperatureAlarm() { + mEnableTemperatureAlarm = mContext.getResources().getInteger( + R.integer.config_showTemperatureAlarm) != 0; + if (!mEnableTemperatureAlarm) { + return; + } + + 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(); - if (Settings.Global.getInt(resolver, Settings.Global.SHOW_TEMPERATURE_WARNING, - resources.getInteger(R.integer.config_showTemperatureWarning)) == 0) { + mEnableTemperatureWarning = Settings.Global.getInt(resolver, + Settings.Global.SHOW_TEMPERATURE_WARNING, + resources.getInteger(R.integer.config_showTemperatureWarning)) != 0; + if (!mEnableTemperatureWarning) { return; } - mThresholdTemp = Settings.Global.getFloat(resolver, Settings.Global.WARNING_TEMPERATURE, + 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); + /** + * 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(float temp) { + 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 ca55e1f23d20..96e8310a1c62 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 1be83229cf22..c40d72d483a0 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -49,6 +49,7 @@ <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" /> <uses-permission android:name="android.permission.NETWORK_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.VIBRATE" /> <application android:debuggable="true"> <uses-library android:name="android.test.runner" /> 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 000000000000..651d2b7af7db --- /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 000000000000..04d3163f0574 --- /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 bf6cc53aa5e9..d37c2852bc1f 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 a9d49f91e44e..054bdee9fd97 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,11 +156,95 @@ 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); when(mEnhancedEstimates.getLowWarningThreshold()) @@ -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 d79d833f2233..4ebf9aa005d8 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 |