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

Commit c4b826bd authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

Multi-shade foundation - integration (5/5).

Early foundation for the multi-shade framework.

Integrates the basic framework into the current
NotificationShadeWindowView. Does not do any extra work to make it work.

The change is behind the DUAL_SHADE flag which is currently off.

https://drive.google.com/file/d/1nmbIQuwvWZ0VqnWHxqfcpxbZqapbHtmZ/view?usp=sharing

Bug: 272130181
Test: Manually tested end-to-end. When the flag is off, there is no
impact on system UI behaviour. When the flag is on, dual shade appears
on top of the normal shade. It even accepts input and the shades can be
expanded and collapsed.
Flag: DUAL_SHADE

Change-Id: I0fc00d05a1fc9451ca92a53638dc3cbc7a058459
parent ac1655ce
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright (C) 2023 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License.
  ~
  -->

<com.android.systemui.multishade.ui.view.MultiShadeView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
+7 −0
Original line number Diff line number Diff line
@@ -110,6 +110,13 @@
        android:clipChildren="false"
        android:clipToPadding="false" />

    <ViewStub
        android:id="@+id/multi_shade_stub"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:inflatedId="@+id/multi_shade"
        android:layout="@layout/multi_shade" />

    <com.android.systemui.biometrics.AuthRippleView
        android:id="@+id/auth_ripple"
        android:layout_width="match_parent"
+71 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.android.systemui.multishade.ui.view

import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.compose.ComposeFacade
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.util.time.SystemClock
import kotlinx.coroutines.launch

/**
 * View that hosts the multi-shade system and acts as glue between legacy code and the
 * implementation.
 */
class MultiShadeView(
    context: Context,
    attrs: AttributeSet?,
) :
    FrameLayout(
        context,
        attrs,
    ) {

    fun init(
        interactor: MultiShadeInteractor,
        clock: SystemClock,
    ) {
        repeatWhenAttached {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.CREATED) {
                    addView(
                        ComposeFacade.createMultiShadeView(
                            context = context,
                            viewModel =
                                MultiShadeViewModel(
                                    viewModelScope = this,
                                    interactor = interactor,
                                ),
                            clock = clock,
                        )
                    )
                }

                // Here when destroyed.
                removeAllViews()
            }
        }
    }
}
+23 −4
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import android.app.StatusBarManager;
import android.media.AudioManager;
import android.media.session.MediaSessionLegacyHelper;
import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;
import android.view.GestureDetector;
import android.view.InputDevice;
@@ -31,6 +30,7 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;

import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.AuthKeyguardMessageArea;
@@ -39,8 +39,10 @@ import com.android.keyguard.dagger.KeyguardBouncerComponent;
import com.android.systemui.R;
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.compose.ComposeFacade;
import com.android.systemui.dock.DockManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
@@ -49,6 +51,8 @@ import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.binder.KeyguardBouncerViewBinder;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel;
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel;
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.multishade.ui.view.MultiShadeView;
import com.android.systemui.statusbar.DragDownHelper;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.NotificationInsetsController;
@@ -63,11 +67,13 @@ import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
import com.android.systemui.util.time.SystemClock;

import java.io.PrintWriter;
import java.util.function.Consumer;

import javax.inject.Inject;
import javax.inject.Provider;

/**
 * Controller for {@link NotificationShadeWindowView}.
@@ -115,6 +121,7 @@ public class NotificationShadeWindowViewController {
                mIsOcclusionTransitionRunning =
                    step.getTransitionState() == TransitionState.RUNNING;
            };
    private final SystemClock mClock;

    @Inject
    public NotificationShadeWindowViewController(
@@ -142,7 +149,9 @@ public class NotificationShadeWindowViewController {
            UdfpsOverlayInteractor udfpsOverlayInteractor,
            KeyguardTransitionInteractor keyguardTransitionInteractor,
            PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
            FeatureFlags featureFlags) {
            FeatureFlags featureFlags,
            Provider<MultiShadeInteractor> multiShadeInteractorProvider,
            SystemClock clock) {
        mLockscreenShadeTransitionController = transitionController;
        mFalsingCollector = falsingCollector;
        mStatusBarStateController = statusBarStateController;
@@ -175,6 +184,16 @@ public class NotificationShadeWindowViewController {

        collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(),
                mLockscreenToDreamingTransition);

        mClock = clock;
        if (ComposeFacade.INSTANCE.isComposeAvailable()
                && featureFlags.isEnabled(Flags.DUAL_SHADE)) {
            final ViewStub multiShadeViewStub = mView.findViewById(R.id.multi_shade_stub);
            if (multiShadeViewStub != null) {
                final MultiShadeView multiShadeView = (MultiShadeView) multiShadeViewStub.inflate();
                multiShadeView.init(multiShadeInteractorProvider.get(), clock);
            }
        }
    }

    /**
@@ -269,7 +288,7 @@ public class NotificationShadeWindowViewController {
                mLockIconViewController.onTouchEvent(
                        ev,
                        () -> mService.wakeUpIfDozing(
                                SystemClock.uptimeMillis(),
                                mClock.uptimeMillis(),
                                mView,
                                "LOCK_ICON_TOUCH",
                                PowerManager.WAKE_REASON_GESTURE)
@@ -453,7 +472,7 @@ public class NotificationShadeWindowViewController {

    public void cancelCurrentTouch() {
        if (mTouchActive) {
            final long now = SystemClock.uptimeMillis();
            final long now = mClock.uptimeMillis();
            final MotionEvent event;
            if (mIsTrackpadGestureBackEnabled) {
                event = MotionEvent.obtain(mDownEvent);
+154 −112
Original line number Diff line number Diff line
@@ -37,6 +37,9 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInterac
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
import com.android.systemui.multishade.data.repository.MultiShadeRepository
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.NotificationInsetsController
@@ -50,8 +53,12 @@ import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.window.StatusBarWindowStateController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -65,10 +72,12 @@ import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
class NotificationShadeWindowViewControllerTest : SysuiTestCase() {

    @Mock private lateinit var view: NotificationShadeWindowView
    @Mock private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController
    @Mock private lateinit var centralSurfaces: CentralSurfaces
@@ -102,6 +111,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {

    private lateinit var underTest: NotificationShadeWindowViewController

    private lateinit var testScope: TestScope

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
@@ -115,8 +126,12 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
        whenever(keyguardTransitionInteractor.lockscreenToDreamingTransition)
            .thenReturn(emptyFlow<TransitionStep>())

        val featureFlags = FakeFeatureFlags();
        val featureFlags = FakeFeatureFlags()
        featureFlags.set(Flags.TRACKPAD_GESTURE_BACK, false)
        featureFlags.set(Flags.DUAL_SHADE, false)

        val inputProxy = MultiShadeInputProxy()
        testScope = TestScope()
        underTest =
            NotificationShadeWindowViewController(
                lockscreenShadeTransitionController,
@@ -144,6 +159,18 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
                keyguardTransitionInteractor,
                primaryBouncerToGoneTransitionViewModel,
                featureFlags,
                {
                    MultiShadeInteractor(
                        applicationScope = testScope.backgroundScope,
                        repository =
                            MultiShadeRepository(
                                applicationContext = context,
                                inputProxy = inputProxy,
                            ),
                        inputProxy = inputProxy,
                    )
                },
                FakeSystemClock(),
            )
        underTest.setupExpandedStatusBar()

@@ -156,16 +183,18 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
    // tests need to be added to test the rest of handleDispatchTouchEvent.

    @Test
    fun handleDispatchTouchEvent_nullStatusBarViewController_returnsFalse() {
    fun handleDispatchTouchEvent_nullStatusBarViewController_returnsFalse() =
        testScope.runTest {
            underTest.setStatusBarViewController(null)

        val returnVal = interactionEventHandler.handleDispatchTouchEvent(downEv)
            val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)

            assertThat(returnVal).isFalse()
        }

    @Test
    fun handleDispatchTouchEvent_downTouchBelowView_sendsTouchToSb() {
    fun handleDispatchTouchEvent_downTouchBelowView_sendsTouchToSb() =
        testScope.runTest {
            underTest.setStatusBarViewController(phoneStatusBarViewController)
            val ev = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0)
            whenever(phoneStatusBarViewController.sendTouchToView(ev)).thenReturn(true)
@@ -177,13 +206,15 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
        }

    @Test
    fun handleDispatchTouchEvent_downTouchBelowViewThenAnotherTouch_sendsTouchToSb() {
    fun handleDispatchTouchEvent_downTouchBelowViewThenAnotherTouch_sendsTouchToSb() =
        testScope.runTest {
            underTest.setStatusBarViewController(phoneStatusBarViewController)
            val downEvBelow =
                MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0)
            interactionEventHandler.handleDispatchTouchEvent(downEvBelow)

        val nextEvent = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, VIEW_BOTTOM + 5f, 0)
            val nextEvent =
                MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, VIEW_BOTTOM + 5f, 0)
            whenever(phoneStatusBarViewController.sendTouchToView(nextEvent)).thenReturn(true)

            val returnVal = interactionEventHandler.handleDispatchTouchEvent(nextEvent)
@@ -193,22 +224,24 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
        }

    @Test
    fun handleDispatchTouchEvent_downAndPanelCollapsedAndInSbBoundAndSbWindowShow_sendsTouchToSb() {
    fun handleDispatchTouchEvent_downAndPanelCollapsedAndInSbBoundAndSbWindowShow_sendsTouchToSb() =
        testScope.runTest {
            underTest.setStatusBarViewController(phoneStatusBarViewController)
            whenever(statusBarWindowStateController.windowIsShowing()).thenReturn(true)
            whenever(notificationPanelViewController.isFullyCollapsed).thenReturn(true)
            whenever(phoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat()))
                .thenReturn(true)
        whenever(phoneStatusBarViewController.sendTouchToView(downEv)).thenReturn(true)
            whenever(phoneStatusBarViewController.sendTouchToView(DOWN_EVENT)).thenReturn(true)

        val returnVal = interactionEventHandler.handleDispatchTouchEvent(downEv)
            val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)

        verify(phoneStatusBarViewController).sendTouchToView(downEv)
            verify(phoneStatusBarViewController).sendTouchToView(DOWN_EVENT)
            assertThat(returnVal).isTrue()
        }

    @Test
    fun handleDispatchTouchEvent_panelNotCollapsed_returnsNull() {
    fun handleDispatchTouchEvent_panelNotCollapsed_returnsNull() =
        testScope.runTest {
            underTest.setStatusBarViewController(phoneStatusBarViewController)
            whenever(statusBarWindowStateController.windowIsShowing()).thenReturn(true)
            whenever(phoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat()))
@@ -216,14 +249,15 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
            // Item we're testing
            whenever(notificationPanelViewController.isFullyCollapsed).thenReturn(false)

        val returnVal = interactionEventHandler.handleDispatchTouchEvent(downEv)
            val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)

        verify(phoneStatusBarViewController, never()).sendTouchToView(downEv)
            verify(phoneStatusBarViewController, never()).sendTouchToView(DOWN_EVENT)
            assertThat(returnVal).isNull()
        }

    @Test
    fun handleDispatchTouchEvent_touchNotInSbBounds_returnsNull() {
    fun handleDispatchTouchEvent_touchNotInSbBounds_returnsNull() =
        testScope.runTest {
            underTest.setStatusBarViewController(phoneStatusBarViewController)
            whenever(statusBarWindowStateController.windowIsShowing()).thenReturn(true)
            whenever(notificationPanelViewController.isFullyCollapsed).thenReturn(true)
@@ -231,14 +265,15 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
            whenever(phoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat()))
                .thenReturn(false)

        val returnVal = interactionEventHandler.handleDispatchTouchEvent(downEv)
            val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)

        verify(phoneStatusBarViewController, never()).sendTouchToView(downEv)
            verify(phoneStatusBarViewController, never()).sendTouchToView(DOWN_EVENT)
            assertThat(returnVal).isNull()
        }

    @Test
    fun handleDispatchTouchEvent_sbWindowNotShowing_noSendTouchToSbAndReturnsTrue() {
    fun handleDispatchTouchEvent_sbWindowNotShowing_noSendTouchToSbAndReturnsTrue() =
        testScope.runTest {
            underTest.setStatusBarViewController(phoneStatusBarViewController)
            whenever(notificationPanelViewController.isFullyCollapsed).thenReturn(true)
            whenever(phoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat()))
@@ -246,14 +281,15 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
            // Item we're testing
            whenever(statusBarWindowStateController.windowIsShowing()).thenReturn(false)

        val returnVal = interactionEventHandler.handleDispatchTouchEvent(downEv)
            val returnVal = interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)

        verify(phoneStatusBarViewController, never()).sendTouchToView(downEv)
            verify(phoneStatusBarViewController, never()).sendTouchToView(DOWN_EVENT)
            assertThat(returnVal).isTrue()
        }

    @Test
    fun handleDispatchTouchEvent_downEventSentToSbThenAnotherEvent_sendsTouchToSb() {
    fun handleDispatchTouchEvent_downEventSentToSbThenAnotherEvent_sendsTouchToSb() =
        testScope.runTest {
            underTest.setStatusBarViewController(phoneStatusBarViewController)
            whenever(statusBarWindowStateController.windowIsShowing()).thenReturn(true)
            whenever(notificationPanelViewController.isFullyCollapsed).thenReturn(true)
@@ -261,7 +297,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
                .thenReturn(true)

            // Down event first
        interactionEventHandler.handleDispatchTouchEvent(downEv)
            interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT)

            // Then another event
            val nextEvent = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
@@ -274,29 +310,35 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
        }

    @Test
    fun shouldInterceptTouchEvent_downEventAlternateBouncer_ignoreIfInUdfpsOverlay() {
    fun shouldInterceptTouchEvent_downEventAlternateBouncer_ignoreIfInUdfpsOverlay() =
        testScope.runTest {
            // Down event within udfpsOverlay bounds while alternateBouncer is showing
        whenever(udfpsOverlayInteractor.canInterceptTouchInUdfpsBounds(downEv)).thenReturn(false)
            whenever(udfpsOverlayInteractor.canInterceptTouchInUdfpsBounds(DOWN_EVENT))
                .thenReturn(false)
            whenever(alternateBouncerInteractor.isVisibleState()).thenReturn(true)

            // Then touch should not be intercepted
        val shouldIntercept = interactionEventHandler.shouldInterceptTouchEvent(downEv)
            val shouldIntercept = interactionEventHandler.shouldInterceptTouchEvent(DOWN_EVENT)
            assertThat(shouldIntercept).isFalse()
        }

    @Test
    fun testGetBouncerContainer() {
    fun testGetBouncerContainer() =
        testScope.runTest {
            Mockito.clearInvocations(view)
            underTest.bouncerContainer
            verify(view).findViewById<ViewGroup>(R.id.keyguard_bouncer_container)
        }

    @Test
    fun testGetKeyguardMessageArea() {
    fun testGetKeyguardMessageArea() =
        testScope.runTest {
            underTest.keyguardMessageArea
            verify(view).findViewById<ViewGroup>(R.id.keyguard_message_area)
        }
}

private val downEv = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
    companion object {
        private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
        private const val VIEW_BOTTOM = 100
    }
}
Loading