Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ef198391 authored by Shan Huang's avatar Shan Huang Committed by Android (Google) Code Review
Browse files

Merge "Show ripple in a window when phone is unlocked." into sc-dev

parents 454771de 299cdbad
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -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()
+36 −32
Original line number Diff line number Diff line
@@ -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() }
        updateRippleColor()
    }

    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 {
    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?) {
                (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay).add(rippleView)
                layoutRippleView()
                viewHost.removeOnAttachStateChangeListener(this)
                layoutRipple()
                rippleView.startRipple(Runnable {
                    mWM.removeView(rippleView)
                })
                rippleView.removeOnAttachStateChangeListener(this)
            }
        })

        updateRippleColor()
        mWM.addView(rippleView, windowLayoutParams)
    }

    private fun layoutRippleView() {
        // Overlays are not auto measured and laid out so we do it manually here.
    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) {
+0 −1
Original line number Diff line number Diff line
@@ -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(
+25 −34
Original line number Diff line number Diff line
@@ -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