[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
This commit is contained in:
Caitlin Shkuratov
2022-10-31 20:01:52 +00:00
parent 2c34290e87
commit 1b65e52344
15 changed files with 669 additions and 20 deletions

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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))
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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
}
}
}

View File

@@ -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")

View File

@@ -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);
}

View File

@@ -155,6 +155,9 @@ public interface BatteryController extends DemoMode,
default void onWirelessChargingChanged(boolean isWirlessCharging) {
}
default void onIsOverheatedChanged(boolean isOverheated) {
}
}
/**

View File

@@ -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();
}

View File

@@ -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!
}

View File

@@ -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
);
}
}

View File

@@ -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"
}
}
}

View File

@@ -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)
}
}

View File

@@ -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());
}
}