Merge "[Test Week] Add ViewCacheTest" into main
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index ea091ca..872a4d0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -24,6 +24,7 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP;
@@ -37,6 +38,7 @@
 import android.util.Log;
 import android.view.HapticFeedbackConstants;
 import android.view.View;
+import android.view.inputmethod.Flags;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
@@ -147,7 +149,7 @@
                 break;
             case BUTTON_IME_SWITCH:
                 logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP);
-                showIMESwitcher();
+                onImeSwitcherPress();
                 break;
             case BUTTON_A11Y:
                 logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_TAP);
@@ -190,6 +192,12 @@
                 backRecentsLongpress(buttonType);
                 return true;
             case BUTTON_IME_SWITCH:
+                if (Flags.imeSwitcherRevamp()) {
+                    logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+                    onImeSwitcherLongPress();
+                    return true;
+                }
+                return false;
             default:
                 return false;
         }
@@ -305,10 +313,14 @@
         mSystemUiProxy.onBackPressed();
     }
 
-    private void showIMESwitcher() {
+    private void onImeSwitcherPress() {
         mSystemUiProxy.onImeSwitcherPressed();
     }
 
+    private void onImeSwitcherLongPress() {
+        mSystemUiProxy.onImeSwitcherLongPress();
+    }
+
     private void notifyA11yClick(boolean longClick) {
         if (longClick) {
             mSystemUiProxy.notifyAccessibilityButtonLongClicked();
diff --git a/quickstep/src/com/android/quickstep/OrientationRectF.java b/quickstep/src/com/android/quickstep/OrientationRectF.java
index aa01b05..2b7ecb2 100644
--- a/quickstep/src/com/android/quickstep/OrientationRectF.java
+++ b/quickstep/src/com/android/quickstep/OrientationRectF.java
@@ -67,13 +67,15 @@
     }
 
     public boolean applyTransform(MotionEvent event, int deltaRotation, boolean forceTransform) {
+        if (deltaRotation == 0) {
+            return contains(event.getX(), event.getY());
+        }
         mTmpMatrix.reset();
         postDisplayRotation(deltaRotation, mHeight, mWidth, mTmpMatrix);
         if (forceTransform) {
             if (DEBUG) {
                 Log.d(TAG, "Transforming rotation due to forceTransform, "
                         + "deltaRotation: " + deltaRotation
-                        + "mRotation: " + mRotation
                         + " this: " + this);
             }
             event.applyTransform(mTmpMatrix);
diff --git a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
index 29a57fc..5264643 100644
--- a/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/SimpleOrientationTouchTransformer.java
@@ -34,12 +34,18 @@
 
     private final Context mContext;
     private OrientationRectF mOrientationRectF;
+    private OrientationRectF mTouchingOrientationRectF;
+    private int mViewRotation;
 
     public SimpleOrientationTouchTransformer(Context context) {
+        this(context, DisplayController.INSTANCE.get(context));
+    }
+
+    @androidx.annotation.VisibleForTesting
+    public SimpleOrientationTouchTransformer(Context context, DisplayController displayController) {
         mContext = context;
-        DisplayController.INSTANCE.get(context).addChangeListener(this);
-        onDisplayInfoChanged(context, DisplayController.INSTANCE.get(context).getInfo(),
-                CHANGE_ALL);
+        displayController.addChangeListener(this);
+        onDisplayInfoChanged(context, displayController.getInfo(), CHANGE_ALL);
     }
 
     @Override
@@ -56,7 +62,29 @@
                 info.rotation);
     }
 
+    /**
+     * Called when the touch is started. This preserves the touching orientation until the touch is
+     * done (i.e. ACTION_CANCEL or ACTION_UP). So the transform won't produce inconsistent position
+     * if display is changed during the touch.
+     */
+    public void updateTouchingOrientation(int viewRotation) {
+        mViewRotation = viewRotation;
+        mTouchingOrientationRectF = new OrientationRectF(mOrientationRectF.left,
+                mOrientationRectF.top, mOrientationRectF.right, mOrientationRectF.bottom,
+                mOrientationRectF.getRotation());
+    }
+
+    /** Called when the touch is finished. */
+    public void clearTouchingOrientation() {
+        mTouchingOrientationRectF = null;
+    }
+
     public void transform(MotionEvent ev, int rotation) {
+        if (mTouchingOrientationRectF != null) {
+            mTouchingOrientationRectF.applyTransformToRotation(ev, mViewRotation,
+                    true /* forceTransform */);
+            return;
+        }
         mOrientationRectF.applyTransformToRotation(ev, rotation, true /* forceTransform */);
     }
 }
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 433baa9..0d9f81f 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -229,6 +229,17 @@
     }
 
     @Override
+    public void onImeSwitcherLongPress() {
+        if (mSystemUiProxy != null) {
+            try {
+                mSystemUiProxy.onImeSwitcherLongPress();
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed call onImeSwitcherLongPress");
+            }
+        }
+    }
+
+    @Override
     public void setHomeRotationEnabled(boolean enabled) {
         if (mSystemUiProxy != null) {
             try {
diff --git a/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
index cb44a1a..fcf9ab1 100644
--- a/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
+++ b/quickstep/src/com/android/quickstep/util/InputConsumerProxy.java
@@ -108,19 +108,28 @@
             return false;
         }
 
+        final SimpleOrientationTouchTransformer touchTransformer =
+                SimpleOrientationTouchTransformer.INSTANCE.get(mContext);
+        final int viewRotation = mRotationSupplier.get();
+        final boolean needTransform = viewRotation != ev.getSurfaceRotation();
         if (action == ACTION_DOWN) {
             mTouchInProgress = true;
+            if (needTransform) {
+                touchTransformer.updateTouchingOrientation(viewRotation);
+            }
             initInputConsumerIfNeeded(/* isFromTouchDown= */ true);
         } else if (action == ACTION_CANCEL || action == ACTION_UP) {
             // Finish any pending actions
             mTouchInProgress = false;
+            touchTransformer.clearTouchingOrientation();
             if (mDestroyPending) {
                 destroy();
             }
         }
         if (mInputConsumer != null) {
-            SimpleOrientationTouchTransformer.INSTANCE.get(mContext).transform(ev,
-                    mRotationSupplier.get());
+            if (needTransform) {
+                touchTransformer.transform(ev, viewRotation);
+            }
             mInputConsumer.onMotionEvent(ev);
         }
 
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
index 0f06d98..399aea6 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java
@@ -4,6 +4,8 @@
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_TAP;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP;
 import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
@@ -26,6 +28,7 @@
 
 import android.os.Handler;
 import android.view.View;
+import android.view.inputmethod.Flags;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
@@ -109,8 +112,27 @@
 
     @Test
     public void testPressImeSwitcher() {
+        mNavButtonController.init(mockTaskbarControllers);
         mNavButtonController.onButtonClick(BUTTON_IME_SWITCH, mockView);
+        verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP);
+        verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS);
         verify(mockSystemUiProxy, times(1)).onImeSwitcherPressed();
+        verify(mockSystemUiProxy, never()).onImeSwitcherLongPress();
+    }
+
+    @Test
+    public void testLongPressImeSwitcher() {
+        mNavButtonController.init(mockTaskbarControllers);
+        mNavButtonController.onButtonLongClick(BUTTON_IME_SWITCH, mockView);
+        verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP);
+        verify(mockSystemUiProxy, never()).onImeSwitcherPressed();
+        if (Flags.imeSwitcherRevamp()) {
+            verify(mockStatsLogger, times(1)).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+            verify(mockSystemUiProxy, times(1)).onImeSwitcherLongPress();
+        } else {
+            verify(mockStatsLogger, never()).log(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS);
+            verify(mockSystemUiProxy, never()).onImeSwitcherLongPress();
+        }
     }
 
     @Test
diff --git a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
index 298dd6c..f5d082d 100644
--- a/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
+++ b/quickstep/tests/src/com/android/quickstep/OrientationTouchTransformerTest.java
@@ -21,6 +21,7 @@
 
 import static com.android.launcher3.util.NavigationMode.NO_BUTTON;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -288,6 +289,34 @@
         assertTrue(mTouchTransformer.touchInValidSwipeRegions(inRegion2.getX(), inRegion2.getY()));
     }
 
+    @Test
+    public void testSimpleOrientationTouchTransformer() {
+        final DisplayController displayController = mock(DisplayController.class);
+        doReturn(mInfo).when(displayController).getInfo();
+        final SimpleOrientationTouchTransformer transformer =
+                new SimpleOrientationTouchTransformer(getApplicationContext(), displayController);
+        final MotionEvent move1 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10);
+        transformer.transform(move1, Surface.ROTATION_90);
+        // The position is transformed to 90 degree.
+        assertEquals(10, move1.getX(), 0f /* delta */);
+        assertEquals(NORMAL_SCREEN_SIZE.getWidth() - 100, move1.getY(), 0f /* delta */);
+
+        // If the touching state is specified, the position is still transformed to 90 degree even
+        // if the given rotation is changed.
+        final MotionEvent move2 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10);
+        transformer.updateTouchingOrientation(Surface.ROTATION_90);
+        transformer.transform(move2, Surface.ROTATION_0);
+        assertEquals(move1.getX(), move2.getX(), 0f /* delta */);
+        assertEquals(move1.getY(), move2.getY(), 0f /* delta */);
+
+        // If the touching state is cleared, it restores to use the given rotation.
+        final MotionEvent move3 = generateMotionEvent(MotionEvent.ACTION_MOVE, 100, 10);
+        transformer.clearTouchingOrientation();
+        transformer.transform(move3, Surface.ROTATION_0);
+        assertEquals(100, move3.getX(), 0f /* delta */);
+        assertEquals(10, move3.getY(), 0f /* delta */);
+    }
+
     private DisplayController.Info createDisplayInfo(Size screenSize, int rotation) {
         Point displaySize = new Point(screenSize.getWidth(), screenSize.getHeight());
         RotationUtils.rotateSize(displaySize, rotation);
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 861631d..fbd24d8 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -795,6 +795,9 @@
         @UiEvent(doc = "User launches Overview from meta+tab keyboard shortcut")
         LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT(1765),
 
+        @UiEvent(doc = "User long pressed on the taskbar IME switcher button")
+        LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS(1798),
+
         // ADD MORE
         ;
 
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index 1fb8c83..3016c9a 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -37,6 +37,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
 
 import com.android.launcher3.BaseActivity;
@@ -484,7 +485,8 @@
      * Sets or unsets a flag the can change whether the widget host should be in the listening
      * state.
      */
-    private void setShouldListenFlag(int flag, boolean on) {
+    @VisibleForTesting
+    void setShouldListenFlag(int flag, boolean on) {
         if (on) {
             mFlags.updateAndGet(old -> old | flag);
         } else {
diff --git a/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt
new file mode 100644
index 0000000..1a659e2
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/widget/LauncherWidgetHolderTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 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.launcher3.widget
+
+import androidx.test.annotation.UiThreadTest
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.BuildConfig.WIDGETS_ENABLED
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.widget.LauncherWidgetHolder.FLAG_ACTIVITY_RESUMED
+import com.android.launcher3.widget.LauncherWidgetHolder.FLAG_ACTIVITY_STARTED
+import com.android.launcher3.widget.LauncherWidgetHolder.FLAG_STATE_IS_NORMAL
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(LauncherMultivalentJUnit::class)
+class LauncherWidgetHolderTest {
+    private lateinit var widgetHolder: LauncherWidgetHolder
+
+    @Before
+    fun setUp() {
+        assertTrue(WIDGETS_ENABLED)
+        widgetHolder =
+            LauncherWidgetHolder(ActivityContextWrapper(getInstrumentation().targetContext)) {}
+    }
+
+    @After
+    fun tearDown() {
+        widgetHolder.destroy()
+    }
+
+    @Test
+    fun widget_holder_start_listening() {
+        val testView = mock(PendingAppWidgetHostView::class.java)
+        widgetHolder.mViews[0] = testView
+        widgetHolder.setListeningFlag(false)
+        assertFalse(widgetHolder.isListening)
+        widgetHolder.startListening()
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        getInstrumentation().waitForIdleSync()
+        assertTrue(widgetHolder.isListening)
+        verify(testView, times(1)).reInflate()
+        widgetHolder.clearWidgetViews()
+    }
+
+    @Test
+    fun holder_start_listening_after_activity_start() {
+        widgetHolder.setShouldListenFlag(FLAG_STATE_IS_NORMAL or FLAG_ACTIVITY_RESUMED, true)
+        widgetHolder.setActivityStarted(false)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+        widgetHolder.setActivityStarted(true)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+    }
+
+    @Test
+    fun holder_start_listening_after_activity_resume() {
+        widgetHolder.setShouldListenFlag(FLAG_STATE_IS_NORMAL or FLAG_ACTIVITY_STARTED, true)
+        widgetHolder.setActivityResumed(false)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+        widgetHolder.setActivityResumed(true)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+    }
+
+    @Test
+    fun holder_start_listening_after_state_normal() {
+        widgetHolder.setShouldListenFlag(FLAG_ACTIVITY_RESUMED or FLAG_ACTIVITY_STARTED, true)
+        widgetHolder.setStateIsNormal(false)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+        widgetHolder.setStateIsNormal(true)
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertTrue(widgetHolder.shouldListen(widgetHolder.mFlags.get()))
+    }
+
+    @Test
+    @UiThreadTest
+    fun widget_holder_create_view() {
+        val mockProviderInfo = mock(LauncherAppWidgetProviderInfo::class.java)
+        doReturn(false).whenever(mockProviderInfo).isCustomWidget
+        assertEquals(0, widgetHolder.mViews.size())
+        widgetHolder.createView(APP_WIDGET_ID, mockProviderInfo)
+        assertEquals(1, widgetHolder.mViews.size())
+        assertEquals(APP_WIDGET_ID, widgetHolder.mViews.get(0).appWidgetId)
+        widgetHolder.deleteAppWidgetId(APP_WIDGET_ID)
+        assertEquals(0, widgetHolder.mViews.size())
+    }
+
+    @Test
+    fun holder_add_provider_change_listener() {
+        val listener = LauncherWidgetHolder.ProviderChangedListener {}
+        widgetHolder.addProviderChangeListener(listener)
+        getInstrumentation().waitForIdleSync()
+        assertEquals(1, widgetHolder.mProviderChangedListeners.size)
+        assertSame(widgetHolder.mProviderChangedListeners.first(), listener)
+        widgetHolder.removeProviderChangeListener(listener)
+    }
+
+    @Test
+    fun holder_remove_provider_change_listener() {
+        val listener = LauncherWidgetHolder.ProviderChangedListener {}
+        widgetHolder.addProviderChangeListener(listener)
+        widgetHolder.removeProviderChangeListener(listener)
+        getInstrumentation().waitForIdleSync()
+        assertEquals(0, widgetHolder.mProviderChangedListeners.size)
+    }
+
+    @Test
+    fun widget_holder_stop_listening() {
+        widgetHolder.setListeningFlag(true)
+        assertTrue(widgetHolder.isListening)
+        widgetHolder.stopListening()
+        widgetHolder.widgetHolderExecutor.submit {}.get()
+        assertFalse(widgetHolder.isListening)
+    }
+
+    companion object {
+        private const val APP_WIDGET_ID = 0
+    }
+}