Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1b65e523 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[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
parent 2c34290e
Loading
Loading
Loading
Loading
+3 −4
Original line number Diff line number Diff line
@@ -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
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -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
+202 −0
Original line number Diff line number Diff line
/*
 * 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))
    }
}
+38 −6
Original line number Diff line number Diff line
@@ -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
+9 −0
Original line number Diff line number Diff line
@@ -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);
Loading