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