• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.charging
18 
19 import android.content.Context
20 import android.content.res.Configuration
21 import android.graphics.PixelFormat
22 import android.graphics.PointF
23 import android.os.SystemProperties
24 import android.util.DisplayMetrics
25 import android.view.View
26 import android.view.WindowManager
27 import com.android.internal.annotations.VisibleForTesting
28 import com.android.internal.logging.UiEvent
29 import com.android.internal.logging.UiEventLogger
30 import com.android.settingslib.Utils
31 import com.android.systemui.dagger.SysUISingleton
32 import com.android.systemui.statusbar.FeatureFlags
33 import com.android.systemui.statusbar.commandline.Command
34 import com.android.systemui.statusbar.commandline.CommandRegistry
35 import com.android.systemui.statusbar.policy.BatteryController
36 import com.android.systemui.statusbar.policy.ConfigurationController
37 import com.android.systemui.util.leak.RotationUtils
38 import com.android.systemui.R
39 import com.android.systemui.util.time.SystemClock
40 import java.io.PrintWriter
41 import javax.inject.Inject
42 import kotlin.math.min
43 import kotlin.math.pow
44 
45 private const val MAX_DEBOUNCE_LEVEL = 3
46 private const val BASE_DEBOUNCE_TIME = 2000
47 
48 /***
49  * Controls the ripple effect that shows when wired charging begins.
50  * The ripple uses the accent color of the current theme.
51  */
52 @SysUISingleton
53 class WiredChargingRippleController @Inject constructor(
54     commandRegistry: CommandRegistry,
55     batteryController: BatteryController,
56     configurationController: ConfigurationController,
57     featureFlags: FeatureFlags,
58     private val context: Context,
59     private val windowManager: WindowManager,
60     private val systemClock: SystemClock,
61     private val uiEventLogger: UiEventLogger
62 ) {
63     private var pluggedIn: Boolean? = null
64     private val rippleEnabled: Boolean = featureFlags.isChargingRippleEnabled &&
65             !SystemProperties.getBoolean("persist.debug.suppress-charging-ripple", false)
66     private var normalizedPortPosX: Float = context.resources.getFloat(
67             R.dimen.physical_charger_port_location_normalized_x)
68     private var normalizedPortPosY: Float = context.resources.getFloat(
69             R.dimen.physical_charger_port_location_normalized_y)
<lambda>null70     private val windowLayoutParams = WindowManager.LayoutParams().apply {
71         width = WindowManager.LayoutParams.MATCH_PARENT
72         height = WindowManager.LayoutParams.MATCH_PARENT
73         layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
74         format = PixelFormat.TRANSLUCENT
75         type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
76         fitInsetsTypes = 0 // Ignore insets from all system bars
77         title = "Wired Charging Animation"
78         flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
79                 or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
80         setTrustedOverlay()
81     }
82     private var lastTriggerTime: Long? = null
83     private var debounceLevel = 0
84 
85     @VisibleForTesting
86     var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null)
87 
88     init {
89         pluggedIn = batteryController.isPluggedIn
90         val batteryStateChangeCallback = object : BatteryController.BatteryStateChangeCallback {
onBatteryLevelChangednull91             override fun onBatteryLevelChanged(
92                 level: Int,
93                 nowPluggedIn: Boolean,
94                 charging: Boolean
95             ) {
96                 // Suppresses the ripple when it's disabled, or when the state change comes
97                 // from wireless charging.
98                 if (!rippleEnabled || batteryController.isPluggedInWireless) {
99                     return
100                 }
101                 val wasPluggedIn = pluggedIn
102                 pluggedIn = nowPluggedIn
103                 if ((wasPluggedIn == null || !wasPluggedIn) && nowPluggedIn) {
104                     startRippleWithDebounce()
105                 }
106             }
107         }
108         batteryController.addCallback(batteryStateChangeCallback)
109 
110         val configurationChangedListener = object : ConfigurationController.ConfigurationListener {
onUiModeChangednull111             override fun onUiModeChanged() {
112                 updateRippleColor()
113             }
onThemeChangednull114             override fun onThemeChanged() {
115                 updateRippleColor()
116             }
onOverlayChangednull117             override fun onOverlayChanged() {
118                 updateRippleColor()
119             }
120 
onConfigChangednull121             override fun onConfigChanged(newConfig: Configuration?) {
122                 normalizedPortPosX = context.resources.getFloat(
123                         R.dimen.physical_charger_port_location_normalized_x)
124                 normalizedPortPosY = context.resources.getFloat(
125                         R.dimen.physical_charger_port_location_normalized_y)
126             }
127         }
128         configurationController.addCallback(configurationChangedListener)
129 
<lambda>null130         commandRegistry.registerCommand("charging-ripple") { ChargingRippleCommand() }
131         updateRippleColor()
132     }
133 
134     // Lazily debounce ripple to avoid triggering ripple constantly (e.g. from flaky chargers).
startRippleWithDebouncenull135     internal fun startRippleWithDebounce() {
136         val now = systemClock.elapsedRealtime()
137         // Debounce wait time = 2 ^ debounce level
138         if (lastTriggerTime == null ||
139                 (now - lastTriggerTime!!) > BASE_DEBOUNCE_TIME * (2.0.pow(debounceLevel))) {
140             // Not waiting for debounce. Start ripple.
141             startRipple()
142             debounceLevel = 0
143         } else {
144             // Still waiting for debounce. Ignore ripple and bump debounce level.
145             debounceLevel = min(MAX_DEBOUNCE_LEVEL, debounceLevel + 1)
146         }
147         lastTriggerTime = now
148     }
149 
startRipplenull150     fun startRipple() {
151         if (!rippleEnabled || rippleView.rippleInProgress || rippleView.parent != null) {
152             // Skip if ripple is still playing, or not playing but already added the parent
153             // (which might happen just before the animation starts or right after
154             // the animation ends.)
155             return
156         }
157         windowLayoutParams.packageName = context.opPackageName
158         rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
159             override fun onViewDetachedFromWindow(view: View?) {}
160 
161             override fun onViewAttachedToWindow(view: View?) {
162                 layoutRipple()
163                 rippleView.startRipple(Runnable {
164                     windowManager.removeView(rippleView)
165                 })
166                 rippleView.removeOnAttachStateChangeListener(this)
167             }
168         })
169         windowManager.addView(rippleView, windowLayoutParams)
170         uiEventLogger.log(WiredChargingRippleEvent.CHARGING_RIPPLE_PLAYED)
171     }
172 
layoutRipplenull173     private fun layoutRipple() {
174         val displayMetrics = DisplayMetrics()
175         context.display.getRealMetrics(displayMetrics)
176         val width = displayMetrics.widthPixels
177         val height = displayMetrics.heightPixels
178         rippleView.radius = Integer.max(width, height).toFloat()
179         rippleView.origin = when (RotationUtils.getRotation(context)) {
180             RotationUtils.ROTATION_LANDSCAPE -> {
181                 PointF(width * normalizedPortPosY, height * (1 - normalizedPortPosX))
182             }
183             RotationUtils.ROTATION_UPSIDE_DOWN -> {
184                 PointF(width * (1 - normalizedPortPosX), height * (1 - normalizedPortPosY))
185             }
186             RotationUtils.ROTATION_SEASCAPE -> {
187                 PointF(width * (1 - normalizedPortPosY), height * normalizedPortPosX)
188             }
189             else -> {
190                 // ROTATION_NONE
191                 PointF(width * normalizedPortPosX, height * normalizedPortPosY)
192             }
193         }
194     }
195 
updateRippleColornull196     private fun updateRippleColor() {
197         rippleView.setColor(
198                 Utils.getColorAttr(context, android.R.attr.colorAccent).defaultColor)
199     }
200 
201     inner class ChargingRippleCommand : Command {
executenull202         override fun execute(pw: PrintWriter, args: List<String>) {
203             startRipple()
204         }
205 
helpnull206         override fun help(pw: PrintWriter) {
207             pw.println("Usage: adb shell cmd statusbar charging-ripple")
208         }
209     }
210 
211     enum class WiredChargingRippleEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
212         @UiEvent(doc = "Wired charging ripple effect played")
213         CHARGING_RIPPLE_PLAYED(829);
214 
getIdnull215         override fun getId() = _id
216     }
217 }
218