Merge "Add a refactored TaskThumbnailView" into main
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
index a12cf83..a593712 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java
@@ -11,6 +11,7 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.util.AttributeSet;
+import android.util.Log;
 import android.util.Pair;
 import android.view.MotionEvent;
 import android.view.View;
@@ -43,6 +44,7 @@
 
 import kotlin.Unit;
 
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.function.Consumer;
 
@@ -58,6 +60,7 @@
  */
 public class GroupedTaskView extends TaskView {
 
+    private static final String TAG = TaskView.class.getSimpleName();
     @Nullable
     private Task mSecondaryTask;
     // TODO(b/336612373): Support new TTV for GroupedTaskView
@@ -264,7 +267,6 @@
             InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
         }, false /* freezeTaskList */, true /*launchingExistingTaskview*/);
 
-
         // Callbacks get run from recentsView for case when recents animation already running
         recentsView.addSideTaskLaunchCallback(endCallback);
         return endCallback;
@@ -287,6 +289,8 @@
                 launchingExistingTaskView ? this : null, mTask.key.id,
                 mSecondaryTask.key.id, SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
                 callback, isQuickswitch, getSnapPosition());
+        Log.d(TAG, "launchTaskInternal - launchExistingSplitPair: " + Arrays.toString(
+                getTaskIds()));
     }
 
     @Override
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 599a92a..445f082 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2736,18 +2736,20 @@
      * Returns true if we should add a stub taskView for the running task id
      */
     protected boolean shouldAddStubTaskView(Task[] runningTasks) {
-        if (runningTasks.length > 1) {
-            TaskView primaryTaskView = getTaskViewByTaskId(runningTasks[0].key.id);
-            TaskView secondaryTaskView = getTaskViewByTaskId(runningTasks[1].key.id);
-            int leftTopTaskViewId =
-                    (primaryTaskView == null) ? -1 : primaryTaskView.getTaskViewId();
-            int rightBottomTaskViewId =
-                    (secondaryTaskView == null) ? -1 : secondaryTaskView.getTaskViewId();
-            // Add a new stub view if both taskIds don't match any taskViews
-            return leftTopTaskViewId != rightBottomTaskViewId || leftTopTaskViewId == -1;
+        TaskView taskView = getTaskViewByTaskId(runningTasks[0].key.id);
+        if (taskView == null) {
+            // No TaskView found, add a stub task.
+            return true;
         }
-        Task runningTaskInfo = runningTasks[0];
-        return runningTaskInfo != null && getTaskViewByTaskId(runningTaskInfo.key.id) == null;
+
+        if (runningTasks.length > 1) {
+            // Ensure all taskIds matches the TaskView, otherwise add a stub task.
+            return Arrays.stream(runningTasks).anyMatch(
+                    runningTask -> !taskView.containsTaskId(runningTask.key.id));
+        } else {
+            // Ensure the TaskView only contains a single taskId, otherwise add a stub task.
+            return taskView.containsMultipleTasks();
+        }
     }
 
     /**
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 1274f0e..c90e789 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -909,6 +909,8 @@
                     getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
             if (ActivityManagerWrapper.getInstance()
                     .startActivityFromRecents(mTask.key, opts.options)) {
+                Log.d(TAG, "launchTaskAnimated - startActivityFromRecents: " + Arrays.toString(
+                        getTaskIds()));
                 ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED);
                 RecentsView recentsView = getRecentsView();
                 if (recentsView.getRunningTaskViewId() != -1) {
@@ -934,6 +936,7 @@
                 return null;
             }
         } else {
+            Log.d(TAG, "launchTaskAnimated - mTask is null");
             return null;
         }
     }
@@ -1078,6 +1081,8 @@
                 }
             });
             anim.start();
+            Log.d(TAG, "launchTasks - composeRecentsLaunchAnimator: " + Arrays.toString(
+                    getTaskIds()));
             recentsView.onTaskLaunchedInLiveTileMode();
             return runnableList;
         } else {
diff --git a/quickstep/tests/OWNERS b/quickstep/tests/OWNERS
index c271803..02e8ebc 100644
--- a/quickstep/tests/OWNERS
+++ b/quickstep/tests/OWNERS
@@ -2,4 +2,3 @@
 sunnygoyal@google.com
 winsonc@google.com
 hyunyoungs@google.com
-mateuszc@google.com
diff --git a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
index 857a4e3..edc0a6f 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplPrivateSpaceTest.java
@@ -21,7 +21,6 @@
 
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assume.assumeTrue;
 
 import android.util.Log;
 
@@ -29,6 +28,8 @@
 import androidx.test.filters.LargeTest;
 
 import com.android.launcher3.tapl.LauncherInstrumentation;
+import com.android.launcher3.tapl.PrivateSpaceContainer;
+import com.android.launcher3.util.TestUtil;
 import com.android.launcher3.util.rule.ScreenRecordRule;
 
 import org.junit.After;
@@ -43,7 +44,9 @@
 public class TaplPrivateSpaceTest extends AbstractQuickStepTest {
 
     private int mProfileUserId;
-    private boolean mPrivateProfileSetupSuccessful;
+
+    private static final String PRIVATE_PROFILE_NAME = "LauncherPrivateProfile";
+    private static final String INSTALLED_APP_NAME = "Aardwolf";
     private static final String TAG = "TaplPrivateSpaceTest";
 
     @Override
@@ -52,8 +55,6 @@
         initialize(this);
 
         createAndStartPrivateProfileUser();
-        assumeTrue("Private Profile Setup not successful, aborting",
-                mPrivateProfileSetupSuccessful);
 
         mDevice.pressHome();
         waitForLauncherCondition("Launcher didn't start", Objects::nonNull);
@@ -72,7 +73,7 @@
 
     private void createAndStartPrivateProfileUser() {
         String createUserOutput = executeShellCommand("pm create-user --profileOf 0 --user-type "
-                + "android.os.usertype.profile.PRIVATE LauncherPrivateProfile");
+                + "android.os.usertype.profile.PRIVATE " + PRIVATE_PROFILE_NAME);
         updatePrivateProfileSetupSuccessful("pm create-user", createUserOutput);
         String[] tokens = createUserOutput.split("\\s+");
         mProfileUserId = Integer.parseInt(tokens[tokens.length - 1]);
@@ -86,24 +87,42 @@
 
     @After
     public void removePrivateProfile() {
-        String output = executeShellCommand("pm remove-user " + mProfileUserId);
-        updateProfileRemovalSuccessful("pm remove-user", output);
-        waitForPrivateSpaceRemoval();
+        String userListOutput = executeShellCommand("pm list users");
+        if (isPrivateProfilePresent("pm list users", userListOutput)) {
+            String output = executeShellCommand("pm remove-user " + mProfileUserId);
+            updateProfileRemovalSuccessful("pm remove-user", output);
+            waitForPrivateSpaceRemoval();
+        }
     }
 
     @Test
     @ScreenRecordRule.ScreenRecord // b/334946529
     public void testPrivateSpaceContainerIsPresent() {
-        assumeTrue(mPrivateProfileSetupSuccessful);
         // Scroll to the bottom of All Apps
         executeOnLauncher(launcher -> launcher.getAppsView().resetAndScrollToPrivateSpaceHeader());
-        waitForResumed("Launcher internal state is still Background");
 
         // Verify Unlocked View elements are present.
         assertNotNull("Private Space Unlocked View not found, or is not correct",
                 mLauncher.getAllApps().getPrivateSpaceUnlockedView());
     }
 
+    @Test
+    @ScreenRecordRule.ScreenRecord // b/334946529
+    public void testUserInstalledAppIsShownAboveDivider() throws IOException {
+        // Ensure that the App is not installed in main user otherwise, it may not be found in
+        // PS container.
+        TestUtil.uninstallDummyApp();
+        // Install the app in Private Profile
+        TestUtil.installDummyAppForUser(mProfileUserId);
+        waitForLauncherUIUpdate();
+        // Scroll to the bottom of All Apps
+        executeOnLauncher(launcher -> launcher.getAppsView().resetAndScrollToPrivateSpaceHeader());
+
+        // Verify the Installed App is displayed in correct position.
+        PrivateSpaceContainer psContainer = mLauncher.getAllApps().getPrivateSpaceUnlockedView();
+        psContainer.verifyInstalledAppIsPresent(INSTALLED_APP_NAME);
+    }
+
     private void waitForPrivateSpaceSetup() {
         waitForLauncherCondition("Private Profile not setup",
                 launcher -> launcher.getAppsView().hasPrivateProfile(),
@@ -129,7 +148,7 @@
     private void updatePrivateProfileSetupSuccessful(String cli, String output) {
         Log.d(TAG, "updatePrivateProfileSetupSuccessful, cli=" + cli + " " + "output="
                 + output);
-        mPrivateProfileSetupSuccessful = output.startsWith("Success");
+        assertTrue(output, output.startsWith("Success"));
     }
 
     private void updateProfileRemovalSuccessful(String cli, String output) {
@@ -137,6 +156,11 @@
         assertTrue(output, output.startsWith("Success"));
     }
 
+    private boolean isPrivateProfilePresent(String cli, String output) {
+        Log.d(TAG, "updatePrivateProfilePresent, cli=" + cli + " " + "output=" + output);
+        return output.contains(PRIVATE_PROFILE_NAME);
+    }
+
     private String executeShellCommand(String command) {
         try {
             return mDevice.executeShellCommand(command);
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
index 43ebb17..7ab72f2 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsKeyboardQuickSwitch.java
@@ -65,7 +65,7 @@
     public void setUp() throws Exception {
         Assume.assumeTrue(mLauncher.isTablet());
         super.setUp();
-        startAppFast(CALCULATOR_APP_PACKAGE);
+        startAppFastInFullscreen(CALCULATOR_APP_PACKAGE);
         startTestActivity(2);
     }
 
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
index b2b4959..51749a7 100644
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ b/src/com/android/launcher3/util/VibratorWrapper.java
@@ -19,14 +19,12 @@
 import static android.os.VibrationEffect.createPredefined;
 import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
 
-import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
 
 import android.annotation.SuppressLint;
-import android.content.ContentResolver;
 import android.content.Context;
-import android.database.ContentObserver;
 import android.media.AudioAttributes;
+import android.net.Uri;
 import android.os.SystemClock;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
@@ -51,6 +49,8 @@
 
     public static final VibrationEffect EFFECT_CLICK =
             createPredefined(VibrationEffect.EFFECT_CLICK);
+    private static final Uri HAPTIC_FEEDBACK_URI =
+            Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED);
 
     private static final float LOW_TICK_SCALE = 0.9f;
     private static final float DRAG_TEXTURE_SCALE = 0.03f;
@@ -76,8 +76,9 @@
     private final Context mContext;
     private final Vibrator mVibrator;
     private final boolean mHasVibrator;
+    private final SettingsCache.OnChangeListener mHapticChangeListener =
+            isEnabled -> mIsHapticFeedbackEnabled = isEnabled;
 
-    private ContentObserver mHapticFeedbackObserver;
     private boolean mIsHapticFeedbackEnabled;
 
     private VibratorWrapper(Context context) {
@@ -85,16 +86,9 @@
         mVibrator = context.getSystemService(Vibrator.class);
         mHasVibrator = mVibrator.hasVibrator();
         if (mHasVibrator) {
-            final ContentResolver resolver = context.getContentResolver();
-            mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver);
-            mHapticFeedbackObserver = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
-                @Override
-                public void onChange(boolean selfChange) {
-                    mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver);
-                }
-            };
-            resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED),
-                    false /* notifyForDescendants */, mHapticFeedbackObserver);
+            SettingsCache cache = SettingsCache.INSTANCE.get(mContext);
+            cache.register(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
+            mIsHapticFeedbackEnabled = cache.getValue(HAPTIC_FEEDBACK_URI, 0);
         } else {
             mIsHapticFeedbackEnabled = false;
         }
@@ -129,8 +123,9 @@
 
     @Override
     public void close() {
-        if (mHapticFeedbackObserver != null) {
-            mContext.getContentResolver().unregisterContentObserver(mHapticFeedbackObserver);
+        if (mHasVibrator) {
+            SettingsCache.INSTANCE.get(mContext)
+                    .unregister(HAPTIC_FEEDBACK_URI, mHapticChangeListener);
         }
     }
 
@@ -183,10 +178,6 @@
         mLastDragTime = 0;
     }
 
-    private boolean isHapticFeedbackEnabled(ContentResolver resolver) {
-        return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1;
-    }
-
     /** Vibrates with the given effect if haptic feedback is available and enabled. */
     public void vibrate(VibrationEffect vibrationEffect) {
         if (mHasVibrator && mIsHapticFeedbackEnabled) {
diff --git a/tests/OWNERS b/tests/OWNERS
index b5ee7d7..6b8643c 100644
--- a/tests/OWNERS
+++ b/tests/OWNERS
@@ -3,4 +3,3 @@
 sunnygoyal@google.com
 winsonc@google.com
 hyunyoungs@google.com
-mateuszc@google.com
diff --git a/tests/shared/com/android/launcher3/testing/OWNERS b/tests/shared/com/android/launcher3/testing/OWNERS
new file mode 100644
index 0000000..02e8ebc
--- /dev/null
+++ b/tests/shared/com/android/launcher3/testing/OWNERS
@@ -0,0 +1,4 @@
+vadimt@google.com
+sunnygoyal@google.com
+winsonc@google.com
+hyunyoungs@google.com
diff --git a/tests/tapl/com/android/launcher3/tapl/PrivateSpaceContainer.java b/tests/tapl/com/android/launcher3/tapl/PrivateSpaceContainer.java
index 2c16e01..daddd2f 100644
--- a/tests/tapl/com/android/launcher3/tapl/PrivateSpaceContainer.java
+++ b/tests/tapl/com/android/launcher3/tapl/PrivateSpaceContainer.java
@@ -16,6 +16,8 @@
 
 package com.android.launcher3.tapl;
 
+import android.graphics.Point;
+
 import androidx.test.uiautomator.UiObject2;
 
 /**
@@ -57,4 +59,16 @@
     private void verifyDividerIsPresent() {
         mLauncher.waitForObjectInContainer(mAppListRecycler, DIVIDER_RES_ID);
     }
+
+    /**
+     * Verifies that a user installed app is present above the divider.
+     */
+    public void verifyInstalledAppIsPresent(String appName) {
+        HomeAppIcon appIcon = mLauncher.getAllApps().getAppIcon(appName);
+        final Point iconCenter = appIcon.mObject.getVisibleCenter();
+        UiObject2 divider = mLauncher.waitForObjectInContainer(mAppListRecycler, DIVIDER_RES_ID);
+        final Point dividerCenter = divider.getVisibleCenter();
+        mLauncher.assertTrue("Installed App: " + appName + " is not above the divider",
+                iconCenter.y < dividerCenter.y);
+    }
 }