Statusbar charging animation chip when plugging in device

Bug: 197638244
Test: Separate CL (ag/20982130)
Change-Id: I93b8bb4e0eb277e9491e7667de0142f6b1501fea
This commit is contained in:
Johannes Gallmann
2023-01-10 16:11:28 +01:00
parent 36c2629482
commit bd502db502
13 changed files with 214 additions and 45 deletions

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2023 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.
-->
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<solid android:color="?androidprv:attr/colorAccentPrimary" />
<corners android:radius="@dimen/ongoing_appops_chip_bg_corner_radius" />
</shape>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?><!--
Copyright (C) 2023 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical|end"
tools:parentTag="com.android.systemui.statusbar.BatteryStatusChip">
<LinearLayout
android:id="@+id/rounded_container"
android:layout_width="wrap_content"
android:layout_height="@dimen/ongoing_appops_chip_height"
android:layout_gravity="center"
android:background="@drawable/statusbar_chip_bg"
android:clipToOutline="true"
android:gravity="center"
android:maxWidth="@dimen/ongoing_appops_chip_max_width"
android:minWidth="@dimen/ongoing_appops_chip_min_width">
<com.android.systemui.battery.BatteryMeterView
android:id="@+id/battery_meter_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginHorizontal="10dp" />
</LinearLayout>
</merge>

View File

@@ -22,10 +22,8 @@
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|end"
android:focusable="true"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingStart="8dp"
>
<LinearLayout
@@ -35,6 +33,8 @@
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:gravity="center"
android:clipToOutline="true"
android:clipToPadding="false"
android:layout_gravity="center"
android:minWidth="@dimen/ongoing_appops_chip_min_width"
android:maxWidth="@dimen/ongoing_appops_chip_max_width"

View File

@@ -24,6 +24,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.animation.LayoutTransition;
import android.animation.ObjectAnimator;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -195,7 +196,13 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver {
return false;
}
void onBatteryLevelChanged(int level, boolean pluggedIn) {
/**
* Update battery level
*
* @param level int between 0 and 100 (representing percentage value)
* @param pluggedIn whether the device is plugged in or not
*/
public void onBatteryLevelChanged(@IntRange(from = 0, to = 100) int level, boolean pluggedIn) {
mDrawable.setCharging(pluggedIn);
mDrawable.setBatteryLevel(level);
mCharging = pluggedIn;

View File

@@ -298,6 +298,9 @@ object Flags {
val NEW_STATUS_BAR_ICONS_DEBUG_COLORING =
unreleasedFlag(611, "new_status_bar_icons_debug_coloring")
// TODO(b/265892345): Tracking Bug
val PLUG_IN_STATUS_BAR_CHIP = unreleasedFlag(265892345, "plug_in_status_bar_chip")
// 700 - dialer/calls
// TODO(b/254512734): Tracking Bug
val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip")

View File

@@ -107,6 +107,6 @@ class OngoingPrivacyChip @JvmOverloads constructor(
val padding = context.resources
.getDimensionPixelSize(R.dimen.ongoing_appops_chip_side_padding)
iconsContainer.setPaddingRelative(padding, 0, padding, 0)
iconsContainer.background = context.getDrawable(R.drawable.privacy_chip_bg)
iconsContainer.background = context.getDrawable(R.drawable.statusbar_privacy_chip_bg)
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2023 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.statusbar
import android.annotation.IntRange
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.widget.LinearLayout
import com.android.settingslib.Utils
import com.android.systemui.R
import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.statusbar.events.BackgroundAnimatableView
class BatteryStatusChip @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs), BackgroundAnimatableView {
private val roundedContainer: LinearLayout
private val batteryMeterView: BatteryMeterView
override val contentView: View
get() = batteryMeterView
init {
inflate(context, R.layout.battery_status_chip, this)
roundedContainer = findViewById(R.id.rounded_container)
batteryMeterView = findViewById(R.id.battery_meter_view)
updateResources()
}
/**
* When animating as a chip in the status bar, we want to animate the width for the rounded
* container. We have to subtract our own top and left offset because the bounds come to us as
* absolute on-screen bounds.
*/
override fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) {
roundedContainer.setLeftTopRightBottom(l - left, t - top, r - left, b - top)
}
fun setBatteryLevel(@IntRange(from = 0, to = 100) batteryLevel: Int) {
batteryMeterView.setForceShowPercent(true)
batteryMeterView.onBatteryLevelChanged(batteryLevel, true)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateResources()
}
@SuppressLint("UseCompatLoadingForDrawables")
private fun updateResources() {
val primaryColor =
Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
val textColorSecondary =
Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorSecondary)
batteryMeterView.updateColors(primaryColor, textColorSecondary, primaryColor)
roundedContainer.background = mContext.getDrawable(R.drawable.statusbar_chip_bg)
}
}

View File

@@ -16,17 +16,16 @@
package com.android.systemui.statusbar.events
import android.annotation.IntRange
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import com.android.settingslib.graph.ThemedBatteryDrawable
import com.android.systemui.R
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.statusbar.BatteryStatusChip
typealias ViewCreator = (context: Context) -> BackgroundAnimatableView
@@ -73,17 +72,16 @@ class BGImageView(
}
}
class BatteryEvent : StatusEvent {
class BatteryEvent(@IntRange(from = 0, to = 100) val batteryLevel: Int) : StatusEvent {
override val priority = 50
override val forceVisible = false
override val showAnimation = true
override var contentDescription: String? = ""
override val viewCreator: (context: Context) -> BGImageView = { context ->
val iv = BGImageView(context)
iv.setImageDrawable(ThemedBatteryDrawable(context, Color.WHITE))
iv.setBackgroundDrawable(ColorDrawable(Color.GREEN))
iv
override val viewCreator: ViewCreator = { context ->
BatteryStatusChip(context).apply {
setBatteryLevel(batteryLevel)
}
}
override fun toString(): String {

View File

@@ -117,16 +117,21 @@ class SystemEventChipAnimationController @Inject constructor(
interpolator = null
addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
}
currentAnimatedView?.contentView?.alpha = 0f
val contentAlphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
startDelay = 10.frames
duration = 10.frames
interpolator = null
addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
}
val moveIn = ValueAnimator.ofInt(chipMinWidth, chipWidth).apply {
startDelay = 7.frames
duration = 23.frames
interpolator = STATUS_BAR_X_MOVE_IN
addUpdateListener {
updateAnimatedViewBoundsWidth(animatedValue as Int)
}
addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
}
val animSet = AnimatorSet()
animSet.playTogether(alphaIn, moveIn)
animSet.playTogether(alphaIn, contentAlphaIn, moveIn)
return animSet
}
@@ -210,15 +215,32 @@ class SystemEventChipAnimationController @Inject constructor(
}
private fun createMoveOutAnimationDefault(): Animator {
val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
startDelay = 6.frames
duration = 6.frames
interpolator = null
addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
}
val contentAlphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
duration = 5.frames
interpolator = null
addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
}
val moveOut = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
duration = 23.frames
interpolator = STATUS_BAR_X_MOVE_OUT
addUpdateListener {
currentAnimatedView?.apply {
updateAnimatedViewBoundsWidth(it.animatedValue as Int)
}
}
}
return moveOut
val animSet = AnimatorSet()
animSet.playTogether(alphaOut, contentAlphaOut, moveOut)
return animSet
}
private fun init() {
@@ -296,6 +318,8 @@ class SystemEventChipAnimationController @Inject constructor(
interface BackgroundAnimatableView {
val view: View // Since this can't extend View, add a view prop
get() = this as View
val contentView: View? // This will be alpha faded during appear and disappear animation
get() = null
val chipWidth: Int
get() = view.measuredWidth
fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int)

View File

@@ -16,11 +16,14 @@
package com.android.systemui.statusbar.events
import android.annotation.IntRange
import android.content.Context
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_PRIVACY
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.privacy.PrivacyChipBuilder
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyItemController
@@ -37,21 +40,18 @@ class SystemEventCoordinator @Inject constructor(
private val systemClock: SystemClock,
private val batteryController: BatteryController,
private val privacyController: PrivacyItemController,
private val context: Context
private val context: Context,
private val featureFlags: FeatureFlags
) {
private lateinit var scheduler: SystemStatusAnimationScheduler
fun startObserving() {
/* currently unused
batteryController.addCallback(batteryStateListener)
*/
privacyController.addCallback(privacyStateListener)
}
fun stopObserving() {
/* currently unused
batteryController.removeCallback(batteryStateListener)
*/
privacyController.removeCallback(privacyStateListener)
}
@@ -59,8 +59,10 @@ class SystemEventCoordinator @Inject constructor(
this.scheduler = s
}
fun notifyPluggedIn() {
scheduler.onStatusEvent(BatteryEvent())
fun notifyPluggedIn(@IntRange(from = 0, to = 100) batteryLevel: Int) {
if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) {
scheduler.onStatusEvent(BatteryEvent(batteryLevel))
}
}
fun notifyPrivacyItemsEmpty() {
@@ -79,25 +81,25 @@ class SystemEventCoordinator @Inject constructor(
}
private val batteryStateListener = object : BatteryController.BatteryStateChangeCallback {
var plugged = false
var stateKnown = false
private var plugged = false
private var stateKnown = false
override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
if (!stateKnown) {
stateKnown = true
plugged = pluggedIn
notifyListeners()
notifyListeners(level)
return
}
if (plugged != pluggedIn) {
plugged = pluggedIn
notifyListeners()
notifyListeners(level)
}
}
private fun notifyListeners() {
private fun notifyListeners(@IntRange(from = 0, to = 100) batteryLevel: Int) {
// We only care about the plugged in status
if (plugged) notifyPluggedIn()
if (plugged) notifyPluggedIn(batteryLevel)
}
}

View File

@@ -66,7 +66,8 @@ open class SystemStatusAnimationScheduler @Inject constructor(
companion object {
private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
}
public fun isImmersiveIndicatorEnabled(): Boolean {
fun isImmersiveIndicatorEnabled(): Boolean {
return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
PROPERTY_ENABLE_IMMERSIVE_INDICATOR, true)
}
@@ -80,11 +81,7 @@ open class SystemStatusAnimationScheduler @Inject constructor(
private var scheduledEvent: StatusEvent? = null
private var cancelExecutionRunnable: Runnable? = null
private val listeners = mutableSetOf<SystemStatusAnimationCallback>()
fun getListeners(): MutableSet<SystemStatusAnimationCallback> {
return listeners
}
val listeners = mutableSetOf<SystemStatusAnimationCallback>()
init {
coordinator.attachScheduler(this)
@@ -99,9 +96,8 @@ open class SystemStatusAnimationScheduler @Inject constructor(
// Don't deal with threading for now (no need let's be honest)
Assert.isMainThread()
if ((event.priority > scheduledEvent?.priority ?: -1) &&
animationState != ANIMATING_OUT &&
(animationState != SHOWING_PERSISTENT_DOT && event.forceVisible)) {
if ((event.priority > (scheduledEvent?.priority ?: -1)) &&
animationState != ANIMATING_OUT && animationState != SHOWING_PERSISTENT_DOT) {
// events can only be scheduled if a higher priority or no other event is in progress
if (DEBUG) {
Log.d(TAG, "scheduling event $event")
@@ -143,7 +139,7 @@ open class SystemStatusAnimationScheduler @Inject constructor(
}
}
public fun isTooEarly(): Boolean {
fun isTooEarly(): Boolean {
return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
}

View File

@@ -64,14 +64,14 @@ class StatusBarSystemEventAnimator(
override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator {
animatedView.translationX = translationXOut.toFloat()
val moveIn = ValueAnimator.ofFloat(1f, 0f).setDuration(28.frames)
moveIn.startDelay = 2.frames
val moveIn = ValueAnimator.ofFloat(1f, 0f).setDuration(23.frames)
moveIn.startDelay = 7.frames
moveIn.interpolator = STATUS_BAR_X_MOVE_IN
moveIn.addUpdateListener { animation: ValueAnimator ->
animatedView.translationX = translationXOut * animation.animatedValue as Float
}
val alphaIn = ValueAnimator.ofFloat(0f, 1f).setDuration(10.frames)
alphaIn.startDelay = 4.frames
val alphaIn = ValueAnimator.ofFloat(0f, 1f).setDuration(5.frames)
alphaIn.startDelay = 11.frames
alphaIn.interpolator = null
alphaIn.addUpdateListener { animation: ValueAnimator ->
animatedView.alpha = animation.animatedValue as Float