diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index f4dda4cc461ab..80cdd1f79cb5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -484,12 +484,14 @@ public abstract class WMShellBaseModule { ShellInit shellInit, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, + ActivityTaskManager activityTaskManager, Optional desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor ) { return Optional.ofNullable( RecentTasksController.create(context, shellInit, shellCommandHandler, - taskStackListener, desktopModeTaskRepository, mainExecutor)); + taskStackListener, activityTaskManager, desktopModeTaskRepository, + mainExecutor)); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java index a5748f69388f9..2a625524b48b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java @@ -17,6 +17,11 @@ package com.android.wm.shell.recents; import com.android.wm.shell.common.annotations.ExternalThread; +import com.android.wm.shell.util.GroupedRecentTaskInfo; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Interface for interacting with the recent tasks. @@ -29,4 +34,11 @@ public interface RecentTasks { default IRecentTasks createExternalInterface() { return null; } + + /** + * Gets the set of recent tasks. + */ + default void getRecentTasks(int maxNum, int flags, int userId, Executor callbackExecutor, + Consumer> callback) { + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 6409e70b0752a..02b5a35f653b5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -58,6 +58,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * Manages the recent task list from the system, caching it as necessary. @@ -72,6 +74,7 @@ public class RecentTasksController implements TaskStackListenerCallback, private final ShellExecutor mMainExecutor; private final TaskStackListenerImpl mTaskStackListener; private final RecentTasks mImpl = new RecentTasksImpl(); + private final ActivityTaskManager mActivityTaskManager; private IRecentTasksListener mListener; private final boolean mIsDesktopMode; @@ -96,6 +99,7 @@ public class RecentTasksController implements TaskStackListenerCallback, ShellInit shellInit, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, + ActivityTaskManager activityTaskManager, Optional desktopModeTaskRepository, @ShellMainThread ShellExecutor mainExecutor ) { @@ -103,17 +107,19 @@ public class RecentTasksController implements TaskStackListenerCallback, return null; } return new RecentTasksController(context, shellInit, shellCommandHandler, taskStackListener, - desktopModeTaskRepository, mainExecutor); + activityTaskManager, desktopModeTaskRepository, mainExecutor); } RecentTasksController(Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, TaskStackListenerImpl taskStackListener, + ActivityTaskManager activityTaskManager, Optional desktopModeTaskRepository, ShellExecutor mainExecutor) { mContext = context; mShellCommandHandler = shellCommandHandler; + mActivityTaskManager = activityTaskManager; mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC); mTaskStackListener = taskStackListener; mDesktopModeTaskRepository = desktopModeTaskRepository; @@ -269,16 +275,11 @@ public class RecentTasksController implements TaskStackListenerCallback, mListener = null; } - @VisibleForTesting - List getRawRecentTasks(int maxNum, int flags, int userId) { - return ActivityTaskManager.getInstance().getRecentTasks(maxNum, flags, userId); - } - @VisibleForTesting ArrayList getRecentTasks(int maxNum, int flags, int userId) { // Note: the returned task list is from the most-recent to least-recent order - final List rawList = getRawRecentTasks(maxNum, flags, - userId); + final List rawList = mActivityTaskManager.getRecentTasks( + maxNum, flags, userId); // Make a mapping of task id -> task info final SparseArray rawMapping = new SparseArray<>(); @@ -335,8 +336,9 @@ public class RecentTasksController implements TaskStackListenerCallback, if (componentName == null) { return null; } - List tasks = getRawRecentTasks(Integer.MAX_VALUE, - ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser()); + List tasks = mActivityTaskManager.getRecentTasks( + Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE, + ActivityManager.getCurrentUser()); for (int i = 0; i < tasks.size(); i++) { final ActivityManager.RecentTaskInfo task = tasks.get(i); if (task.isVisible) { @@ -374,6 +376,16 @@ public class RecentTasksController implements TaskStackListenerCallback, mIRecentTasks = new IRecentTasksImpl(RecentTasksController.this); return mIRecentTasks; } + + @Override + public void getRecentTasks(int maxNum, int flags, int userId, Executor executor, + Consumer> callback) { + mMainExecutor.execute(() -> { + List tasks = + RecentTasksController.this.getRecentTasks(maxNum, flags, userId); + executor.execute(() -> callback.accept(tasks)); + }); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 70fee2be90c6a..b8aaaa76e3c7e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -40,6 +40,7 @@ import static org.mockito.Mockito.when; import static java.lang.Integer.MAX_VALUE; import android.app.ActivityManager; +import android.app.ActivityTaskManager; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Rect; @@ -52,7 +53,6 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; -import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; @@ -68,7 +68,9 @@ import org.mockito.Mock; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Optional; +import java.util.function.Consumer; /** * Tests for {@link RecentTasksController}. @@ -85,11 +87,13 @@ public class RecentTasksControllerTest extends ShellTestCase { private ShellCommandHandler mShellCommandHandler; @Mock private DesktopModeTaskRepository mDesktopModeTaskRepository; + @Mock + private ActivityTaskManager mActivityTaskManager; private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; private ShellInit mShellInit; - private ShellExecutor mMainExecutor; + private TestShellExecutor mMainExecutor; @Before public void setUp() { @@ -97,8 +101,8 @@ public class RecentTasksControllerTest extends ShellTestCase { when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); mShellInit = spy(new ShellInit(mMainExecutor)); mRecentTasksController = spy(new RecentTasksController(mContext, mShellInit, - mShellCommandHandler, mTaskStackListener, Optional.of(mDesktopModeTaskRepository), - mMainExecutor)); + mShellCommandHandler, mTaskStackListener, mActivityTaskManager, + Optional.of(mDesktopModeTaskRepository), mMainExecutor)); mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler, null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController), mMainExecutor); @@ -187,6 +191,37 @@ public class RecentTasksControllerTest extends ShellTestCase { t6.taskId, -1); } + @Test + public void testGetRecentTasks_ReturnsRecentTasksAsynchronously() { + @SuppressWarnings("unchecked") + final List[] recentTasks = new List[1]; + Consumer> consumer = argument -> recentTasks[0] = argument; + ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1); + ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2); + ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3); + ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4); + ActivityManager.RecentTaskInfo t5 = makeTaskInfo(5); + ActivityManager.RecentTaskInfo t6 = makeTaskInfo(6); + setRawList(t1, t2, t3, t4, t5, t6); + + // Mark a couple pairs [t2, t4], [t3, t5] + SplitBounds pair1Bounds = new SplitBounds(new Rect(), new Rect(), 2, 4); + SplitBounds pair2Bounds = new SplitBounds(new Rect(), new Rect(), 3, 5); + + mRecentTasksController.addSplitPair(t2.taskId, t4.taskId, pair1Bounds); + mRecentTasksController.addSplitPair(t3.taskId, t5.taskId, pair2Bounds); + + mRecentTasksController.asRecentTasks() + .getRecentTasks(MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0, Runnable::run, consumer); + mMainExecutor.flushAll(); + + assertGroupedTasksListEquals(recentTasks[0], + t1.taskId, -1, + t2.taskId, t4.taskId, + t3.taskId, t5.taskId, + t6.taskId, -1); + } + @Test public void testGetRecentTasks_groupActiveFreeformTasks() { StaticMockitoSession mockitoSession = mockitoSession().mockStatic( @@ -296,7 +331,7 @@ public class RecentTasksControllerTest extends ShellTestCase { for (ActivityManager.RecentTaskInfo task : tasks) { rawList.add(task); } - doReturn(rawList).when(mRecentTasksController).getRawRecentTasks(anyInt(), anyInt(), + doReturn(rawList).when(mActivityTaskManager).getRecentTasks(anyInt(), anyInt(), anyInt()); return rawList; } @@ -307,7 +342,7 @@ public class RecentTasksControllerTest extends ShellTestCase { * @param expectedTaskIds list of task ids that map to the flattened task ids of the tasks in * the grouped task list */ - private void assertGroupedTasksListEquals(ArrayList recentTasks, + private void assertGroupedTasksListEquals(List recentTasks, int... expectedTaskIds) { int[] flattenedTaskIds = new int[recentTasks.size() * 2]; for (int i = 0; i < recentTasks.size(); i++) { diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt index 59c6635ed8bc5..2b381a954e273 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt @@ -42,7 +42,7 @@ class MediaProjectionAppSelectorController( } private fun List.sortTasks(): List = - asReversed().sortedBy { + sortedBy { // Show normal tasks first and only then tasks with opened app selector it.topActivityComponent == appSelectorComponentName } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt index 0bdddfe2821d9..0927f3b007244 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt @@ -17,11 +17,17 @@ package com.android.systemui.mediaprojection.appselector.data import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ComponentInfoFlags import android.graphics.drawable.Drawable +import android.os.UserHandle +import com.android.launcher3.icons.BaseIconFactory.IconOptions +import com.android.launcher3.icons.IconFactory import com.android.systemui.dagger.qualifiers.Background +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import javax.inject.Inject interface AppIconLoader { suspend fun loadIcon(userId: Int, component: ComponentName): Drawable? @@ -31,11 +37,20 @@ class IconLoaderLibAppIconLoader @Inject constructor( @Background private val backgroundDispatcher: CoroutineDispatcher, + private val context: Context, + private val packageManager: PackageManager ) : AppIconLoader { override suspend fun loadIcon(userId: Int, component: ComponentName): Drawable? = withContext(backgroundDispatcher) { - // TODO(b/240924731): add a blocking call to load an icon using iconloaderlib - null + IconFactory.obtain(context).use { iconFactory -> + val activityInfo = packageManager + .getActivityInfo(component, ComponentInfoFlags.of(0)) + val icon = activityInfo.loadIcon(packageManager) ?: return@withContext null + val userHandler = UserHandle.of(userId) + val options = IconOptions().apply { setUser(userHandler) } + val badgedIcon = iconFactory.createBadgedIconBitmap(icon, options) + badgedIcon.newIcon(context) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt index 6d67e28e15e40..cd994b857e953 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt @@ -16,11 +16,13 @@ package com.android.systemui.mediaprojection.appselector.data +import android.annotation.ColorInt import android.content.ComponentName data class RecentTask( val taskId: Int, val userId: Int, val topActivityComponent: ComponentName?, - val baseIntentComponent: ComponentName? + val baseIntentComponent: ComponentName?, + @ColorInt val colorBackground: Int? ) diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt index 5a0943556d9d4..e8b49cd8ec1ce 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt @@ -16,23 +16,61 @@ package com.android.systemui.mediaprojection.appselector.data +import android.app.ActivityManager +import android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.util.kotlin.getOrNull +import com.android.wm.shell.recents.RecentTasks +import com.android.wm.shell.util.GroupedRecentTaskInfo +import java.util.Optional +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext -import javax.inject.Inject +import java.util.concurrent.Executor interface RecentTaskListProvider { + /** Loads recent tasks, the returned task list is from the most-recent to least-recent order */ suspend fun loadRecentTasks(): List } class ShellRecentTaskListProvider @Inject -constructor(@Background private val coroutineDispatcher: CoroutineDispatcher) : - RecentTaskListProvider { +constructor( + @Background private val coroutineDispatcher: CoroutineDispatcher, + @Background private val backgroundExecutor: Executor, + private val recentTasks: Optional +) : RecentTaskListProvider { + + private val recents by lazy { recentTasks.getOrNull() } override suspend fun loadRecentTasks(): List = withContext(coroutineDispatcher) { - // TODO(b/240924731): add blocking call to load the recents - emptyList() + val rawRecentTasks: List = recents?.getTasks() ?: emptyList() + + rawRecentTasks + .flatMap { listOfNotNull(it.taskInfo1, it.taskInfo2) } + .map { + RecentTask( + it.taskId, + it.userId, + it.topActivity, + it.baseIntent?.component, + it.taskDescription?.backgroundColor + ) + } + } + + private suspend fun RecentTasks.getTasks(): List = + suspendCoroutine { continuation -> + getRecentTasks( + Integer.MAX_VALUE, + RECENT_IGNORE_UNAVAILABLE, + ActivityManager.getCurrentUser(), + backgroundExecutor + ) { tasks -> + continuation.resume(tasks) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskThumbnailLoader.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskThumbnailLoader.kt index 429128032f40c..47faaed103024 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskThumbnailLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskThumbnailLoader.kt @@ -18,6 +18,7 @@ package com.android.systemui.mediaprojection.appselector.data import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.systemui.shared.system.ActivityManagerWrapper import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext @@ -30,12 +31,13 @@ class ActivityTaskManagerThumbnailLoader @Inject constructor( @Background private val coroutineDispatcher: CoroutineDispatcher, -) : - RecentTaskThumbnailLoader { + private val activityManager: ActivityManagerWrapper +) : RecentTaskThumbnailLoader { override suspend fun loadThumbnail(taskId: Int): ThumbnailData? = withContext(coroutineDispatcher) { - // TODO(b/240924731): add blocking call to load a thumbnail - null + val thumbnailData = + activityManager.getTaskThumbnail(taskId, /* isLowResolution= */ false) + if (thumbnailData.thumbnail == null) null else thumbnailData } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt index 37b7f47fb69ea..00b1f3268baef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt @@ -54,7 +54,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { } @Test - fun initMultipleRecentTasksWithoutAppSelectorTask_bindsListInReverse() { + fun initMultipleRecentTasksWithoutAppSelectorTask_bindsListInTheSameOrder() { val tasks = listOf( createRecentTask(taskId = 1), createRecentTask(taskId = 2), @@ -66,15 +66,15 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { verify(view).bind( listOf( - createRecentTask(taskId = 3), - createRecentTask(taskId = 2), createRecentTask(taskId = 1), + createRecentTask(taskId = 2), + createRecentTask(taskId = 3), ) ) } @Test - fun initRecentTasksWithAppSelectorTasks_bindsListInReverseAndAppSelectorTasksAtTheEnd() { + fun initRecentTasksWithAppSelectorTasks_bindsAppSelectorTasksAtTheEnd() { val tasks = listOf( createRecentTask(taskId = 1), createRecentTask(taskId = 2, topActivityComponent = appSelectorComponentName), @@ -88,11 +88,11 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { verify(view).bind( listOf( - createRecentTask(taskId = 5), - createRecentTask(taskId = 3), createRecentTask(taskId = 1), - createRecentTask(taskId = 4, topActivityComponent = appSelectorComponentName), + createRecentTask(taskId = 3), + createRecentTask(taskId = 5), createRecentTask(taskId = 2, topActivityComponent = appSelectorComponentName), + createRecentTask(taskId = 4, topActivityComponent = appSelectorComponentName), ) ) } @@ -105,7 +105,8 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { taskId = taskId, topActivityComponent = topActivityComponent, baseIntentComponent = ComponentName("com", "Test"), - userId = 0 + userId = 0, + colorBackground = 0 ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt new file mode 100644 index 0000000000000..939af16d81cd6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt @@ -0,0 +1,112 @@ +package com.android.systemui.mediaprojection.appselector.data + +import android.app.ActivityManager.RecentTaskInfo +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.wm.shell.recents.RecentTasks +import com.android.wm.shell.util.GroupedRecentTaskInfo +import com.google.common.truth.Truth.assertThat +import java.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Test +import org.junit.runner.RunWith +import java.util.function.Consumer + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ShellRecentTaskListProviderTest : SysuiTestCase() { + + private val dispatcher = Dispatchers.Unconfined + private val recentTasks: RecentTasks = mock() + private val recentTaskListProvider = + ShellRecentTaskListProvider(dispatcher, Runnable::run, Optional.of(recentTasks)) + + @Test + fun loadRecentTasks_oneTask_returnsTheSameTask() { + givenRecentTasks(createSingleTask(taskId = 1)) + + val result = runBlocking { recentTaskListProvider.loadRecentTasks() } + + assertThat(result).containsExactly(createRecentTask(taskId = 1)) + } + + @Test + fun loadRecentTasks_multipleTasks_returnsTheSameTasks() { + givenRecentTasks( + createSingleTask(taskId = 1), + createSingleTask(taskId = 2), + createSingleTask(taskId = 3), + ) + + val result = runBlocking { recentTaskListProvider.loadRecentTasks() } + + assertThat(result) + .containsExactly( + createRecentTask(taskId = 1), + createRecentTask(taskId = 2), + createRecentTask(taskId = 3), + ) + } + + @Test + fun loadRecentTasks_groupedTask_returnsUngroupedTasks() { + givenRecentTasks(createTaskPair(taskId1 = 1, taskId2 = 2)) + + val result = runBlocking { recentTaskListProvider.loadRecentTasks() } + + assertThat(result) + .containsExactly(createRecentTask(taskId = 1), createRecentTask(taskId = 2)) + } + + @Test + fun loadRecentTasks_mixedSingleAndGroupedTask_returnsUngroupedTasks() { + givenRecentTasks( + createSingleTask(taskId = 1), + createTaskPair(taskId1 = 2, taskId2 = 3), + createSingleTask(taskId = 4), + createTaskPair(taskId1 = 5, taskId2 = 6), + ) + + val result = runBlocking { recentTaskListProvider.loadRecentTasks() } + + assertThat(result) + .containsExactly( + createRecentTask(taskId = 1), + createRecentTask(taskId = 2), + createRecentTask(taskId = 3), + createRecentTask(taskId = 4), + createRecentTask(taskId = 5), + createRecentTask(taskId = 6), + ) + } + + @Suppress("UNCHECKED_CAST") + private fun givenRecentTasks(vararg tasks: GroupedRecentTaskInfo) { + whenever(recentTasks.getRecentTasks(any(), any(), any(), any(), any())).thenAnswer { + val consumer = it.arguments.last() as Consumer> + consumer.accept(tasks.toList()) + } + } + + private fun createRecentTask(taskId: Int): RecentTask = + RecentTask( + taskId = taskId, + userId = 0, + topActivityComponent = null, + baseIntentComponent = null, + colorBackground = null + ) + + private fun createSingleTask(taskId: Int): GroupedRecentTaskInfo = + GroupedRecentTaskInfo.forSingleTask(createTaskInfo(taskId)) + + private fun createTaskPair(taskId1: Int, taskId2: Int): GroupedRecentTaskInfo = + GroupedRecentTaskInfo.forSplitTasks(createTaskInfo(taskId1), createTaskInfo(taskId2), null) + + private fun createTaskInfo(taskId: Int) = RecentTaskInfo().apply { this.taskId = taskId } +}