diff options
| author | 2022-10-31 20:01:52 +0000 | |
|---|---|---|
| committer | 2022-11-11 19:13:49 +0000 | |
| commit | 1b65e52344090a324e2f7472da25f5e5320ac235 (patch) | |
| tree | c7c6e5b1038a354937a0d1fd8c7315532cbe8680 | |
| parent | 2c34290e8741107ff0f03b4f326da7a14c1675a2 (diff) | |
[Dock Defend] Add an optional shield to the status bar battery drawable.
The shield will be displayed when the battery is marked as overheated.
Bug: 255625888
Test: manual: issue `adb shell am broadcast -a com.android.systemui.demo
-e command battery -e overheated true` and verify the battery icon
displays a shield
Test: See bug for manual demo video
Test: statusbar.battery tests
Change-Id: I6b3036ae47ad7b5ae8dc78f83d019130935c9b50
15 files changed, 669 insertions, 20 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt index 5fa04f93e993..faea5b2c3e71 100644 --- a/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt +++ b/packages/SettingsLib/src/com/android/settingslib/graph/ThemedBatteryDrawable.kt @@ -412,14 +412,13 @@ open class ThemedBatteryDrawable(private val context: Context, frameColor: Int) } companion object { - private const val TAG = "ThemedBatteryDrawable" - private const val WIDTH = 12f - private const val HEIGHT = 20f + const val WIDTH = 12f + const val HEIGHT = 20f private const val CRITICAL_LEVEL = 15 // On a 12x20 grid, how wide to make the fill protection stroke. // Scales when our size changes private const val PROTECTION_STROKE_WIDTH = 3f // Arbitrarily chosen for visibility at small sizes - private const val PROTECTION_MIN_STROKE_WIDTH = 6f + const val PROTECTION_MIN_STROKE_WIDTH = 6f } } diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index ce9829b318cd..55d637916d0c 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -485,6 +485,12 @@ <!-- Whether to show a severe low battery dialog. --> <bool name="config_severe_battery_dialog">false</bool> + <!-- A path representing a shield. Will sometimes be displayed with the battery icon when + needed. This path is a 10px wide and 13px tall. --> + <string name="config_batterymeterShieldPath" translatable="false"> + M5 0L0 1.88V6.19C0 9.35 2.13 12.29 5 13.01C7.87 12.29 10 9.35 10 6.19V1.88L5 0Z + </string> + <!-- A path similar to frameworks/base/core/res/res/values/config.xml config_mainBuiltInDisplayCutout that describes a path larger than the exact path of a display cutout. If present as well as config_enableDisplayCutoutProtection is set to true, then diff --git a/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt new file mode 100644 index 000000000000..a91100a8856e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2022 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.battery + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PixelFormat +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import android.graphics.drawable.DrawableWrapper +import android.util.PathParser +import com.android.settingslib.graph.ThemedBatteryDrawable +import com.android.systemui.R +import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT +import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD +import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH +import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD +import com.android.systemui.battery.BatterySpecs.SHIELD_LEFT_OFFSET +import com.android.systemui.battery.BatterySpecs.SHIELD_STROKE +import com.android.systemui.battery.BatterySpecs.SHIELD_TOP_OFFSET + +/** + * A battery drawable that accessorizes [ThemedBatteryDrawable] with additional information if + * necessary. + * + * For now, it adds a shield in the bottom-right corner when [displayShield] is true. + */ +class AccessorizedBatteryDrawable( + private val context: Context, + frameColor: Int, +) : DrawableWrapper(ThemedBatteryDrawable(context, frameColor)) { + private val mainBatteryDrawable: ThemedBatteryDrawable + get() = drawable as ThemedBatteryDrawable + + private val shieldPath = Path() + private val scaledShield = Path() + private val scaleMatrix = Matrix() + + private var shieldLeftOffsetScaled = SHIELD_LEFT_OFFSET + private var shieldTopOffsetScaled = SHIELD_TOP_OFFSET + + private val dualTone = + context.resources.getBoolean(com.android.internal.R.bool.config_batterymeterDualTone) + + private val shieldTransparentOutlinePaint = + Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = Color.TRANSPARENT + p.strokeWidth = ThemedBatteryDrawable.PROTECTION_MIN_STROKE_WIDTH + p.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + p.style = Paint.Style.FILL_AND_STROKE + } + + private val shieldPaint = + Paint(Paint.ANTI_ALIAS_FLAG).also { p -> + p.color = Color.MAGENTA + p.style = Paint.Style.FILL + p.isDither = true + } + + init { + loadPaths() + } + + override fun onBoundsChange(bounds: Rect) { + super.onBoundsChange(bounds) + updateSizes() + } + + var displayShield: Boolean = false + + private fun updateSizes() { + val b = bounds + if (b.isEmpty) { + return + } + + val mainWidth = BatterySpecs.getMainBatteryWidth(b.width().toFloat(), displayShield) + val mainHeight = BatterySpecs.getMainBatteryHeight(b.height().toFloat(), displayShield) + + drawable?.setBounds( + b.left, + b.top, + /* right= */ b.left + mainWidth.toInt(), + /* bottom= */ b.top + mainHeight.toInt() + ) + + if (displayShield) { + val sx = b.right / BATTERY_WIDTH_WITH_SHIELD + val sy = b.bottom / BATTERY_HEIGHT_WITH_SHIELD + scaleMatrix.setScale(sx, sy) + shieldPath.transform(scaleMatrix, scaledShield) + + shieldLeftOffsetScaled = sx * SHIELD_LEFT_OFFSET + shieldTopOffsetScaled = sy * SHIELD_TOP_OFFSET + + val scaledStrokeWidth = + (sx * SHIELD_STROKE).coerceAtLeast( + ThemedBatteryDrawable.PROTECTION_MIN_STROKE_WIDTH + ) + shieldTransparentOutlinePaint.strokeWidth = scaledStrokeWidth + } + } + + override fun getIntrinsicHeight(): Int { + val height = + if (displayShield) { + BATTERY_HEIGHT_WITH_SHIELD + } else { + BATTERY_HEIGHT + } + // TODO(b/255625888): Cache the density so we don't have to re-fetch. + return (height * context.resources.displayMetrics.density).toInt() + } + + override fun getIntrinsicWidth(): Int { + val width = + if (displayShield) { + BATTERY_WIDTH_WITH_SHIELD + } else { + BATTERY_WIDTH + } + // TODO(b/255625888): Cache the density so we don't have to re-fetch. + return (width * context.resources.displayMetrics.density).toInt() + } + + override fun draw(c: Canvas) { + c.saveLayer(null, null) + // Draw the main battery icon + super.draw(c) + + if (displayShield) { + c.translate(shieldLeftOffsetScaled, shieldTopOffsetScaled) + // We need a transparent outline around the shield, so first draw the transparent-ness + // then draw the shield + c.drawPath(scaledShield, shieldTransparentOutlinePaint) + c.drawPath(scaledShield, shieldPaint) + } + c.restore() + } + + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun setAlpha(p0: Int) { + // Unused internally -- see [ThemedBatteryDrawable.setAlpha]. + } + + override fun setColorFilter(colorfilter: ColorFilter?) { + super.setColorFilter(colorFilter) + shieldPaint.colorFilter = colorFilter + } + + /** Sets whether the battery is currently charging. */ + fun setCharging(charging: Boolean) { + mainBatteryDrawable.charging = charging + } + + /** Sets the current level (out of 100) of the battery. */ + fun setBatteryLevel(level: Int) { + mainBatteryDrawable.setBatteryLevel(level) + } + + /** Sets whether power save is enabled. */ + fun setPowerSaveEnabled(powerSaveEnabled: Boolean) { + mainBatteryDrawable.powerSaveEnabled = powerSaveEnabled + } + + /** Returns whether power save is currently enabled. */ + fun getPowerSaveEnabled(): Boolean { + return mainBatteryDrawable.powerSaveEnabled + } + + /** Sets the colors to use for the icon. */ + fun setColors(fgColor: Int, bgColor: Int, singleToneColor: Int) { + shieldPaint.color = if (dualTone) fgColor else singleToneColor + mainBatteryDrawable.setColors(fgColor, bgColor, singleToneColor) + } + + private fun loadPaths() { + val shieldPathString = context.resources.getString(R.string.config_batterymeterShieldPath) + shieldPath.set(PathParser.createPathFromPathData(shieldPathString)) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java index 6a10d4ab1e8b..d301858505b9 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java @@ -45,7 +45,6 @@ import android.widget.TextView; import androidx.annotation.StyleRes; import androidx.annotation.VisibleForTesting; -import com.android.settingslib.graph.ThemedBatteryDrawable; import com.android.systemui.DualToneHandler; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; @@ -68,7 +67,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { public static final int MODE_OFF = 2; public static final int MODE_ESTIMATE = 3; - private final ThemedBatteryDrawable mDrawable; + private final AccessorizedBatteryDrawable mDrawable; private final ImageView mBatteryIconView; private TextView mBatteryPercentView; @@ -78,6 +77,8 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { private int mShowPercentMode = MODE_DEFAULT; private boolean mShowPercentAvailable; private boolean mCharging; + private boolean mDisplayShield; + private boolean mDisplayShieldEnabled; // Error state where we know nothing about the current battery state private boolean mBatteryStateUnknown; // Lazily-loaded since this is expected to be a rare-if-ever state @@ -106,7 +107,7 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { final int frameColor = atts.getColor(R.styleable.BatteryMeterView_frameColor, context.getColor(R.color.meter_background_color)); mPercentageStyleId = atts.getResourceId(R.styleable.BatteryMeterView_textAppearance, 0); - mDrawable = new ThemedBatteryDrawable(context, frameColor); + mDrawable = new AccessorizedBatteryDrawable(context, frameColor); atts.recycle(); mShowPercentAvailable = context.getResources().getBoolean( @@ -203,6 +204,19 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { mDrawable.setPowerSaveEnabled(isPowerSave); } + void onIsOverheatedChanged(boolean isOverheated) { + // The battery drawable is a different size depending on whether it's currently overheated + // or not, so we need to re-scale the view when overheated changes. + boolean requiresScaling = mDisplayShield != isOverheated; + // If the battery is marked as overheated, we should display a shield indicating that the + // battery is being "defended". + mDisplayShield = isOverheated; + if (requiresScaling) { + scaleBatteryMeterViews(); + } + // TODO(b/255625888): We should also update the content description. + } + private TextView loadPercentView() { return (TextView) LayoutInflater.from(getContext()) .inflate(R.layout.battery_percentage_view, null); @@ -227,6 +241,10 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { mBatteryEstimateFetcher = fetcher; } + void setDisplayShieldEnabled(boolean displayShieldEnabled) { + mDisplayShieldEnabled = displayShieldEnabled; + } + void updatePercentText() { if (mBatteryStateUnknown) { setContentDescription(getContext().getString(R.string.accessibility_battery_unknown)); @@ -349,15 +367,29 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver { res.getValue(R.dimen.status_bar_icon_scale_factor, typedValue, true); float iconScaleFactor = typedValue.getFloat(); - int batteryHeight = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height); - int batteryWidth = res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width); + float mainBatteryHeight = + res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_height) * iconScaleFactor; + float mainBatteryWidth = + res.getDimensionPixelSize(R.dimen.status_bar_battery_icon_width) * iconScaleFactor; + + boolean displayShield = mDisplayShieldEnabled && mDisplayShield; + float fullBatteryIconHeight = + BatterySpecs.getFullBatteryHeight(mainBatteryHeight, displayShield); + float fullBatteryIconWidth = + BatterySpecs.getFullBatteryWidth(mainBatteryWidth, displayShield); + + // TODO(b/255625888): Add some marginTop so that, even when the battery icon has the shield, + // the bottom of the main icon is still aligned with the bottom of all the other icons. int marginBottom = res.getDimensionPixelSize(R.dimen.battery_margin_bottom); LinearLayout.LayoutParams scaledLayoutParams = new LinearLayout.LayoutParams( - (int) (batteryWidth * iconScaleFactor), (int) (batteryHeight * iconScaleFactor)); + Math.round(fullBatteryIconWidth), + Math.round(fullBatteryIconHeight)); scaledLayoutParams.setMargins(0, 0, 0, marginBottom); + mDrawable.setDisplayShield(displayShield); mBatteryIconView.setLayoutParams(scaledLayoutParams); + mBatteryIconView.invalidateDrawable(mDrawable); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java index ae9a32309d45..77cb9d1bf594 100644 --- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java +++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java @@ -29,6 +29,8 @@ import android.view.View; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.settings.CurrentUserTracker; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.policy.BatteryController; @@ -84,6 +86,11 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView> public void onBatteryUnknownStateChanged(boolean isUnknown) { mView.onBatteryUnknownStateChanged(isUnknown); } + + @Override + public void onIsOverheatedChanged(boolean isOverheated) { + mView.onIsOverheatedChanged(isOverheated); + } }; // Some places may need to show the battery conditionally, and not obey the tuner @@ -98,6 +105,7 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView> BroadcastDispatcher broadcastDispatcher, @Main Handler mainHandler, ContentResolver contentResolver, + FeatureFlags featureFlags, BatteryController batteryController) { super(view); mConfigurationController = configurationController; @@ -106,6 +114,7 @@ public class BatteryMeterViewController extends ViewController<BatteryMeterView> mBatteryController = batteryController; mView.setBatteryEstimateFetcher(mBatteryController::getEstimatedTimeRemainingString); + mView.setDisplayShieldEnabled(featureFlags.isEnabled(Flags.BATTERY_SHIELD_ICON)); mSlotBattery = getResources().getString(com.android.internal.R.string.status_bar_battery); mSettingObserver = new SettingObserver(mainHandler); diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt b/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt new file mode 100644 index 000000000000..6455a9656fde --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/battery/BatterySpecs.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 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.battery + +import com.android.settingslib.graph.ThemedBatteryDrawable + +/** An object storing specs related to the battery icon in the status bar. */ +object BatterySpecs { + + /** Width of the main battery icon, not including the shield. */ + const val BATTERY_WIDTH = ThemedBatteryDrawable.WIDTH + /** Height of the main battery icon, not including the shield. */ + const val BATTERY_HEIGHT = ThemedBatteryDrawable.HEIGHT + + private const val SHIELD_WIDTH = 10f + private const val SHIELD_HEIGHT = 13f + + /** + * Amount that the left side of the shield should be offset from the left side of the battery. + */ + const val SHIELD_LEFT_OFFSET = 8f + /** Amount that the top of the shield should be offset from the top of the battery. */ + const val SHIELD_TOP_OFFSET = 10f + + const val SHIELD_STROKE = 4f + + /** The full width of the battery icon, including the main battery icon *and* the shield. */ + const val BATTERY_WIDTH_WITH_SHIELD = SHIELD_LEFT_OFFSET + SHIELD_WIDTH + /** The full height of the battery icon, including the main battery icon *and* the shield. */ + const val BATTERY_HEIGHT_WITH_SHIELD = SHIELD_TOP_OFFSET + SHIELD_HEIGHT + + /** + * Given the desired height of the main battery icon in pixels, returns the height that the full + * battery icon will take up in pixels. + * + * If there's no shield, this will just return [mainBatteryHeight]. Otherwise, the shield + * extends slightly below the bottom of the main battery icon so we need some extra height. + */ + @JvmStatic + fun getFullBatteryHeight(mainBatteryHeight: Float, displayShield: Boolean): Float { + return if (!displayShield) { + mainBatteryHeight + } else { + val verticalScaleFactor = mainBatteryHeight / BATTERY_HEIGHT + verticalScaleFactor * BATTERY_HEIGHT_WITH_SHIELD + } + } + + /** + * Given the desired width of the main battery icon in pixels, returns the width that the full + * battery icon will take up in pixels. + * + * If there's no shield, this will just return [mainBatteryWidth]. Otherwise, the shield extends + * past the right side of the main battery icon so we need some extra width. + */ + @JvmStatic + fun getFullBatteryWidth(mainBatteryWidth: Float, displayShield: Boolean): Float { + return if (!displayShield) { + mainBatteryWidth + } else { + val horizontalScaleFactor = mainBatteryWidth / BATTERY_WIDTH + horizontalScaleFactor * BATTERY_WIDTH_WITH_SHIELD + } + } + + /** + * Given the height of the full battery icon, return how tall the main battery icon should be. + * + * If there's no shield, this will just return [fullBatteryHeight]. Otherwise, the shield takes + * up some of the view's height so the main battery width will be just a portion of + * [fullBatteryHeight]. + */ + @JvmStatic + fun getMainBatteryHeight(fullBatteryHeight: Float, displayShield: Boolean): Float { + return if (!displayShield) { + fullBatteryHeight + } else { + return (BATTERY_HEIGHT / BATTERY_HEIGHT_WITH_SHIELD) * fullBatteryHeight + } + } + + /** + * Given the width of the full battery icon, return how wide the main battery icon should be. + * + * If there's no shield, this will just return [fullBatteryWidth]. Otherwise, the shield takes + * up some of the view's width so the main battery width will be just a portion of + * [fullBatteryWidth]. + */ + @JvmStatic + fun getMainBatteryWidth(fullBatteryWidth: Float, displayShield: Boolean): Float { + return if (!displayShield) { + fullBatteryWidth + } else { + return (BATTERY_WIDTH / BATTERY_WIDTH_WITH_SHIELD) * fullBatteryWidth + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 99dfefa4fa41..f34749f9d892 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -233,6 +233,9 @@ object Flags { // TODO(b/256613548): Tracking Bug val NEW_STATUS_BAR_WIFI_ICON_BACKEND = unreleasedFlag(609, "new_status_bar_wifi_icon_backend") + // TODO(b/256623670): Tracking Bug + @JvmField val BATTERY_SHIELD_ICON = unreleasedFlag(610, "battery_shield_icon") + // 700 - dialer/calls // TODO(b/254512734): Tracking Bug val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java index dc902666874d..615f2304ecf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarViewModule.java @@ -226,6 +226,7 @@ public abstract class StatusBarViewModule { BroadcastDispatcher broadcastDispatcher, @Main Handler mainHandler, ContentResolver contentResolver, + FeatureFlags featureFlags, BatteryController batteryController ) { return new BatteryMeterViewController( @@ -235,6 +236,7 @@ public abstract class StatusBarViewModule { broadcastDispatcher, mainHandler, contentResolver, + featureFlags, batteryController); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java index 149ed0a71b91..d10d7cf8460b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java @@ -155,6 +155,9 @@ public interface BatteryController extends DemoMode, default void onWirelessChargingChanged(boolean isWirlessCharging) { } + + default void onIsOverheatedChanged(boolean isOverheated) { + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index c7ad76722929..3c2ac7b7a124 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -16,6 +16,9 @@ package com.android.systemui.statusbar.policy; +import static android.os.BatteryManager.BATTERY_HEALTH_OVERHEAT; +import static android.os.BatteryManager.BATTERY_HEALTH_UNKNOWN; +import static android.os.BatteryManager.EXTRA_HEALTH; import static android.os.BatteryManager.EXTRA_PRESENT; import android.annotation.WorkerThread; @@ -87,6 +90,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC protected boolean mPowerSave; private boolean mAodPowerSave; private boolean mWirelessCharging; + private boolean mIsOverheated = false; private boolean mTestMode = false; @VisibleForTesting boolean mHasReceivedBattery = false; @@ -184,6 +188,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC cb.onPowerSaveChanged(mPowerSave); cb.onBatteryUnknownStateChanged(mStateUnknown); cb.onWirelessChargingChanged(mWirelessCharging); + cb.onIsOverheatedChanged(mIsOverheated); } @Override @@ -222,6 +227,13 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC fireBatteryUnknownStateChanged(); } + int batteryHealth = intent.getIntExtra(EXTRA_HEALTH, BATTERY_HEALTH_UNKNOWN); + boolean isOverheated = batteryHealth == BATTERY_HEALTH_OVERHEAT; + if (isOverheated != mIsOverheated) { + mIsOverheated = isOverheated; + fireIsOverheatedChanged(); + } + fireBatteryLevelChanged(); } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) { updatePowerSave(); @@ -292,6 +304,10 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC return mPluggedChargingSource == BatteryManager.BATTERY_PLUGGED_WIRELESS; } + public boolean isOverheated() { + return mIsOverheated; + } + @Override public void getEstimatedTimeRemainingString(EstimateFetchCompletion completion) { // Need to fetch or refresh the estimate, but it may involve binder calls so offload the @@ -402,6 +418,15 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC } } + private void fireIsOverheatedChanged() { + synchronized (mChangeCallbacks) { + final int n = mChangeCallbacks.size(); + for (int i = 0; i < n; i++) { + mChangeCallbacks.get(i).onIsOverheatedChanged(mIsOverheated); + } + } + } + @Override public void dispatchDemoCommand(String command, Bundle args) { if (!mDemoModeController.isInDemoMode()) { @@ -412,6 +437,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC String plugged = args.getString("plugged"); String powerSave = args.getString("powersave"); String present = args.getString("present"); + String overheated = args.getString("overheated"); if (level != null) { mLevel = Math.min(Math.max(Integer.parseInt(level), 0), 100); } @@ -426,6 +452,10 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC mStateUnknown = !present.equals("true"); fireBatteryUnknownStateChanged(); } + if (overheated != null) { + mIsOverheated = overheated.equals("true"); + fireIsOverheatedChanged(); + } fireBatteryLevelChanged(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt new file mode 100644 index 000000000000..982f033d05e5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/battery/AccessorizedBatteryDrawableTest.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 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.battery + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT +import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD +import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH +import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class AccessorizedBatteryDrawableTest : SysuiTestCase() { + @Test + fun intrinsicSize_shieldFalse_isBatterySize() { + val drawable = AccessorizedBatteryDrawable(context, frameColor = 0) + drawable.displayShield = false + + val density = context.resources.displayMetrics.density + assertThat(drawable.intrinsicHeight).isEqualTo((BATTERY_HEIGHT * density).toInt()) + assertThat(drawable.intrinsicWidth).isEqualTo((BATTERY_WIDTH * density).toInt()) + } + + @Test + fun intrinsicSize_shieldTrue_isBatteryPlusShieldSize() { + val drawable = AccessorizedBatteryDrawable(context, frameColor = 0) + drawable.displayShield = true + + val density = context.resources.displayMetrics.density + assertThat(drawable.intrinsicHeight) + .isEqualTo((BATTERY_HEIGHT_WITH_SHIELD * density).toInt()) + assertThat(drawable.intrinsicWidth).isEqualTo((BATTERY_WIDTH_WITH_SHIELD * density).toInt()) + } + + // TODO(b/255625888): Screenshot tests for this drawable would be amazing! +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java index 1d038a43ec8e..bc8f96198b27 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java @@ -35,6 +35,8 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; @@ -59,6 +61,7 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { private Handler mHandler; @Mock private ContentResolver mContentResolver; + private FakeFeatureFlags mFeatureFlags; @Mock private BatteryController mBatteryController; @@ -71,19 +74,13 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { when(mBatteryMeterView.getContext()).thenReturn(mContext); when(mBatteryMeterView.getResources()).thenReturn(mContext.getResources()); - mController = new BatteryMeterViewController( - mBatteryMeterView, - mConfigurationController, - mTunerService, - mBroadcastDispatcher, - mHandler, - mContentResolver, - mBatteryController - ); + mFeatureFlags = new FakeFeatureFlags(); + mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, false); } @Test public void onViewAttached_callbacksRegistered() { + initController(); mController.onViewAttached(); verify(mConfigurationController).addCallback(any()); @@ -101,6 +98,7 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { @Test public void onViewDetached_callbacksUnregistered() { + initController(); // Set everything up first. mController.onViewAttached(); @@ -114,6 +112,7 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { @Test public void ignoreTunerUpdates_afterOnViewAttached_callbackUnregistered() { + initController(); // Start out receiving tuner updates mController.onViewAttached(); @@ -124,10 +123,43 @@ public class BatteryMeterViewControllerTest extends SysuiTestCase { @Test public void ignoreTunerUpdates_beforeOnViewAttached_callbackNeverRegistered() { + initController(); + mController.ignoreTunerUpdates(); mController.onViewAttached(); verify(mTunerService, never()).addTunable(any(), any()); } + + @Test + public void shieldFlagDisabled_viewNotified() { + mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, false); + + initController(); + + verify(mBatteryMeterView).setDisplayShieldEnabled(false); + } + + @Test + public void shieldFlagEnabled_viewNotified() { + mFeatureFlags.set(Flags.BATTERY_SHIELD_ICON, true); + + initController(); + + verify(mBatteryMeterView).setDisplayShieldEnabled(true); + } + + private void initController() { + mController = new BatteryMeterViewController( + mBatteryMeterView, + mConfigurationController, + mTunerService, + mBroadcastDispatcher, + mHandler, + mContentResolver, + mFeatureFlags, + mBatteryController + ); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt index b4ff2a5e1fbf..851b5408a4e0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.battery import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper +import android.widget.ImageView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.battery.BatteryMeterView.BatteryEstimateFetcher @@ -58,6 +59,45 @@ class BatteryMeterViewTest : SysuiTestCase() { // No assert needed } + @Test + fun isOverheatedChanged_true_drawableGetsTrue() { + mBatteryMeterView.setDisplayShieldEnabled(true) + val drawable = getBatteryDrawable() + + mBatteryMeterView.onIsOverheatedChanged(true) + + assertThat(drawable.displayShield).isTrue() + } + + @Test + fun isOverheatedChanged_false_drawableGetsFalse() { + mBatteryMeterView.setDisplayShieldEnabled(true) + val drawable = getBatteryDrawable() + + // Start as true + mBatteryMeterView.onIsOverheatedChanged(true) + + // Update to false + mBatteryMeterView.onIsOverheatedChanged(false) + + assertThat(drawable.displayShield).isFalse() + } + + @Test + fun isOverheatedChanged_true_featureflagOff_drawableGetsFalse() { + mBatteryMeterView.setDisplayShieldEnabled(false) + val drawable = getBatteryDrawable() + + mBatteryMeterView.onIsOverheatedChanged(true) + + assertThat(drawable.displayShield).isFalse() + } + + private fun getBatteryDrawable(): AccessorizedBatteryDrawable { + return (mBatteryMeterView.getChildAt(0) as ImageView) + .drawable as AccessorizedBatteryDrawable + } + private class Fetcher : BatteryEstimateFetcher { override fun fetchBatteryTimeRemainingEstimate( completion: EstimateFetchCompletion) { @@ -68,4 +108,4 @@ class BatteryMeterViewTest : SysuiTestCase() { private companion object { const val ESTIMATE = "2 hours 2 minutes" } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt new file mode 100644 index 000000000000..39cb0476e429 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatterySpecsTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2022 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.battery + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT +import com.android.systemui.battery.BatterySpecs.BATTERY_HEIGHT_WITH_SHIELD +import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH +import com.android.systemui.battery.BatterySpecs.BATTERY_WIDTH_WITH_SHIELD +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class BatterySpecsTest : SysuiTestCase() { + @Test + fun getFullBatteryHeight_shieldFalse_returnsMainHeight() { + val fullHeight = BatterySpecs.getFullBatteryHeight(56f, displayShield = false) + + assertThat(fullHeight).isEqualTo(56f) + } + + @Test + fun getFullBatteryHeight_shieldTrue_returnsMainHeightPlusShield() { + val mainHeight = BATTERY_HEIGHT * 5 + val fullHeight = BatterySpecs.getFullBatteryHeight(mainHeight, displayShield = true) + + // Since the main battery was scaled 5x, the output height should also be scaled 5x + val expectedFullHeight = BATTERY_HEIGHT_WITH_SHIELD * 5 + + assertThat(fullHeight).isWithin(.0001f).of(expectedFullHeight) + } + + @Test + fun getFullBatteryWidth_shieldFalse_returnsMainWidth() { + val fullWidth = BatterySpecs.getFullBatteryWidth(33f, displayShield = false) + + assertThat(fullWidth).isEqualTo(33f) + } + + @Test + fun getFullBatteryWidth_shieldTrue_returnsMainWidthPlusShield() { + val mainWidth = BATTERY_WIDTH * 3.3f + + val fullWidth = BatterySpecs.getFullBatteryWidth(mainWidth, displayShield = true) + + // Since the main battery was scaled 3.3x, the output width should also be scaled 5x + val expectedFullWidth = BATTERY_WIDTH_WITH_SHIELD * 3.3f + assertThat(fullWidth).isWithin(.0001f).of(expectedFullWidth) + } + + @Test + fun getMainBatteryHeight_shieldFalse_returnsFullHeight() { + val mainHeight = BatterySpecs.getMainBatteryHeight(89f, displayShield = false) + + assertThat(mainHeight).isEqualTo(89f) + } + + @Test + fun getMainBatteryHeight_shieldTrue_returnsNotFullHeight() { + val fullHeight = BATTERY_HEIGHT_WITH_SHIELD * 7.7f + + val mainHeight = BatterySpecs.getMainBatteryHeight(fullHeight, displayShield = true) + + // Since the full height was scaled 7.7x, the main height should also be scaled 7.7x. + val expectedHeight = BATTERY_HEIGHT * 7.7f + assertThat(mainHeight).isWithin(.0001f).of(expectedHeight) + } + + @Test + fun getMainBatteryWidth_shieldFalse_returnsFullWidth() { + val mainWidth = BatterySpecs.getMainBatteryWidth(2345f, displayShield = false) + + assertThat(mainWidth).isEqualTo(2345f) + } + + @Test + fun getMainBatteryWidth_shieldTrue_returnsNotFullWidth() { + val fullWidth = BATTERY_WIDTH_WITH_SHIELD * 0.6f + + val mainWidth = BatterySpecs.getMainBatteryWidth(fullWidth, displayShield = true) + + // Since the full width was scaled 0.6x, the main height should also be scaled 0.6x. + val expectedWidth = BATTERY_WIDTH * 0.6f + assertThat(mainWidth).isWithin(.0001f).of(expectedWidth) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java index 43d0fe9559b6..1eee08c22187 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java @@ -221,4 +221,33 @@ public class BatteryControllerTest extends SysuiTestCase { Assert.assertFalse(mBatteryController.isChargingSourceDock()); } + + @Test + public void batteryStateChanged_healthNotOverheated_outputsFalse() { + Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); + intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_GOOD); + + mBatteryController.onReceive(getContext(), intent); + + Assert.assertFalse(mBatteryController.isOverheated()); + } + + @Test + public void batteryStateChanged_healthOverheated_outputsTrue() { + Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); + intent.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT); + + mBatteryController.onReceive(getContext(), intent); + + Assert.assertTrue(mBatteryController.isOverheated()); + } + + @Test + public void batteryStateChanged_noHealthGiven_outputsFalse() { + Intent intent = new Intent(Intent.ACTION_BATTERY_CHANGED); + + mBatteryController.onReceive(getContext(), intent); + + Assert.assertFalse(mBatteryController.isOverheated()); + } } |