diff options
| author | 2022-09-23 14:11:03 +0000 | |
|---|---|---|
| committer | 2022-09-23 14:11:03 +0000 | |
| commit | d84bec201ce133d43f351b69d306ee75843568fa (patch) | |
| tree | 1889cacba850bb0b84de0c6a9301d9a656ee8e63 | |
| parent | 32b7c278ba9088a795c041644b9cc46c482e6df4 (diff) | |
| parent | 66e91066196bb441f65f45f9c26b8134a91ffc81 (diff) | |
Merge "[Partial Screensharing] Add initial recents selector UI" into tm-qpr-dev am: 66e9106619
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19867406
Change-Id: I4f6b2799fa1009f3c3b888ac926c8c7455cfbc28
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
17 files changed, 769 insertions, 24 deletions
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 84557b7909c2..f5ba275b604b 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -1069,7 +1069,12 @@ public class ChooserActivity extends ResolverActivity implements } } - private ViewGroup createContentPreviewView(ViewGroup parent) { + /** + * Create a view that will be shown in the content preview area + * @param parent reference to the parent container where the view should be attached to + * @return content preview view + */ + protected ViewGroup createContentPreviewView(ViewGroup parent) { Intent targetIntent = getTargetIntent(); int previewType = findPreferredContentPreview(targetIntent, getContentResolver()); return displayContentPreview(previewType, targetIntent, getLayoutInflater(), parent); @@ -2653,7 +2658,7 @@ public class ChooserActivity extends ResolverActivity implements boolean isExpandable = getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode(); - if (directShareHeight != 0 && isSendAction(getTargetIntent()) + if (directShareHeight != 0 && shouldShowContentPreview() && isExpandable) { // make sure to leave room for direct share 4->8 expansion int requiredExpansionHeight = @@ -2901,7 +2906,14 @@ public class ChooserActivity extends ResolverActivity implements return shouldShowTabs() && mMultiProfilePagerAdapter.getListAdapterForUserHandle( UserHandle.of(UserHandle.myUserId())).getCount() > 0 - && isSendAction(getTargetIntent()); + && shouldShowContentPreview(); + } + + /** + * @return true if we want to show the content preview area + */ + protected boolean shouldShowContentPreview() { + return isSendAction(getTargetIntent()); } private void updateStickyContentPreview() { @@ -3234,7 +3246,7 @@ public class ChooserActivity extends ResolverActivity implements return 0; } - if (!isSendAction(getTargetIntent())) { + if (!shouldShowContentPreview()) { return 0; } @@ -3265,7 +3277,7 @@ public class ChooserActivity extends ResolverActivity implements // There can be at most one row in the listview, that is internally // a ViewGroup with 2 rows public int getServiceTargetRowCount() { - if (isSendAction(getTargetIntent()) + if (shouldShowContentPreview() && !ActivityManager.isLowRamDeviceStatic()) { return 1; } diff --git a/packages/SystemUI/res/layout/media_projection_app_selector.xml b/packages/SystemUI/res/layout/media_projection_app_selector.xml index 4ad6849e9c45..226bc6afc5ab 100644 --- a/packages/SystemUI/res/layout/media_projection_app_selector.xml +++ b/packages/SystemUI/res/layout/media_projection_app_selector.xml @@ -36,13 +36,14 @@ android:background="@*android:drawable/bottomsheet_background"> <ImageView - android:id="@*android:id/icon" android:layout_width="@dimen/media_projection_app_selector_icon_size" android:layout_height="@dimen/media_projection_app_selector_icon_size" android:layout_marginTop="@*android:dimen/chooser_edge_margin_normal" android:layout_marginBottom="@*android:dimen/chooser_edge_margin_normal" android:importantForAccessibility="no" - android:tint="?android:attr/textColorPrimary"/> + android:tint="?android:attr/textColorPrimary" + android:src="@drawable/ic_present_to_all" + /> <TextView android:id="@*android:id/title" android:layout_height="wrap_content" diff --git a/packages/SystemUI/res/layout/media_projection_recent_tasks.xml b/packages/SystemUI/res/layout/media_projection_recent_tasks.xml new file mode 100644 index 000000000000..a2b3c404f077 --- /dev/null +++ b/packages/SystemUI/res/layout/media_projection_recent_tasks.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2022 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. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="vertical" + android:background="?android:attr/colorBackground" + > + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="256dp"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/media_projection_recent_tasks_recycler" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + /> + + <ProgressBar + android:id="@+id/media_projection_recent_tasks_loader" + android:layout_width="@dimen/media_projection_app_selector_loader_size" + android:layout_height="@dimen/media_projection_app_selector_loader_size" + android:layout_gravity="center" + android:indeterminate="true" + android:indeterminateOnly="true" /> + </FrameLayout> + + <!-- Divider line --> + <ImageView + android:layout_width="72dp" + android:layout_height="2dp" + android:layout_marginBottom="8dp" + android:layout_marginTop="24dp" + android:importantForAccessibility="no" + android:src="@*android:drawable/ic_drag_handle" + android:tint="?android:attr/textColorSecondary" /> +</LinearLayout> diff --git a/packages/SystemUI/res/layout/media_projection_task_item.xml b/packages/SystemUI/res/layout/media_projection_task_item.xml new file mode 100644 index 000000000000..75f73cd7a1e9 --- /dev/null +++ b/packages/SystemUI/res/layout/media_projection_task_item.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2022 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. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + android:orientation="vertical"> + + <ImageView + android:id="@+id/task_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_margin="8dp" + android:importantForAccessibility="no" /> + + <!-- TODO(b/240924926) use a custom view that will handle thumbnail cropping correctly --> + <!-- TODO(b/240924926) dynamically change the view size based on the screen size --> + <ImageView + android:id="@+id/task_thumbnail" + android:layout_width="100dp" + android:layout_height="216dp" + android:scaleType="centerCrop" + /> +</LinearLayout> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index eb6c45747924..e09a6ee91fc6 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1454,6 +1454,8 @@ <dimen name="fgs_manager_list_top_spacing">12dp</dimen> <dimen name="media_projection_app_selector_icon_size">32dp</dimen> + <dimen name="media_projection_app_selector_recents_padding">16dp</dimen> + <dimen name="media_projection_app_selector_loader_size">32dp</dimen> <!-- Dream overlay related dimensions --> <dimen name="dream_overlay_status_bar_height">60dp</dimen> diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt index 0f1ee31e066e..c6bd777fbd7a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt @@ -24,22 +24,45 @@ import android.os.Bundle import android.os.IBinder import android.os.ResultReceiver import android.os.UserHandle -import android.widget.ImageView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.android.internal.annotations.VisibleForTesting import com.android.internal.app.ChooserActivity import com.android.internal.app.ResolverListController import com.android.internal.app.chooser.NotSelectableTargetInfo import com.android.internal.app.chooser.TargetInfo -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.util.AsyncActivityLauncher import com.android.systemui.R -import com.android.internal.R as AndroidR +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorView +import com.android.systemui.mediaprojection.appselector.data.RecentTask +import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter +import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter.RecentTaskClickListener +import com.android.systemui.util.AsyncActivityLauncher +import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration +import javax.inject.Inject -class MediaProjectionAppSelectorActivity constructor( +class MediaProjectionAppSelectorActivity( private val activityLauncher: AsyncActivityLauncher, + private val controller: MediaProjectionAppSelectorController, + private val recentTasksAdapterFactory: RecentTasksAdapter.Factory, /** This is used to override the dependency in a screenshot test */ @VisibleForTesting - private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)? = null -) : ChooserActivity() { + private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)? +) : ChooserActivity(), MediaProjectionAppSelectorView, RecentTaskClickListener { + + @Inject + constructor( + activityLauncher: AsyncActivityLauncher, + controller: MediaProjectionAppSelectorController, + recentTasksAdapterFactory: RecentTasksAdapter.Factory, + ) : this(activityLauncher, controller, recentTasksAdapterFactory, null) + + private var recentsRoot: ViewGroup? = null + private var recentsProgress: View? = null + private var recentsRecycler: RecyclerView? = null override fun getLayoutResource() = R.layout.media_projection_app_selector @@ -52,10 +75,30 @@ class MediaProjectionAppSelectorActivity constructor( // TODO(b/240939253): update copies val title = getString(R.string.media_projection_dialog_service_title) intent.putExtra(Intent.EXTRA_TITLE, title) - super.onCreate(bundle) + controller.init(this) + } - requireViewById<ImageView>(AndroidR.id.icon).setImageResource(R.drawable.ic_present_to_all) + private fun createRecentsView(parent: ViewGroup): ViewGroup { + val recentsRoot = LayoutInflater.from(this) + .inflate(R.layout.media_projection_recent_tasks, parent, + /* attachToRoot= */ false) as ViewGroup + + recentsProgress = recentsRoot.requireViewById(R.id.media_projection_recent_tasks_loader) + recentsRecycler = recentsRoot.requireViewById(R.id.media_projection_recent_tasks_recycler) + recentsRecycler?.layoutManager = LinearLayoutManager( + this, LinearLayoutManager.HORIZONTAL, + /* reverseLayout= */false + ) + + val itemDecoration = HorizontalSpacerItemDecoration( + resources.getDimensionPixelOffset( + R.dimen.media_projection_app_selector_recents_padding + ) + ) + recentsRecycler?.addItemDecoration(itemDecoration) + + return recentsRoot } override fun appliedThemeResId(): Int = @@ -108,6 +151,7 @@ class MediaProjectionAppSelectorActivity constructor( override fun onDestroy() { activityLauncher.destroy() + controller.destroy() super.onDestroy() } @@ -115,6 +159,27 @@ class MediaProjectionAppSelectorActivity constructor( // do nothing } + override fun bind(recentTasks: List<RecentTask>) { + val recents = recentsRoot ?: return + val progress = recentsProgress ?: return + val recycler = recentsRecycler ?: return + + if (recentTasks.isEmpty()) { + recents.visibility = View.GONE + return + } + + progress.visibility = View.GONE + recycler.visibility = View.VISIBLE + recents.visibility = View.VISIBLE + + recycler.adapter = recentTasksAdapterFactory.create(recentTasks, this) + } + + override fun onRecentClicked(task: RecentTask, view: View) { + // TODO(b/240924732) Handle clicking on a recent task + } + private fun onTargetActivityLaunched(launchToken: IBinder) { if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) { // The client requested to return the result in the result receiver instead of @@ -145,6 +210,14 @@ class MediaProjectionAppSelectorActivity constructor( override fun shouldGetOnlyDefaultActivities() = false + // TODO(b/240924732) flip the flag when the recents selector is ready + override fun shouldShowContentPreview() = false + + override fun createContentPreviewView(parent: ViewGroup): ViewGroup = + recentsRoot ?: createRecentsView(parent).also { + recentsRoot = it + } + companion object { /** * When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt index 969699834024..185b4fca87d0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt @@ -17,13 +17,29 @@ package com.android.systemui.media.dagger import android.app.Activity +import android.content.ComponentName +import android.content.Context +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.media.MediaProjectionAppSelectorActivity -import com.android.systemui.util.AsyncActivityLauncher +import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController +import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader +import com.android.systemui.mediaprojection.appselector.data.AppIconLoader +import com.android.systemui.mediaprojection.appselector.data.IconLoaderLibAppIconLoader +import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider +import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader +import com.android.systemui.mediaprojection.appselector.data.ShellRecentTaskListProvider import dagger.Binds import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class MediaProjectionAppSelector @Module abstract class MediaProjectionModule { @@ -31,17 +47,43 @@ abstract class MediaProjectionModule { @Binds @IntoMap @ClassKey(MediaProjectionAppSelectorActivity::class) - abstract fun bindMediaProjectionAppSelectorActivity( - activity: MediaProjectionAppSelectorActivity): Activity + abstract fun provideMediaProjectionAppSelectorActivity( + activity: MediaProjectionAppSelectorActivity + ): Activity + + @Binds + abstract fun bindRecentTaskThumbnailLoader( + impl: ActivityTaskManagerThumbnailLoader + ): RecentTaskThumbnailLoader + + @Binds + abstract fun bindRecentTaskListProvider( + impl: ShellRecentTaskListProvider + ): RecentTaskListProvider + + @Binds + abstract fun bindAppIconLoader(impl: IconLoaderLibAppIconLoader): AppIconLoader companion object { @Provides - fun provideMediaProjectionAppSelectorActivity( - activityLauncher: AsyncActivityLauncher - ): MediaProjectionAppSelectorActivity { - return MediaProjectionAppSelectorActivity( - activityLauncher + fun provideController( + recentTaskListProvider: RecentTaskListProvider, + context: Context, + @MediaProjectionAppSelector scope: CoroutineScope + ): MediaProjectionAppSelectorController { + val appSelectorComponentName = + ComponentName(context, MediaProjectionAppSelectorActivity::class.java) + + return MediaProjectionAppSelectorController( + recentTaskListProvider, + scope, + appSelectorComponentName ) } + + @MediaProjectionAppSelector + @Provides + fun provideCoroutineScope(@Application applicationScope: CoroutineScope): CoroutineScope = + CoroutineScope(applicationScope.coroutineContext + SupervisorJob()) } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt new file mode 100644 index 000000000000..59c6635ed8bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 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.mediaprojection.appselector + +import android.content.ComponentName +import com.android.systemui.media.dagger.MediaProjectionAppSelector +import com.android.systemui.mediaprojection.appselector.data.RecentTask +import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +class MediaProjectionAppSelectorController( + private val recentTaskListProvider: RecentTaskListProvider, + @MediaProjectionAppSelector private val scope: CoroutineScope, + private val appSelectorComponentName: ComponentName +) { + + fun init(view: MediaProjectionAppSelectorView) { + scope.launch { + val tasks = recentTaskListProvider.loadRecentTasks().sortTasks() + view.bind(tasks) + } + } + + fun destroy() { + scope.cancel() + } + + private fun List<RecentTask>.sortTasks(): List<RecentTask> = + asReversed().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/MediaProjectionAppSelectorView.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorView.kt new file mode 100644 index 000000000000..6550aa5569e4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorView.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2022 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.mediaprojection.appselector + +import com.android.systemui.mediaprojection.appselector.data.RecentTask + +interface MediaProjectionAppSelectorView { + fun bind(recentTasks: List<RecentTask>) +} 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 new file mode 100644 index 000000000000..0bdddfe2821d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 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.mediaprojection.appselector.data + +import android.content.ComponentName +import android.graphics.drawable.Drawable +import com.android.systemui.dagger.qualifiers.Background +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import javax.inject.Inject + +interface AppIconLoader { + suspend fun loadIcon(userId: Int, component: ComponentName): Drawable? +} + +class IconLoaderLibAppIconLoader +@Inject +constructor( + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : 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 + } +} 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 new file mode 100644 index 000000000000..6d67e28e15e4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 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.mediaprojection.appselector.data + +import android.content.ComponentName + +data class RecentTask( + val taskId: Int, + val userId: Int, + val topActivityComponent: ComponentName?, + val baseIntentComponent: ComponentName? +) 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 new file mode 100644 index 000000000000..5a0943556d9d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 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.mediaprojection.appselector.data + +import com.android.systemui.dagger.qualifiers.Background +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import javax.inject.Inject + +interface RecentTaskListProvider { + suspend fun loadRecentTasks(): List<RecentTask> +} + +class ShellRecentTaskListProvider +@Inject +constructor(@Background private val coroutineDispatcher: CoroutineDispatcher) : + RecentTaskListProvider { + + override suspend fun loadRecentTasks(): List<RecentTask> = + withContext(coroutineDispatcher) { + // TODO(b/240924731): add blocking call to load the recents + emptyList() + } +} 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 new file mode 100644 index 000000000000..429128032f40 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskThumbnailLoader.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 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.mediaprojection.appselector.data + +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.shared.recents.model.ThumbnailData +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +interface RecentTaskThumbnailLoader { + suspend fun loadThumbnail(taskId: Int): ThumbnailData? +} + +class ActivityTaskManagerThumbnailLoader +@Inject +constructor( + @Background private val coroutineDispatcher: CoroutineDispatcher, +) : + RecentTaskThumbnailLoader { + + override suspend fun loadThumbnail(taskId: Int): ThumbnailData? = + withContext(coroutineDispatcher) { + // TODO(b/240924731): add blocking call to load a thumbnail + null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt new file mode 100644 index 000000000000..ec5abc7a12f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2022 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.mediaprojection.appselector.view + +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import com.android.systemui.R +import com.android.systemui.media.dagger.MediaProjectionAppSelector +import com.android.systemui.mediaprojection.appselector.data.AppIconLoader +import com.android.systemui.mediaprojection.appselector.data.RecentTask +import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +class RecentTaskViewHolder @AssistedInject constructor( + @Assisted root: ViewGroup, + private val iconLoader: AppIconLoader, + private val thumbnailLoader: RecentTaskThumbnailLoader, + @MediaProjectionAppSelector private val scope: CoroutineScope +) : RecyclerView.ViewHolder(root) { + + private val iconView: ImageView = root.requireViewById(R.id.task_icon) + private val thumbnailView: ImageView = root.requireViewById(R.id.task_thumbnail) + + private var job: Job? = null + + fun bind(task: RecentTask, onClick: (View) -> Unit) { + job?.cancel() + + job = + scope.launch { + task.baseIntentComponent?.let { component -> + launch { + val icon = iconLoader.loadIcon(task.userId, component) + iconView.setImageDrawable(icon) + } + } + launch { + val thumbnail = thumbnailLoader.loadThumbnail(task.taskId) + thumbnailView.setImageBitmap(thumbnail?.thumbnail) + } + } + + thumbnailView.setOnClickListener(onClick) + } + + fun onRecycled() { + iconView.setImageDrawable(null) + thumbnailView.setImageBitmap(null) + job?.cancel() + job = null + } + + @AssistedFactory + fun interface Factory { + fun create(root: ViewGroup): RecentTaskViewHolder + } +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt new file mode 100644 index 000000000000..ec9cfa88f34d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 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.mediaprojection.appselector.view + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.android.systemui.R +import com.android.systemui.mediaprojection.appselector.data.RecentTask +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class RecentTasksAdapter @AssistedInject constructor( + @Assisted private val items: List<RecentTask>, + @Assisted private val listener: RecentTaskClickListener, + private val viewHolderFactory: RecentTaskViewHolder.Factory +) : RecyclerView.Adapter<RecentTaskViewHolder>() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentTaskViewHolder { + val taskItem = + LayoutInflater.from(parent.context) + .inflate(R.layout.media_projection_task_item, null) as ViewGroup + + return viewHolderFactory.create(taskItem) + } + + override fun onBindViewHolder(holder: RecentTaskViewHolder, position: Int) { + val task = items[position] + holder.bind(task, onClick = { + listener.onRecentClicked(task, holder.itemView) + }) + } + + override fun getItemCount(): Int = items.size + + override fun onViewRecycled(holder: RecentTaskViewHolder) { + holder.onRecycled() + } + + interface RecentTaskClickListener { + fun onRecentClicked(task: RecentTask, view: View) + } + + @AssistedFactory + fun interface Factory { + fun create(items: List<RecentTask>, listener: RecentTaskClickListener): RecentTasksAdapter + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/recycler/HorizontalSpacerItemDecoration.kt b/packages/SystemUI/src/com/android/systemui/util/recycler/HorizontalSpacerItemDecoration.kt new file mode 100644 index 000000000000..ac931e510139 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/recycler/HorizontalSpacerItemDecoration.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 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.util.recycler + +import android.graphics.Rect +import android.view.View +import androidx.annotation.Dimension +import androidx.recyclerview.widget.RecyclerView + +/** + * RecyclerView ItemDecorator that adds a horizontal space of the given size between items + * and double that space on the ends. + */ +class HorizontalSpacerItemDecoration(@Dimension private val offset: Int) : + RecyclerView.ItemDecoration() { + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val position: Int = parent.getChildAdapterPosition(view) + val itemCount = parent.adapter?.itemCount ?: 0 + + val left = if (position == 0) offset * 2 else offset + val right = if (position == itemCount - 1) offset * 2 else offset + + outRect.set(left, 0, right, 0) + } +} 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 new file mode 100644 index 000000000000..37b7f47fb69e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt @@ -0,0 +1,119 @@ +package com.android.systemui.mediaprojection.appselector + +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.mediaprojection.appselector.data.RecentTask +import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider +import com.android.systemui.util.mockito.mock +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { + + private val taskListProvider = TestRecentTaskListProvider() + private val scope = CoroutineScope(Dispatchers.Unconfined) + private val appSelectorComponentName = ComponentName("com.test", "AppSelector") + + private val view: MediaProjectionAppSelectorView = mock() + + private val controller = MediaProjectionAppSelectorController( + taskListProvider, + scope, + appSelectorComponentName + ) + + @Test + fun initNoRecentTasks_bindsEmptyList() { + taskListProvider.tasks = emptyList() + + controller.init(view) + + verify(view).bind(emptyList()) + } + + @Test + fun initOneRecentTask_bindsList() { + taskListProvider.tasks = listOf( + createRecentTask(taskId = 1) + ) + + controller.init(view) + + verify(view).bind( + listOf( + createRecentTask(taskId = 1) + ) + ) + } + + @Test + fun initMultipleRecentTasksWithoutAppSelectorTask_bindsListInReverse() { + val tasks = listOf( + createRecentTask(taskId = 1), + createRecentTask(taskId = 2), + createRecentTask(taskId = 3), + ) + taskListProvider.tasks = tasks + + controller.init(view) + + verify(view).bind( + listOf( + createRecentTask(taskId = 3), + createRecentTask(taskId = 2), + createRecentTask(taskId = 1), + ) + ) + } + + @Test + fun initRecentTasksWithAppSelectorTasks_bindsListInReverseAndAppSelectorTasksAtTheEnd() { + val tasks = listOf( + createRecentTask(taskId = 1), + createRecentTask(taskId = 2, topActivityComponent = appSelectorComponentName), + createRecentTask(taskId = 3), + createRecentTask(taskId = 4, topActivityComponent = appSelectorComponentName), + createRecentTask(taskId = 5), + ) + taskListProvider.tasks = tasks + + controller.init(view) + + verify(view).bind( + listOf( + createRecentTask(taskId = 5), + createRecentTask(taskId = 3), + createRecentTask(taskId = 1), + createRecentTask(taskId = 4, topActivityComponent = appSelectorComponentName), + createRecentTask(taskId = 2, topActivityComponent = appSelectorComponentName), + ) + ) + } + + private fun createRecentTask( + taskId: Int, + topActivityComponent: ComponentName? = null + ): RecentTask { + return RecentTask( + taskId = taskId, + topActivityComponent = topActivityComponent, + baseIntentComponent = ComponentName("com", "Test"), + userId = 0 + ) + } + + private class TestRecentTaskListProvider : RecentTaskListProvider { + + var tasks: List<RecentTask> = emptyList() + + override suspend fun loadRecentTasks(): List<RecentTask> = tasks + + } +} |