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