Merge "Show ripple in a window when phone is unlocked." into sc-dev
This commit is contained in:
@@ -33,11 +33,11 @@ private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f
|
||||
* Expanding ripple effect that shows when charging begins.
|
||||
*/
|
||||
class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
|
||||
private var rippleInProgress: Boolean = false
|
||||
private val rippleShader = RippleShader()
|
||||
private val defaultColor: Int = 0xffffffff.toInt()
|
||||
private val ripplePaint = Paint()
|
||||
|
||||
var rippleInProgress: Boolean = false
|
||||
var radius: Float = 0.0f
|
||||
set(value) { rippleShader.radius = value }
|
||||
var origin: PointF = PointF()
|
||||
@@ -62,7 +62,8 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context
|
||||
super.onAttachedToWindow()
|
||||
}
|
||||
|
||||
fun startRipple() {
|
||||
@JvmOverloads
|
||||
fun startRipple(onAnimationEnd: Runnable? = null) {
|
||||
if (rippleInProgress) {
|
||||
return // Ignore if ripple effect is already playing
|
||||
}
|
||||
@@ -80,6 +81,7 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
rippleInProgress = false
|
||||
visibility = View.GONE
|
||||
onAnimationEnd?.run()
|
||||
}
|
||||
})
|
||||
animator.start()
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
package com.android.systemui.statusbar.charging
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.PointF
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
import android.view.ViewGroupOverlay
|
||||
import android.view.WindowManager
|
||||
import com.android.internal.annotations.VisibleForTesting
|
||||
import com.android.settingslib.Utils
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
@@ -30,9 +30,7 @@ import com.android.systemui.statusbar.commandline.Command
|
||||
import com.android.systemui.statusbar.commandline.CommandRegistry
|
||||
import com.android.systemui.statusbar.policy.BatteryController
|
||||
import com.android.systemui.statusbar.policy.ConfigurationController
|
||||
import com.android.systemui.statusbar.policy.KeyguardStateController
|
||||
import java.io.PrintWriter
|
||||
import java.lang.Integer.max
|
||||
import javax.inject.Inject
|
||||
|
||||
/***
|
||||
@@ -45,11 +43,22 @@ class WiredChargingRippleController @Inject constructor(
|
||||
batteryController: BatteryController,
|
||||
configurationController: ConfigurationController,
|
||||
featureFlags: FeatureFlags,
|
||||
private val context: Context,
|
||||
private val keyguardStateController: KeyguardStateController
|
||||
private val context: Context
|
||||
) {
|
||||
private var charging: Boolean? = null
|
||||
private val rippleEnabled: Boolean = featureFlags.isChargingRippleEnabled
|
||||
private val windowLayoutParams = WindowManager.LayoutParams().apply {
|
||||
width = WindowManager.LayoutParams.MATCH_PARENT
|
||||
height = WindowManager.LayoutParams.MATCH_PARENT
|
||||
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
|
||||
format = PixelFormat.TRANSLUCENT
|
||||
type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
|
||||
fitInsetsTypes = 0 // Ignore insets from all system bars
|
||||
title = "Wired Charging Animation"
|
||||
flags = (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null)
|
||||
|
||||
@@ -68,8 +77,8 @@ class WiredChargingRippleController @Inject constructor(
|
||||
val wasCharging = charging
|
||||
charging = nowCharging
|
||||
// Only triggers when the keyguard is active and the device is just plugged in.
|
||||
if (wasCharging == false && nowCharging && keyguardStateController.isShowing) {
|
||||
rippleView.startRipple()
|
||||
if ((wasCharging == null || !wasCharging) && nowCharging) {
|
||||
startRipple()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,46 +94,41 @@ class WiredChargingRippleController @Inject constructor(
|
||||
override fun onOverlayChanged() {
|
||||
updateRippleColor()
|
||||
}
|
||||
override fun onConfigChanged(newConfig: Configuration?) {
|
||||
layoutRippleView()
|
||||
}
|
||||
}
|
||||
configurationController.addCallback(configurationChangedListener)
|
||||
|
||||
commandRegistry.registerCommand("charging-ripple") { ChargingRippleCommand() }
|
||||
}
|
||||
|
||||
fun setViewHost(viewHost: View) {
|
||||
// Add the ripple view as an overlay of the root view so that it always
|
||||
// shows on top.
|
||||
viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewDetachedFromWindow(view: View?) {}
|
||||
|
||||
override fun onViewAttachedToWindow(view: View?) {
|
||||
(viewHost.viewRootImpl.view.overlay as ViewGroupOverlay).add(rippleView)
|
||||
layoutRippleView()
|
||||
viewHost.removeOnAttachStateChangeListener(this)
|
||||
}
|
||||
})
|
||||
|
||||
updateRippleColor()
|
||||
}
|
||||
|
||||
private fun layoutRippleView() {
|
||||
// Overlays are not auto measured and laid out so we do it manually here.
|
||||
fun startRipple() {
|
||||
if (rippleView.rippleInProgress) {
|
||||
return
|
||||
}
|
||||
val mWM = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
windowLayoutParams.packageName = context.opPackageName
|
||||
rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewDetachedFromWindow(view: View?) {}
|
||||
|
||||
override fun onViewAttachedToWindow(view: View?) {
|
||||
layoutRipple()
|
||||
rippleView.startRipple(Runnable {
|
||||
mWM.removeView(rippleView)
|
||||
})
|
||||
rippleView.removeOnAttachStateChangeListener(this)
|
||||
}
|
||||
})
|
||||
mWM.addView(rippleView, windowLayoutParams)
|
||||
}
|
||||
|
||||
private fun layoutRipple() {
|
||||
// TODO(shanh): Set origin base on phone orientation.
|
||||
val displayMetrics = DisplayMetrics()
|
||||
context.display.getRealMetrics(displayMetrics)
|
||||
val width = displayMetrics.widthPixels
|
||||
val height = displayMetrics.heightPixels
|
||||
if (width != rippleView.width || height != rippleView.height) {
|
||||
rippleView.apply {
|
||||
measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))
|
||||
layout(0, 0, width, height)
|
||||
origin = PointF(width / 2f, height.toFloat())
|
||||
radius = max(width, height).toFloat()
|
||||
}
|
||||
}
|
||||
rippleView.origin = PointF(width / 2f, height.toFloat())
|
||||
rippleView.radius = Integer.max(width, height).toFloat()
|
||||
}
|
||||
|
||||
private fun updateRippleColor() {
|
||||
@@ -134,7 +138,7 @@ class WiredChargingRippleController @Inject constructor(
|
||||
|
||||
inner class ChargingRippleCommand : Command {
|
||||
override fun execute(pw: PrintWriter, args: List<String>) {
|
||||
rippleView.startRipple()
|
||||
startRipple()
|
||||
}
|
||||
|
||||
override fun help(pw: PrintWriter) {
|
||||
|
||||
@@ -1228,7 +1228,6 @@ public class StatusBar extends SystemUI implements DemoMode,
|
||||
mScrimController.attachViews(scrimBehind, scrimInFront, scrimForBubble);
|
||||
|
||||
mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim);
|
||||
mChargingRippleAnimationController.setViewHost(mNotificationShadeWindowView);
|
||||
updateLightRevealScrimVisibility();
|
||||
|
||||
mNotificationPanelViewController.initDependencies(
|
||||
|
||||
@@ -16,17 +16,17 @@
|
||||
|
||||
package com.android.systemui.statusbar.charging
|
||||
|
||||
import android.content.Context
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.view.View
|
||||
import android.view.ViewGroupOverlay
|
||||
import android.view.ViewRootImpl
|
||||
import android.view.WindowManager
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.statusbar.FeatureFlags
|
||||
import com.android.systemui.statusbar.commandline.CommandRegistry
|
||||
import com.android.systemui.statusbar.policy.BatteryController
|
||||
import com.android.systemui.statusbar.policy.ConfigurationController
|
||||
import com.android.systemui.statusbar.policy.KeyguardStateController
|
||||
import com.android.systemui.util.mockito.capture
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -34,7 +34,8 @@ import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.eq
|
||||
import org.mockito.Mockito.reset
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
@@ -47,55 +48,45 @@ class WiredChargingRippleControllerTest : SysuiTestCase() {
|
||||
@Mock private lateinit var batteryController: BatteryController
|
||||
@Mock private lateinit var featureFlags: FeatureFlags
|
||||
@Mock private lateinit var configurationController: ConfigurationController
|
||||
@Mock private lateinit var keyguardStateController: KeyguardStateController
|
||||
@Mock private lateinit var rippleView: ChargingRippleView
|
||||
@Mock private lateinit var viewHost: View
|
||||
@Mock private lateinit var viewHostRootImpl: ViewRootImpl
|
||||
@Mock private lateinit var viewGroupOverlay: ViewGroupOverlay
|
||||
@Mock private lateinit var windowManager: WindowManager
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
`when`(viewHost.viewRootImpl).thenReturn(viewHostRootImpl)
|
||||
`when`(viewHostRootImpl.view).thenReturn(viewHost)
|
||||
`when`(viewHost.overlay).thenReturn(viewGroupOverlay)
|
||||
`when`(featureFlags.isChargingRippleEnabled).thenReturn(true)
|
||||
`when`(keyguardStateController.isShowing).thenReturn(true)
|
||||
controller = WiredChargingRippleController(
|
||||
commandRegistry, batteryController, configurationController,
|
||||
featureFlags, context, keyguardStateController)
|
||||
featureFlags, context)
|
||||
controller.rippleView = rippleView // Replace the real ripple view with a mock instance
|
||||
controller.setViewHost(viewHost)
|
||||
context.addMockSystemService(Context.WINDOW_SERVICE, windowManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetRippleViewAsOverlay() {
|
||||
val listenerCaptor = ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
|
||||
verify(viewHost).addOnAttachStateChangeListener(listenerCaptor.capture())
|
||||
|
||||
// Fake attach to window
|
||||
listenerCaptor.value.onViewAttachedToWindow(viewHost)
|
||||
verify(viewGroupOverlay).add(rippleView)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTriggerRipple() {
|
||||
fun testTriggerRipple_UnlockedState() {
|
||||
val captor = ArgumentCaptor
|
||||
.forClass(BatteryController.BatteryStateChangeCallback::class.java)
|
||||
verify(batteryController).addCallback(captor.capture())
|
||||
|
||||
val unusedBatteryLevel = 0
|
||||
// Verify ripple added to window manager.
|
||||
captor.value.onBatteryLevelChanged(
|
||||
unusedBatteryLevel,
|
||||
false /* plugged in */,
|
||||
false /* charging */)
|
||||
verify(rippleView, never()).startRipple()
|
||||
|
||||
captor.value.onBatteryLevelChanged(
|
||||
unusedBatteryLevel,
|
||||
0 /* unusedBatteryLevel */,
|
||||
false /* plugged in */,
|
||||
true /* charging */)
|
||||
verify(rippleView).startRipple()
|
||||
val attachListenerCaptor =
|
||||
ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
|
||||
verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture())
|
||||
verify(windowManager).addView(eq(rippleView), any<WindowManager.LayoutParams>())
|
||||
|
||||
// Verify ripple started
|
||||
val runnableCaptor =
|
||||
ArgumentCaptor.forClass(Runnable::class.java)
|
||||
attachListenerCaptor.value.onViewAttachedToWindow(rippleView)
|
||||
verify(rippleView).startRipple(runnableCaptor.capture())
|
||||
|
||||
// Verify ripple removed
|
||||
runnableCaptor.value.run()
|
||||
verify(windowManager).removeView(rippleView)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user