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.charging
18 
19 import android.content.Context
20 import android.content.res.Configuration
21 import android.graphics.PixelFormat
22 import android.os.SystemProperties
23 import android.view.Surface
24 import android.view.View
25 import android.view.WindowManager
26 import com.android.internal.annotations.VisibleForTesting
27 import com.android.internal.logging.UiEvent
28 import com.android.internal.logging.UiEventLogger
29 import com.android.settingslib.Utils
30 import com.android.systemui.res.R
31 import com.android.systemui.dagger.SysUISingleton
32 import com.android.systemui.flags.FeatureFlags
33 import com.android.systemui.flags.Flags
34 import com.android.systemui.surfaceeffects.ripple.RippleView
35 import com.android.systemui.statusbar.commandline.Command
36 import com.android.systemui.statusbar.commandline.CommandRegistry
37 import com.android.systemui.statusbar.policy.BatteryController
38 import com.android.systemui.statusbar.policy.ConfigurationController
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     private val batteryController: BatteryController,
56     private val 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 = false
64     private val rippleEnabled: Boolean = featureFlags.isEnabled(Flags.CHARGING_RIPPLE) &&
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_KEYGUARD_DIALOG
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
<lambda>null86     var rippleView: RippleView = RippleView(context, attrs = null).also { it.setupShader() }
87 
88     init {
89         pluggedIn = batteryController.isPluggedIn
<lambda>null90         commandRegistry.registerCommand("charging-ripple") { ChargingRippleCommand() }
91         updateRippleColor()
92     }
93 
registerCallbacksnull94     fun registerCallbacks() {
95         val batteryStateChangeCallback = object : BatteryController.BatteryStateChangeCallback {
96             override fun onBatteryLevelChanged(
97                 level: Int,
98                 nowPluggedIn: Boolean,
99                 charging: Boolean
100             ) {
101                 // Suppresses the ripple when the state change comes from wireless charging or
102                 // its dock.
103                 if (batteryController.isPluggedInWireless ||
104                         batteryController.isChargingSourceDock) {
105                     return
106                 }
107 
108                 if (!pluggedIn && nowPluggedIn) {
109                     startRippleWithDebounce()
110                 }
111                 pluggedIn = nowPluggedIn
112             }
113         }
114         batteryController.addCallback(batteryStateChangeCallback)
115 
116         val configurationChangedListener = object : ConfigurationController.ConfigurationListener {
117             override fun onUiModeChanged() {
118                 updateRippleColor()
119             }
120             override fun onThemeChanged() {
121                 updateRippleColor()
122             }
123 
124             override fun onConfigChanged(newConfig: Configuration?) {
125                 normalizedPortPosX = context.resources.getFloat(
126                         R.dimen.physical_charger_port_location_normalized_x)
127                 normalizedPortPosY = context.resources.getFloat(
128                         R.dimen.physical_charger_port_location_normalized_y)
129             }
130         }
131         configurationController.addCallback(configurationChangedListener)
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 (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 bounds = windowManager.currentWindowMetrics.bounds
175         val width = bounds.width()
176         val height = bounds.height()
177         val maxDiameter = Integer.max(width, height) * 2f
178         rippleView.setMaxSize(maxDiameter, maxDiameter)
179         when (context.display?.rotation) {
180             Surface.ROTATION_0 -> {
181                 rippleView.setCenter(
182                         width * normalizedPortPosX, height * normalizedPortPosY)
183             }
184             Surface.ROTATION_90 -> {
185                 rippleView.setCenter(
186                         width * normalizedPortPosY, height * (1 - normalizedPortPosX))
187             }
188             Surface.ROTATION_180 -> {
189                 rippleView.setCenter(
190                         width * (1 - normalizedPortPosX), height * (1 - normalizedPortPosY))
191             }
192             Surface.ROTATION_270 -> {
193                 rippleView.setCenter(
194                         width * (1 - normalizedPortPosY), height * normalizedPortPosX)
195             }
196         }
197     }
198 
updateRippleColornull199     private fun updateRippleColor() {
200         rippleView.setColor(Utils.getColorAttr(context, android.R.attr.colorAccent).defaultColor)
201     }
202 
203     inner class ChargingRippleCommand : Command {
executenull204         override fun execute(pw: PrintWriter, args: List<String>) {
205             startRipple()
206         }
207 
helpnull208         override fun help(pw: PrintWriter) {
209             pw.println("Usage: adb shell cmd statusbar charging-ripple")
210         }
211     }
212 
213     enum class WiredChargingRippleEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
214         @UiEvent(doc = "Wired charging ripple effect played")
215         CHARGING_RIPPLE_PLAYED(829);
216 
getIdnull217         override fun getId() = _id
218     }
219 }
220