Merge "[Partial Screensharing] Implement loading of recent tasks, thumbnails and icons" into tm-qpr-dev

This commit is contained in:
Nick Chameyev
2022-09-28 09:46:56 +00:00
committed by Android (Google) Code Review
11 changed files with 270 additions and 39 deletions

View File

@@ -484,12 +484,14 @@ public abstract class WMShellBaseModule {
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
@ShellMainThread ShellExecutor mainExecutor
) {
return Optional.ofNullable(
RecentTasksController.create(context, shellInit, shellCommandHandler,
taskStackListener, desktopModeTaskRepository, mainExecutor));
taskStackListener, activityTaskManager, desktopModeTaskRepository,
mainExecutor));
}
//

View File

@@ -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<List<GroupedRecentTaskInfo>> callback) {
}
}

View File

@@ -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> 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> 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<ActivityManager.RecentTaskInfo> getRawRecentTasks(int maxNum, int flags, int userId) {
return ActivityTaskManager.getInstance().getRecentTasks(maxNum, flags, userId);
}
@VisibleForTesting
ArrayList<GroupedRecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) {
// Note: the returned task list is from the most-recent to least-recent order
final List<ActivityManager.RecentTaskInfo> rawList = getRawRecentTasks(maxNum, flags,
userId);
final List<ActivityManager.RecentTaskInfo> rawList = mActivityTaskManager.getRecentTasks(
maxNum, flags, userId);
// Make a mapping of task id -> task info
final SparseArray<ActivityManager.RecentTaskInfo> rawMapping = new SparseArray<>();
@@ -335,8 +336,9 @@ public class RecentTasksController implements TaskStackListenerCallback,
if (componentName == null) {
return null;
}
List<ActivityManager.RecentTaskInfo> tasks = getRawRecentTasks(Integer.MAX_VALUE,
ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser());
List<ActivityManager.RecentTaskInfo> 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<List<GroupedRecentTaskInfo>> callback) {
mMainExecutor.execute(() -> {
List<GroupedRecentTaskInfo> tasks =
RecentTasksController.this.getRecentTasks(maxNum, flags, userId);
executor.execute(() -> callback.accept(tasks));
});
}
}

View File

@@ -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<GroupedRecentTaskInfo>[] recentTasks = new List[1];
Consumer<List<GroupedRecentTaskInfo>> 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<GroupedRecentTaskInfo> recentTasks,
private void assertGroupedTasksListEquals(List<GroupedRecentTaskInfo> recentTasks,
int... expectedTaskIds) {
int[] flattenedTaskIds = new int[recentTasks.size() * 2];
for (int i = 0; i < recentTasks.size(); i++) {

View File

@@ -42,7 +42,7 @@ class MediaProjectionAppSelectorController(
}
private fun List<RecentTask>.sortTasks(): List<RecentTask> =
asReversed().sortedBy {
sortedBy {
// Show normal tasks first and only then tasks with opened app selector
it.topActivityComponent == appSelectorComponentName
}

View File

@@ -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, Drawable?> { 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)
}
}
}

View File

@@ -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?
)

View File

@@ -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<RecentTask>
}
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<RecentTasks>
) : RecentTaskListProvider {
private val recents by lazy { recentTasks.getOrNull() }
override suspend fun loadRecentTasks(): List<RecentTask> =
withContext(coroutineDispatcher) {
// TODO(b/240924731): add blocking call to load the recents
emptyList()
val rawRecentTasks: List<GroupedRecentTaskInfo> = 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<GroupedRecentTaskInfo> =
suspendCoroutine { continuation ->
getRecentTasks(
Integer.MAX_VALUE,
RECENT_IGNORE_UNAVAILABLE,
ActivityManager.getCurrentUser(),
backgroundExecutor
) { tasks ->
continuation.resume(tasks)
}
}
}

View File

@@ -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
}
}

View File

@@ -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
)
}

View File

@@ -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<List<GroupedRecentTaskInfo>>
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 }
}