[Status Bar Refactor] Pass status bar view's controller to
NotificationShadeWindowViewController instead of the view itself.

Bug: 209005990
Test: new NotificationShadeWindowViewControllerTest
Change-Id: I6303fb934701a88f7ce52e5e73e8d3df5f18fed8
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index a6980a4..879e694 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -19,7 +19,6 @@
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
 
 import android.app.StatusBarManager;
-import android.graphics.RectF;
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.media.AudioManager;
 import android.media.session.MediaSessionLegacyHelper;
@@ -75,7 +74,7 @@
     private boolean mTouchCancelled;
     private boolean mExpandAnimationRunning;
     private NotificationStackScrollLayout mStackScrollLayout;
-    private PhoneStatusBarView mStatusBarView;
+    private PhoneStatusBarViewController mStatusBarViewController;
     private StatusBar mService;
     private NotificationShadeWindowController mNotificationShadeWindowController;
     private DragDownHelper mDragDownHelper;
@@ -86,8 +85,6 @@
     private final NotificationPanelViewController mNotificationPanelViewController;
     private final PanelExpansionStateManager mPanelExpansionStateManager;
 
-    // Used for determining view / touch intersection
-    private final RectF mTempRect = new RectF();
     private boolean mIsTrackingBarGesture = false;
 
     @Inject
@@ -175,7 +172,7 @@
         mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() {
             @Override
             public Boolean handleDispatchTouchEvent(MotionEvent ev) {
-                if (mStatusBarView == null) {
+                if (mStatusBarViewController == null) { // Fix for b/192490822
                     Log.w(TAG, "Ignoring touch while statusBarView not yet set.");
                     return false;
                 }
@@ -243,27 +240,27 @@
                     expandingBelowNotch = true;
                 }
                 if (expandingBelowNotch) {
-                    return mStatusBarView.dispatchTouchEvent(ev);
+                    return mStatusBarViewController.sendTouchToView(ev);
                 }
 
                 if (!mIsTrackingBarGesture && isDown
                         && mNotificationPanelViewController.isFullyCollapsed()) {
                     float x = ev.getRawX();
                     float y = ev.getRawY();
-                    if (isIntersecting(mStatusBarView, x, y)) {
+                    if (mStatusBarViewController.touchIsWithinView(x, y)) {
                         if (mService.isSameStatusBarState(WINDOW_STATE_SHOWING)) {
                             mIsTrackingBarGesture = true;
-                            return mStatusBarView.dispatchTouchEvent(ev);
+                            return mStatusBarViewController.sendTouchToView(ev);
                         } else { // it's hidden or hiding, don't send to notification shade.
                             return true;
                         }
                     }
                 } else if (mIsTrackingBarGesture) {
-                    final boolean sendToNotification = mStatusBarView.dispatchTouchEvent(ev);
+                    final boolean sendToStatusBar = mStatusBarViewController.sendTouchToView(ev);
                     if (isUp || isCancel) {
                         mIsTrackingBarGesture = false;
                     }
-                    return sendToNotification;
+                    return sendToStatusBar;
                 }
 
                 return null;
@@ -442,8 +439,8 @@
         }
     }
 
-    public void setStatusBarView(PhoneStatusBarView statusBarView) {
-        mStatusBarView = statusBarView;
+    public void setStatusBarViewController(PhoneStatusBarViewController statusBarViewController) {
+        mStatusBarViewController = statusBarViewController;
     }
 
     public void setService(StatusBar statusBar, NotificationShadeWindowController controller) {
@@ -455,11 +452,4 @@
     void setDragDownHelper(DragDownHelper dragDownHelper) {
         mDragDownHelper = dragDownHelper;
     }
-
-    private boolean isIntersecting(View view, float x, float y) {
-        int[] mTempLocation = view.getLocationOnScreen();
-        mTempRect.set(mTempLocation[0], mTempLocation[1], mTempLocation[0] + view.getWidth(),
-                mTempLocation[1] + view.getHeight());
-        return mTempRect.contains(x, y);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index b9386bd..1cb19ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -17,6 +17,7 @@
 
 import android.content.res.Configuration
 import android.graphics.Point
+import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewTreeObserver
@@ -92,6 +93,30 @@
         mView.importantForAccessibility = mode
     }
 
+    /**
+     * Sends a touch event to the status bar view.
+     *
+     * This is required in certain cases because the status bar view is in a separate window from
+     * the rest of SystemUI, and other windows may decide that their touch should instead be treated
+     * as a status bar window touch.
+     */
+    fun sendTouchToView(ev: MotionEvent): Boolean {
+        return mView.dispatchTouchEvent(ev)
+    }
+
+    /**
+     * Returns true if the given (x, y) point (in screen coordinates) is within the status bar
+     * view's range and false otherwise.
+     */
+    fun touchIsWithinView(x: Float, y: Float): Boolean {
+        val left = mView.locationOnScreen[0]
+        val top = mView.locationOnScreen[1]
+        return left <= x &&
+                x <= left + mView.width &&
+                top <= y &&
+                y <= top + mView.height
+    }
+
     class StatusBarViewsCenterProvider : UnfoldMoveFromCenterAnimator.ViewCenterProvider {
         override fun getViewCenter(view: View, outPoint: Point) =
             when (view.id) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 07914cf..8fe03e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -1133,7 +1133,8 @@
                     mStatusBarView = statusBarView;
                     mPhoneStatusBarViewController = statusBarViewController;
                     mStatusBarTransitions = statusBarTransitions;
-                    mNotificationShadeWindowViewController.setStatusBarView(mStatusBarView);
+                    mNotificationShadeWindowViewController
+                            .setStatusBarViewController(mPhoneStatusBarViewController);
                     // Ensure we re-propagate panel expansion values to the panel controller and
                     // any listeners it may have, such as PanelBar. This will also ensure we
                     // re-display the notification panel if necessary (for example, if
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewControllerTest.kt
new file mode 100644
index 0000000..adb76e1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewControllerTest.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2021 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.statusbar.phone
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.MotionEvent
+import androidx.test.filters.SmallTest
+import com.android.keyguard.LockIconViewController
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollectorFake
+import com.android.systemui.dock.DockManager
+import com.android.systemui.statusbar.LockscreenShadeTransitionController
+import com.android.systemui.statusbar.NotificationShadeDepthController
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.phone.NotificationShadeWindowView.InteractionEventHandler
+import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager
+import com.android.systemui.tuner.TunerService
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+@SmallTest
+class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
+    private lateinit var mController: NotificationShadeWindowViewController
+
+    @Mock
+    private lateinit var mView: NotificationShadeWindowView
+    @Mock
+    private lateinit var mTunerService: TunerService
+    @Mock
+    private lateinit var mStatusBarStateController: SysuiStatusBarStateController
+    @Mock
+    private lateinit var mStatusBar: StatusBar
+    @Mock
+    private lateinit var mDockManager: DockManager
+    @Mock
+    private lateinit var mNotificationPanelViewController: NotificationPanelViewController
+    @Mock
+    private lateinit var mNotificationShadeDepthController: NotificationShadeDepthController
+    @Mock
+    private lateinit var mNotificationShadeWindowController: NotificationShadeWindowController
+    @Mock
+    private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController
+    @Mock
+    private lateinit var mStatusBarKeyguardViewManager: StatusBarKeyguardViewManager
+    @Mock
+    private lateinit var mLockscreenShadeTransitionController: LockscreenShadeTransitionController
+    @Mock
+    private lateinit var mLockIconViewController: LockIconViewController
+    @Mock
+    private lateinit var mPhoneStatusBarViewController: PhoneStatusBarViewController
+
+    private lateinit var mInteractionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler>
+    private lateinit var mInteractionEventHandler: InteractionEventHandler
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(mView.bottom).thenReturn(VIEW_BOTTOM)
+
+        mController = NotificationShadeWindowViewController(
+            mLockscreenShadeTransitionController,
+            FalsingCollectorFake(),
+            mTunerService,
+            mStatusBarStateController,
+            mDockManager,
+            mNotificationShadeDepthController,
+            mView,
+            mNotificationPanelViewController,
+            PanelExpansionStateManager(),
+            stackScrollLayoutController,
+            mStatusBarKeyguardViewManager,
+            mLockIconViewController
+        )
+        mController.setupExpandedStatusBar()
+        mController.setService(mStatusBar, mNotificationShadeWindowController)
+
+        mInteractionEventHandlerCaptor =
+            ArgumentCaptor.forClass(InteractionEventHandler::class.java)
+        verify(mView).setInteractionEventHandler(mInteractionEventHandlerCaptor.capture())
+            mInteractionEventHandler = mInteractionEventHandlerCaptor.value
+    }
+
+    // Note: So far, these tests only cover interactions with the status bar view controller. More
+    // tests need to be added to test the rest of handleDispatchTouchEvent.
+
+    @Test
+    fun handleDispatchTouchEvent_nullStatusBarViewController_returnsFalse() {
+        mController.setStatusBarViewController(null)
+
+        val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(downEv)
+
+        assertThat(returnVal).isFalse()
+    }
+
+    @Test
+    fun handleDispatchTouchEvent_downTouchBelowView_sendsTouchToSb() {
+        mController.setStatusBarViewController(mPhoneStatusBarViewController)
+        val ev = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0)
+        whenever(mPhoneStatusBarViewController.sendTouchToView(ev)).thenReturn(true)
+
+        val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(ev)
+
+        verify(mPhoneStatusBarViewController).sendTouchToView(ev)
+        assertThat(returnVal).isTrue()
+    }
+
+    @Test
+    fun handleDispatchTouchEvent_downTouchBelowViewThenAnotherTouch_sendsTouchToSb() {
+        mController.setStatusBarViewController(mPhoneStatusBarViewController)
+        val downEvBelow = MotionEvent.obtain(
+            0L, 0L, MotionEvent.ACTION_DOWN, 0f, VIEW_BOTTOM + 4f, 0
+        )
+        mInteractionEventHandler.handleDispatchTouchEvent(downEvBelow)
+
+        val nextEvent = MotionEvent.obtain(
+            0L, 0L, MotionEvent.ACTION_MOVE, 0f, VIEW_BOTTOM + 5f, 0
+        )
+        whenever(mPhoneStatusBarViewController.sendTouchToView(nextEvent)).thenReturn(true)
+
+        val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(nextEvent)
+
+        verify(mPhoneStatusBarViewController).sendTouchToView(nextEvent)
+        assertThat(returnVal).isTrue()
+    }
+
+    @Test
+    fun handleDispatchTouchEvent_downAndPanelCollapsedAndInSbBoundAndSbWindowShow_sendsTouchToSb() {
+        mController.setStatusBarViewController(mPhoneStatusBarViewController)
+        whenever(mStatusBar.isSameStatusBarState(anyInt())).thenReturn(true)
+        whenever(mNotificationPanelViewController.isFullyCollapsed).thenReturn(true)
+        whenever(mPhoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat()))
+            .thenReturn(true)
+        whenever(mPhoneStatusBarViewController.sendTouchToView(downEv)).thenReturn(true)
+
+        val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(downEv)
+
+        verify(mPhoneStatusBarViewController).sendTouchToView(downEv)
+        assertThat(returnVal).isTrue()
+    }
+
+    @Test
+    fun handleDispatchTouchEvent_panelNotCollapsed_returnsNull() {
+        mController.setStatusBarViewController(mPhoneStatusBarViewController)
+        whenever(mStatusBar.isSameStatusBarState(anyInt())).thenReturn(true)
+        whenever(mPhoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat()))
+            .thenReturn(true)
+        // Item we're testing
+        whenever(mNotificationPanelViewController.isFullyCollapsed).thenReturn(false)
+
+        val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(downEv)
+
+        verify(mPhoneStatusBarViewController, never()).sendTouchToView(downEv)
+        assertThat(returnVal).isNull()
+    }
+
+    @Test
+    fun handleDispatchTouchEvent_touchNotInSbBounds_returnsNull() {
+        mController.setStatusBarViewController(mPhoneStatusBarViewController)
+        whenever(mStatusBar.isSameStatusBarState(anyInt())).thenReturn(true)
+        whenever(mNotificationPanelViewController.isFullyCollapsed).thenReturn(true)
+        // Item we're testing
+        whenever(mPhoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat()))
+            .thenReturn(false)
+
+        val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(downEv)
+
+        verify(mPhoneStatusBarViewController, never()).sendTouchToView(downEv)
+        assertThat(returnVal).isNull()
+    }
+
+    @Test
+    fun handleDispatchTouchEvent_sbWindowNotShowing_noSendTouchToSbAndReturnsTrue() {
+        mController.setStatusBarViewController(mPhoneStatusBarViewController)
+        whenever(mNotificationPanelViewController.isFullyCollapsed).thenReturn(true)
+        whenever(mPhoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat()))
+            .thenReturn(true)
+        // Item we're testing
+        whenever(mStatusBar.isSameStatusBarState(anyInt())).thenReturn(false)
+
+        val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(downEv)
+
+        verify(mPhoneStatusBarViewController, never()).sendTouchToView(downEv)
+        assertThat(returnVal).isTrue()
+    }
+
+    @Test
+    fun handleDispatchTouchEvent_downEventSentToSbThenAnotherEvent_sendsTouchToSb() {
+        mController.setStatusBarViewController(mPhoneStatusBarViewController)
+        whenever(mStatusBar.isSameStatusBarState(anyInt())).thenReturn(true)
+        whenever(mNotificationPanelViewController.isFullyCollapsed).thenReturn(true)
+        whenever(mPhoneStatusBarViewController.touchIsWithinView(anyFloat(), anyFloat()))
+            .thenReturn(true)
+
+        // Down event first
+        mInteractionEventHandler.handleDispatchTouchEvent(downEv)
+
+        // Then another event
+        val nextEvent = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
+        whenever(mPhoneStatusBarViewController.sendTouchToView(nextEvent)).thenReturn(true)
+
+        val returnVal = mInteractionEventHandler.handleDispatchTouchEvent(nextEvent)
+
+        verify(mPhoneStatusBarViewController).sendTouchToView(nextEvent)
+        assertThat(returnVal).isTrue()
+    }
+}
+
+private val downEv = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
+private const val VIEW_BOTTOM = 100
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
index 235de1e..c65a6b6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt
@@ -77,9 +77,10 @@
             val parent = FrameLayout(mContext) // add parent to keep layout params
             view = LayoutInflater.from(mContext)
                 .inflate(R.layout.status_bar, parent, false) as PhoneStatusBarView
+            view.setLeftTopRightBottom(VIEW_LEFT, VIEW_TOP, VIEW_RIGHT, VIEW_BOTTOM)
         }
 
-        controller = createController(view)
+        controller = createAndInitController(view)
     }
 
     @Test
@@ -99,8 +100,7 @@
         val view = createViewMock()
         val argumentCaptor = ArgumentCaptor.forClass(OnPreDrawListener::class.java)
         unfoldConfig.isEnabled = true
-        controller = createController(view)
-        controller.init()
+        controller = createAndInitController(view)
 
         verify(view.viewTreeObserver).addOnPreDrawListener(argumentCaptor.capture())
         argumentCaptor.value.onPreDraw()
@@ -108,6 +108,64 @@
         verify(moveFromCenterAnimation).onViewsReady(any())
     }
 
+    @Test
+    fun touchIsWithinView_inBounds_returnsTrue() {
+        val view = createViewMockWithScreenLocation()
+        controller = createAndInitController(view)
+
+        assertThat(controller.touchIsWithinView(VIEW_LEFT + 1f, VIEW_TOP + 1f)).isTrue()
+    }
+
+    @Test
+    fun touchIsWithinView_onTopLeftCorner_returnsTrue() {
+        val view = createViewMockWithScreenLocation()
+        controller = createAndInitController(view)
+
+        assertThat(controller.touchIsWithinView(VIEW_LEFT.toFloat(), VIEW_TOP.toFloat())).isTrue()
+    }
+
+    @Test
+    fun touchIsWithinView_onBottomRightCorner_returnsTrue() {
+        val view = createViewMockWithScreenLocation()
+        controller = createAndInitController(view)
+
+        assertThat(controller.touchIsWithinView(
+            VIEW_RIGHT.toFloat(), VIEW_BOTTOM.toFloat())
+        ).isTrue()
+    }
+
+    @Test
+    fun touchIsWithinView_xTooSmall_returnsFalse() {
+        val view = createViewMockWithScreenLocation()
+        controller = createAndInitController(view)
+
+        assertThat(controller.touchIsWithinView(VIEW_LEFT - 1f, VIEW_TOP + 1f)).isFalse()
+    }
+
+    @Test
+    fun touchIsWithinView_xTooLarge_returnsFalse() {
+        val view = createViewMockWithScreenLocation()
+        controller = createAndInitController(view)
+
+        assertThat(controller.touchIsWithinView(VIEW_RIGHT + 1f, VIEW_TOP + 1f)).isFalse()
+    }
+
+    @Test
+    fun touchIsWithinView_yTooSmall_returnsFalse() {
+        val view = createViewMockWithScreenLocation()
+        controller = createAndInitController(view)
+
+        assertThat(controller.touchIsWithinView(VIEW_LEFT + 1f, VIEW_TOP - 1f)).isFalse()
+    }
+
+    @Test
+    fun touchIsWithinView_yTooLarge_returnsFalse() {
+        val view = createViewMockWithScreenLocation()
+        controller = createAndInitController(view)
+
+        assertThat(controller.touchIsWithinView(VIEW_LEFT + 1f, VIEW_BOTTOM + 1f)).isFalse()
+    }
+
     private fun createViewMock(): PhoneStatusBarView {
         val view = spy(view)
         val viewTreeObserver = mock(ViewTreeObserver::class.java)
@@ -116,12 +174,23 @@
         return view
     }
 
-    private fun createController(view: PhoneStatusBarView): PhoneStatusBarViewController {
+    private fun createViewMockWithScreenLocation(): PhoneStatusBarView {
+        val view = spy(view)
+        val location = IntArray(2)
+        location[0] = VIEW_LEFT
+        location[1] = VIEW_TOP
+        `when`(view.locationOnScreen).thenReturn(location)
+        return view
+    }
+
+    private fun createAndInitController(view: PhoneStatusBarView): PhoneStatusBarViewController {
         return PhoneStatusBarViewController.Factory(
             Optional.of(sysuiUnfoldComponent),
             Optional.of(progressProvider),
             configurationController
-        ).create(view, touchEventHandler)
+        ).create(view, touchEventHandler).also {
+            it.init()
+        }
     }
 
     private class UnfoldConfig : UnfoldTransitionConfig {
@@ -142,3 +211,8 @@
         }
     }
 }
+
+private const val VIEW_LEFT = 30
+private const val VIEW_RIGHT = 100
+private const val VIEW_TOP = 40
+private const val VIEW_BOTTOM = 100