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.
|
* Expanding ripple effect that shows when charging begins.
|
||||||
*/
|
*/
|
||||||
class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
|
class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
|
||||||
private var rippleInProgress: Boolean = false
|
|
||||||
private val rippleShader = RippleShader()
|
private val rippleShader = RippleShader()
|
||||||
private val defaultColor: Int = 0xffffffff.toInt()
|
private val defaultColor: Int = 0xffffffff.toInt()
|
||||||
private val ripplePaint = Paint()
|
private val ripplePaint = Paint()
|
||||||
|
|
||||||
|
var rippleInProgress: Boolean = false
|
||||||
var radius: Float = 0.0f
|
var radius: Float = 0.0f
|
||||||
set(value) { rippleShader.radius = value }
|
set(value) { rippleShader.radius = value }
|
||||||
var origin: PointF = PointF()
|
var origin: PointF = PointF()
|
||||||
@@ -62,7 +62,8 @@ class ChargingRippleView(context: Context?, attrs: AttributeSet?) : View(context
|
|||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startRipple() {
|
@JvmOverloads
|
||||||
|
fun startRipple(onAnimationEnd: Runnable? = null) {
|
||||||
if (rippleInProgress) {
|
if (rippleInProgress) {
|
||||||
return // Ignore if ripple effect is already playing
|
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?) {
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
rippleInProgress = false
|
rippleInProgress = false
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
|
onAnimationEnd?.run()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
animator.start()
|
animator.start()
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
package com.android.systemui.statusbar.charging
|
package com.android.systemui.statusbar.charging
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.graphics.PixelFormat
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroupOverlay
|
import android.view.WindowManager
|
||||||
import com.android.internal.annotations.VisibleForTesting
|
import com.android.internal.annotations.VisibleForTesting
|
||||||
import com.android.settingslib.Utils
|
import com.android.settingslib.Utils
|
||||||
import com.android.systemui.dagger.SysUISingleton
|
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.commandline.CommandRegistry
|
||||||
import com.android.systemui.statusbar.policy.BatteryController
|
import com.android.systemui.statusbar.policy.BatteryController
|
||||||
import com.android.systemui.statusbar.policy.ConfigurationController
|
import com.android.systemui.statusbar.policy.ConfigurationController
|
||||||
import com.android.systemui.statusbar.policy.KeyguardStateController
|
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import java.lang.Integer.max
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/***
|
/***
|
||||||
@@ -45,11 +43,22 @@ class WiredChargingRippleController @Inject constructor(
|
|||||||
batteryController: BatteryController,
|
batteryController: BatteryController,
|
||||||
configurationController: ConfigurationController,
|
configurationController: ConfigurationController,
|
||||||
featureFlags: FeatureFlags,
|
featureFlags: FeatureFlags,
|
||||||
private val context: Context,
|
private val context: Context
|
||||||
private val keyguardStateController: KeyguardStateController
|
|
||||||
) {
|
) {
|
||||||
private var charging: Boolean? = null
|
private var charging: Boolean? = null
|
||||||
private val rippleEnabled: Boolean = featureFlags.isChargingRippleEnabled
|
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
|
@VisibleForTesting
|
||||||
var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null)
|
var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null)
|
||||||
|
|
||||||
@@ -68,8 +77,8 @@ class WiredChargingRippleController @Inject constructor(
|
|||||||
val wasCharging = charging
|
val wasCharging = charging
|
||||||
charging = nowCharging
|
charging = nowCharging
|
||||||
// Only triggers when the keyguard is active and the device is just plugged in.
|
// Only triggers when the keyguard is active and the device is just plugged in.
|
||||||
if (wasCharging == false && nowCharging && keyguardStateController.isShowing) {
|
if ((wasCharging == null || !wasCharging) && nowCharging) {
|
||||||
rippleView.startRipple()
|
startRipple()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,46 +94,41 @@ class WiredChargingRippleController @Inject constructor(
|
|||||||
override fun onOverlayChanged() {
|
override fun onOverlayChanged() {
|
||||||
updateRippleColor()
|
updateRippleColor()
|
||||||
}
|
}
|
||||||
override fun onConfigChanged(newConfig: Configuration?) {
|
|
||||||
layoutRippleView()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
configurationController.addCallback(configurationChangedListener)
|
configurationController.addCallback(configurationChangedListener)
|
||||||
|
|
||||||
commandRegistry.registerCommand("charging-ripple") { ChargingRippleCommand() }
|
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()
|
updateRippleColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun layoutRippleView() {
|
fun startRipple() {
|
||||||
// Overlays are not auto measured and laid out so we do it manually here.
|
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()
|
val displayMetrics = DisplayMetrics()
|
||||||
context.display.getRealMetrics(displayMetrics)
|
context.display.getRealMetrics(displayMetrics)
|
||||||
val width = displayMetrics.widthPixels
|
val width = displayMetrics.widthPixels
|
||||||
val height = displayMetrics.heightPixels
|
val height = displayMetrics.heightPixels
|
||||||
if (width != rippleView.width || height != rippleView.height) {
|
rippleView.origin = PointF(width / 2f, height.toFloat())
|
||||||
rippleView.apply {
|
rippleView.radius = Integer.max(width, height).toFloat()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateRippleColor() {
|
private fun updateRippleColor() {
|
||||||
@@ -134,7 +138,7 @@ class WiredChargingRippleController @Inject constructor(
|
|||||||
|
|
||||||
inner class ChargingRippleCommand : Command {
|
inner class ChargingRippleCommand : Command {
|
||||||
override fun execute(pw: PrintWriter, args: List<String>) {
|
override fun execute(pw: PrintWriter, args: List<String>) {
|
||||||
rippleView.startRipple()
|
startRipple()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun help(pw: PrintWriter) {
|
override fun help(pw: PrintWriter) {
|
||||||
|
|||||||
@@ -1228,7 +1228,6 @@ public class StatusBar extends SystemUI implements DemoMode,
|
|||||||
mScrimController.attachViews(scrimBehind, scrimInFront, scrimForBubble);
|
mScrimController.attachViews(scrimBehind, scrimInFront, scrimForBubble);
|
||||||
|
|
||||||
mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim);
|
mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim);
|
||||||
mChargingRippleAnimationController.setViewHost(mNotificationShadeWindowView);
|
|
||||||
updateLightRevealScrimVisibility();
|
updateLightRevealScrimVisibility();
|
||||||
|
|
||||||
mNotificationPanelViewController.initDependencies(
|
mNotificationPanelViewController.initDependencies(
|
||||||
|
|||||||
@@ -16,17 +16,17 @@
|
|||||||
|
|
||||||
package com.android.systemui.statusbar.charging
|
package com.android.systemui.statusbar.charging
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.testing.AndroidTestingRunner
|
import android.testing.AndroidTestingRunner
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroupOverlay
|
import android.view.WindowManager
|
||||||
import android.view.ViewRootImpl
|
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import com.android.systemui.SysuiTestCase
|
import com.android.systemui.SysuiTestCase
|
||||||
import com.android.systemui.statusbar.FeatureFlags
|
import com.android.systemui.statusbar.FeatureFlags
|
||||||
import com.android.systemui.statusbar.commandline.CommandRegistry
|
import com.android.systemui.statusbar.commandline.CommandRegistry
|
||||||
import com.android.systemui.statusbar.policy.BatteryController
|
import com.android.systemui.statusbar.policy.BatteryController
|
||||||
import com.android.systemui.statusbar.policy.ConfigurationController
|
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.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -34,7 +34,8 @@ import org.mockito.ArgumentCaptor
|
|||||||
import org.mockito.ArgumentMatchers
|
import org.mockito.ArgumentMatchers
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito.`when`
|
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.reset
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
@@ -47,55 +48,45 @@ class WiredChargingRippleControllerTest : SysuiTestCase() {
|
|||||||
@Mock private lateinit var batteryController: BatteryController
|
@Mock private lateinit var batteryController: BatteryController
|
||||||
@Mock private lateinit var featureFlags: FeatureFlags
|
@Mock private lateinit var featureFlags: FeatureFlags
|
||||||
@Mock private lateinit var configurationController: ConfigurationController
|
@Mock private lateinit var configurationController: ConfigurationController
|
||||||
@Mock private lateinit var keyguardStateController: KeyguardStateController
|
|
||||||
@Mock private lateinit var rippleView: ChargingRippleView
|
@Mock private lateinit var rippleView: ChargingRippleView
|
||||||
@Mock private lateinit var viewHost: View
|
@Mock private lateinit var windowManager: WindowManager
|
||||||
@Mock private lateinit var viewHostRootImpl: ViewRootImpl
|
|
||||||
@Mock private lateinit var viewGroupOverlay: ViewGroupOverlay
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
`when`(viewHost.viewRootImpl).thenReturn(viewHostRootImpl)
|
|
||||||
`when`(viewHostRootImpl.view).thenReturn(viewHost)
|
|
||||||
`when`(viewHost.overlay).thenReturn(viewGroupOverlay)
|
|
||||||
`when`(featureFlags.isChargingRippleEnabled).thenReturn(true)
|
`when`(featureFlags.isChargingRippleEnabled).thenReturn(true)
|
||||||
`when`(keyguardStateController.isShowing).thenReturn(true)
|
|
||||||
controller = WiredChargingRippleController(
|
controller = WiredChargingRippleController(
|
||||||
commandRegistry, batteryController, configurationController,
|
commandRegistry, batteryController, configurationController,
|
||||||
featureFlags, context, keyguardStateController)
|
featureFlags, context)
|
||||||
controller.rippleView = rippleView // Replace the real ripple view with a mock instance
|
controller.rippleView = rippleView // Replace the real ripple view with a mock instance
|
||||||
controller.setViewHost(viewHost)
|
context.addMockSystemService(Context.WINDOW_SERVICE, windowManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSetRippleViewAsOverlay() {
|
fun testTriggerRipple_UnlockedState() {
|
||||||
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() {
|
|
||||||
val captor = ArgumentCaptor
|
val captor = ArgumentCaptor
|
||||||
.forClass(BatteryController.BatteryStateChangeCallback::class.java)
|
.forClass(BatteryController.BatteryStateChangeCallback::class.java)
|
||||||
verify(batteryController).addCallback(captor.capture())
|
verify(batteryController).addCallback(captor.capture())
|
||||||
|
|
||||||
val unusedBatteryLevel = 0
|
// Verify ripple added to window manager.
|
||||||
captor.value.onBatteryLevelChanged(
|
captor.value.onBatteryLevelChanged(
|
||||||
unusedBatteryLevel,
|
0 /* unusedBatteryLevel */,
|
||||||
false /* plugged in */,
|
|
||||||
false /* charging */)
|
|
||||||
verify(rippleView, never()).startRipple()
|
|
||||||
|
|
||||||
captor.value.onBatteryLevelChanged(
|
|
||||||
unusedBatteryLevel,
|
|
||||||
false /* plugged in */,
|
false /* plugged in */,
|
||||||
true /* charging */)
|
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
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user