summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/layout/media_projection_app_selector.xml2
-rw-r--r--packages/SystemUI/res/layout/media_projection_recent_tasks.xml3
-rw-r--r--packages/SystemUI/res/layout/media_projection_task_item.xml16
-rw-r--r--packages/SystemUI/res/values/dimens.xml3
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt137
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt89
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt123
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt155
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt175
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt95
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt9
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt134
17 files changed, 810 insertions, 205 deletions
diff --git a/packages/SystemUI/res/layout/media_projection_app_selector.xml b/packages/SystemUI/res/layout/media_projection_app_selector.xml
index 226bc6afc5ab..e4749381243a 100644
--- a/packages/SystemUI/res/layout/media_projection_app_selector.xml
+++ b/packages/SystemUI/res/layout/media_projection_app_selector.xml
@@ -49,6 +49,8 @@
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
+ android:focusable="false"
+ android:clickable="false"
android:gravity="center"
android:paddingBottom="@*android:dimen/chooser_view_spacing"
android:paddingLeft="24dp"
diff --git a/packages/SystemUI/res/layout/media_projection_recent_tasks.xml b/packages/SystemUI/res/layout/media_projection_recent_tasks.xml
index a2b3c404f077..31baf26e4a1b 100644
--- a/packages/SystemUI/res/layout/media_projection_recent_tasks.xml
+++ b/packages/SystemUI/res/layout/media_projection_recent_tasks.xml
@@ -23,8 +23,9 @@
>
<FrameLayout
+ android:id="@+id/media_projection_recent_tasks_container"
android:layout_width="match_parent"
- android:layout_height="256dp">
+ android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/media_projection_recent_tasks_recycler"
diff --git a/packages/SystemUI/res/layout/media_projection_task_item.xml b/packages/SystemUI/res/layout/media_projection_task_item.xml
index 75f73cd7a1e9..cfa586fb5767 100644
--- a/packages/SystemUI/res/layout/media_projection_task_item.xml
+++ b/packages/SystemUI/res/layout/media_projection_task_item.xml
@@ -18,7 +18,9 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
- android:orientation="vertical">
+ android:orientation="vertical"
+ android:clickable="true"
+ >
<ImageView
android:id="@+id/task_icon"
@@ -27,12 +29,12 @@
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
+ <!-- This view size will be calculated in runtime -->
+ <com.android.systemui.mediaprojection.appselector.view.MediaProjectionTaskView
android:id="@+id/task_thumbnail"
- android:layout_width="100dp"
- android:layout_height="216dp"
- android:scaleType="centerCrop"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clickable="false"
+ android:focusable="false"
/>
</LinearLayout>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 03040d66ebad..2c6e6068c4b1 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1470,6 +1470,9 @@
<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>
+ <dimen name="media_projection_app_selector_task_rounded_corners">10dp</dimen>
+ <dimen name="media_projection_app_selector_task_icon_size">24dp</dimen>
+ <dimen name="media_projection_app_selector_task_icon_margin">8dp</dimen>
<!-- Dream overlay related dimensions -->
<dimen name="dream_overlay_status_bar_height">60dp</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 06dbab980793..d70b971dba14 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -43,7 +43,7 @@ import com.android.systemui.flags.FlagsModule;
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.data.BouncerViewModule;
import com.android.systemui.log.dagger.LogModule;
-import com.android.systemui.media.dagger.MediaProjectionModule;
+import com.android.systemui.mediaprojection.appselector.MediaProjectionModule;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationBarComponent;
import com.android.systemui.people.PeopleModule;
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index ea5780622822..1ac2a078c8a0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -16,8 +16,8 @@
package com.android.systemui.media
import android.app.ActivityOptions
-import android.app.IActivityTaskManager
import android.content.Intent
+import android.content.res.Configuration
import android.media.projection.IMediaProjection
import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
import android.os.Binder
@@ -25,87 +25,73 @@ import android.os.Bundle
import android.os.IBinder
import android.os.ResultReceiver
import android.os.UserHandle
-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.systemui.R
+import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorComponent
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
+import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
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.mediaprojection.appselector.view.MediaProjectionRecentsViewController
+import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.AsyncActivityLauncher
-import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration
import javax.inject.Inject
class MediaProjectionAppSelectorActivity(
+ private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
private val activityLauncher: AsyncActivityLauncher,
- private val activityTaskManager: IActivityTaskManager,
- 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)?
-) : ChooserActivity(), MediaProjectionAppSelectorView, RecentTaskClickListener {
+) : ChooserActivity(), MediaProjectionAppSelectorView, MediaProjectionAppSelectorResultHandler {
@Inject
constructor(
+ componentFactory: MediaProjectionAppSelectorComponent.Factory,
activityLauncher: AsyncActivityLauncher,
- activityTaskManager: IActivityTaskManager,
- controller: MediaProjectionAppSelectorController,
- recentTasksAdapterFactory: RecentTasksAdapter.Factory,
- ) : this(activityLauncher, activityTaskManager, controller, recentTasksAdapterFactory, null)
+ ) : this(componentFactory, activityLauncher, null)
- private var recentsRoot: ViewGroup? = null
- private var recentsProgress: View? = null
- private var recentsRecycler: RecyclerView? = null
+ private lateinit var configurationController: ConfigurationController
+ private lateinit var controller: MediaProjectionAppSelectorController
+ private lateinit var recentsViewController: MediaProjectionRecentsViewController
- override fun getLayoutResource() =
- R.layout.media_projection_app_selector
+ override fun getLayoutResource() = R.layout.media_projection_app_selector
public override fun onCreate(bundle: Bundle?) {
- val queryIntent = Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_LAUNCHER)
+ val component =
+ componentFactory.create(
+ activity = this,
+ view = this,
+ resultHandler = this
+ )
+
+ // Create a separate configuration controller for this activity as the configuration
+ // might be different from the global one
+ configurationController = component.configurationController
+ controller = component.controller
+ recentsViewController = component.recentsViewController
+
+ val queryIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
intent.putExtra(Intent.EXTRA_INTENT, queryIntent)
// 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)
+ controller.init()
}
- 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 onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ configurationController.onConfigurationChanged(newConfig)
}
- override fun appliedThemeResId(): Int =
- R.style.Theme_SystemUI_MediaProjectionAppSelector
+ override fun appliedThemeResId(): Int = R.style.Theme_SystemUI_MediaProjectionAppSelector
override fun createListController(userHandle: UserHandle): ResolverListController =
listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
@@ -127,9 +113,9 @@ class MediaProjectionAppSelectorActivity(
// is typically very fast, so we don't show any loaders.
// We wait for the activity to be launched to make sure that the window of the activity
// is created and ready to be captured.
- val activityStarted = activityLauncher
- .startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
- onTargetActivityLaunched(launchToken)
+ val activityStarted =
+ activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
+ returnSelectedApp(launchToken)
}
// Rely on the ActivityManager to pop up a dialog regarding app suspension
@@ -163,50 +149,27 @@ class MediaProjectionAppSelectorActivity(
}
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)
+ recentsViewController.bind(recentTasks)
}
- override fun onRecentAppClicked(task: RecentTask, view: View) {
- val launchCookie = Binder()
- val activityOptions = ActivityOptions.makeScaleUpAnimation(view, /* startX= */ 0,
- /* startY= */ 0, view.width, view.height)
- activityOptions.launchCookie = launchCookie
-
- activityTaskManager.startActivityFromRecents(task.taskId, activityOptions.toBundle())
- onTargetActivityLaunched(launchCookie)
- }
-
- private fun onTargetActivityLaunched(launchToken: IBinder) {
+ override fun returnSelectedApp(launchCookie: IBinder) {
if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
// The client requested to return the result in the result receiver instead of
// activity result, let's send the media projection to the result receiver
- val resultReceiver = intent
- .getParcelableExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
- ResultReceiver::class.java) as ResultReceiver
- val captureRegion = MediaProjectionCaptureTarget(launchToken)
- val data = Bundle().apply {
- putParcelable(KEY_CAPTURE_TARGET, captureRegion)
- }
+ val resultReceiver =
+ intent.getParcelableExtra(
+ EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
+ ResultReceiver::class.java
+ ) as ResultReceiver
+ val captureRegion = MediaProjectionCaptureTarget(launchCookie)
+ val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
resultReceiver.send(RESULT_OK, data)
} else {
// Return the media projection instance as activity result
val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
- projection.launchCookie = launchToken
+ projection.launchCookie = launchCookie
val intent = Intent()
intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
@@ -223,15 +186,13 @@ class MediaProjectionAppSelectorActivity(
override fun shouldShowContentPreview() = false
override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
- recentsRoot ?: createRecentsView(parent).also {
- recentsRoot = it
- }
+ recentsViewController.createView(parent)
companion object {
/**
- * When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra
- * the activity will send the [CaptureRegion] to the result receiver
- * instead of returning media projection instance through activity result.
+ * When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra the activity will
+ * send the [CaptureRegion] to the result receiver instead of returning media projection
+ * instance through activity result.
*/
const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver"
const val KEY_CAPTURE_TARGET = "capture_region"
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
deleted file mode 100644
index 185b4fca87d0..000000000000
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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.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.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 {
-
- @Binds
- @IntoMap
- @ClassKey(MediaProjectionAppSelectorActivity::class)
- 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 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/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
new file mode 100644
index 000000000000..7fd100fd1398
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.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.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 com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
+import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider
+import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
+import com.android.systemui.statusbar.policy.ConfigurationController
+import dagger.Binds
+import dagger.BindsInstance
+import dagger.Module
+import dagger.Provides
+import dagger.Subcomponent
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import javax.inject.Qualifier
+import javax.inject.Scope
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+
+@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MediaProjectionAppSelector
+
+@Retention(AnnotationRetention.RUNTIME) @Scope annotation class MediaProjectionAppSelectorScope
+
+@Module(subcomponents = [MediaProjectionAppSelectorComponent::class])
+interface MediaProjectionModule {
+ @Binds
+ @IntoMap
+ @ClassKey(MediaProjectionAppSelectorActivity::class)
+ fun provideMediaProjectionAppSelectorActivity(
+ activity: MediaProjectionAppSelectorActivity
+ ): Activity
+}
+
+/** Scoped values for [MediaProjectionAppSelectorComponent].
+ * We create a scope for the activity so certain dependencies like [TaskPreviewSizeProvider]
+ * could be reused. */
+@Module
+interface MediaProjectionAppSelectorModule {
+
+ @Binds
+ @MediaProjectionAppSelectorScope
+ fun bindRecentTaskThumbnailLoader(
+ impl: ActivityTaskManagerThumbnailLoader
+ ): RecentTaskThumbnailLoader
+
+ @Binds
+ @MediaProjectionAppSelectorScope
+ fun bindRecentTaskListProvider(impl: ShellRecentTaskListProvider): RecentTaskListProvider
+
+ @Binds
+ @MediaProjectionAppSelectorScope
+ fun bindAppIconLoader(impl: IconLoaderLibAppIconLoader): AppIconLoader
+
+ companion object {
+ @Provides
+ @MediaProjectionAppSelector
+ @MediaProjectionAppSelectorScope
+ fun provideAppSelectorComponentName(context: Context): ComponentName =
+ ComponentName(context, MediaProjectionAppSelectorActivity::class.java)
+
+ @Provides
+ @MediaProjectionAppSelector
+ @MediaProjectionAppSelectorScope
+ fun bindConfigurationController(
+ activity: MediaProjectionAppSelectorActivity
+ ): ConfigurationController = ConfigurationControllerImpl(activity)
+
+ @Provides
+ @MediaProjectionAppSelector
+ @MediaProjectionAppSelectorScope
+ fun provideCoroutineScope(@Application applicationScope: CoroutineScope): CoroutineScope =
+ CoroutineScope(applicationScope.coroutineContext + SupervisorJob())
+ }
+}
+
+@Subcomponent(modules = [MediaProjectionAppSelectorModule::class])
+@MediaProjectionAppSelectorScope
+interface MediaProjectionAppSelectorComponent {
+
+ /** Generates [MediaProjectionAppSelectorComponent]. */
+ @Subcomponent.Factory
+ interface Factory {
+ /**
+ * Create a factory to inject the activity into the graph
+ */
+ fun create(
+ @BindsInstance activity: MediaProjectionAppSelectorActivity,
+ @BindsInstance view: MediaProjectionAppSelectorView,
+ @BindsInstance resultHandler: MediaProjectionAppSelectorResultHandler,
+ ): MediaProjectionAppSelectorComponent
+ }
+
+ val controller: MediaProjectionAppSelectorController
+ val recentsViewController: MediaProjectionRecentsViewController
+
+ @MediaProjectionAppSelector val configurationController: ConfigurationController
+}
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 2b381a954e27..d744a40b60d8 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
@@ -17,20 +17,22 @@
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
+import javax.inject.Inject
-class MediaProjectionAppSelectorController(
+@MediaProjectionAppSelectorScope
+class MediaProjectionAppSelectorController @Inject constructor(
private val recentTaskListProvider: RecentTaskListProvider,
+ private val view: MediaProjectionAppSelectorView,
@MediaProjectionAppSelector private val scope: CoroutineScope,
- private val appSelectorComponentName: ComponentName
+ @MediaProjectionAppSelector private val appSelectorComponentName: ComponentName
) {
- fun init(view: MediaProjectionAppSelectorView) {
+ fun init() {
scope.launch {
val tasks = recentTaskListProvider.loadRecentTasks().sortTasks()
view.bind(tasks)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
new file mode 100644
index 000000000000..93c3bce87ad3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt
@@ -0,0 +1,15 @@
+package com.android.systemui.mediaprojection.appselector
+
+import android.os.IBinder
+
+/**
+ * Interface that allows to continue the media projection flow and return the selected app
+ * result to the original caller.
+ */
+interface MediaProjectionAppSelectorResultHandler {
+ /**
+ * Return selected app to the original caller of the media projection app picker.
+ * @param launchCookie launch cookie of the launched activity of the target app
+ */
+ fun returnSelectedApp(launchCookie: IBinder)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
new file mode 100644
index 000000000000..c816446d5c25
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.app.ActivityOptions
+import android.app.IActivityTaskManager
+import android.graphics.Rect
+import android.os.Binder
+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.systemui.R
+import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
+import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorScope
+import com.android.systemui.mediaprojection.appselector.data.RecentTask
+import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter.RecentTaskClickListener
+import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener
+import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration
+import javax.inject.Inject
+
+/**
+ * Controller that handles view of the recent apps selector in the media projection activity.
+ * It is responsible for creating and updating recent apps view.
+ */
+@MediaProjectionAppSelectorScope
+class MediaProjectionRecentsViewController
+@Inject
+constructor(
+ private val recentTasksAdapterFactory: RecentTasksAdapter.Factory,
+ private val taskViewSizeProvider: TaskPreviewSizeProvider,
+ private val activityTaskManager: IActivityTaskManager,
+ private val resultHandler: MediaProjectionAppSelectorResultHandler,
+) : RecentTaskClickListener, TaskPreviewSizeListener {
+
+ private var views: Views? = null
+ private var lastBoundData: List<RecentTask>? = null
+
+ init {
+ taskViewSizeProvider.addCallback(this)
+ }
+
+ fun createView(parent: ViewGroup): ViewGroup =
+ views?.root ?: createRecentViews(parent).also {
+ views = it
+ lastBoundData?.let { recents -> bind(recents) }
+ }.root
+
+ fun bind(recentTasks: List<RecentTask>) {
+ views?.apply {
+ if (recentTasks.isEmpty()) {
+ root.visibility = View.GONE
+ return
+ }
+
+ progress.visibility = View.GONE
+ recycler.visibility = View.VISIBLE
+ root.visibility = View.VISIBLE
+
+ recycler.adapter =
+ recentTasksAdapterFactory.create(
+ recentTasks,
+ this@MediaProjectionRecentsViewController
+ )
+ }
+
+ lastBoundData = recentTasks
+ }
+
+ private fun createRecentViews(parent: ViewGroup): Views {
+ val recentsRoot =
+ LayoutInflater.from(parent.context)
+ .inflate(R.layout.media_projection_recent_tasks, parent, /* attachToRoot= */ false)
+ as ViewGroup
+
+ val container = recentsRoot.findViewById<View>(R.id.media_projection_recent_tasks_container)
+ container.setTaskHeightSize()
+
+ val progress = recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_loader)
+ val recycler =
+ recentsRoot.requireViewById<RecyclerView>(R.id.media_projection_recent_tasks_recycler)
+ recycler.layoutManager =
+ LinearLayoutManager(
+ parent.context,
+ LinearLayoutManager.HORIZONTAL,
+ /* reverseLayout= */ false
+ )
+
+ val itemDecoration =
+ HorizontalSpacerItemDecoration(
+ parent.resources.getDimensionPixelOffset(
+ R.dimen.media_projection_app_selector_recents_padding
+ )
+ )
+ recycler.addItemDecoration(itemDecoration)
+
+ return Views(recentsRoot, container, progress, recycler)
+ }
+
+ override fun onRecentAppClicked(task: RecentTask, view: View) {
+ val launchCookie = Binder()
+ val activityOptions =
+ ActivityOptions.makeScaleUpAnimation(
+ view,
+ /* startX= */ 0,
+ /* startY= */ 0,
+ view.width,
+ view.height
+ )
+ activityOptions.launchCookie = launchCookie
+
+ activityTaskManager.startActivityFromRecents(task.taskId, activityOptions.toBundle())
+ resultHandler.returnSelectedApp(launchCookie)
+ }
+
+ override fun onTaskSizeChanged(size: Rect) {
+ views?.recentsContainer?.setTaskHeightSize()
+ }
+
+ private fun View.setTaskHeightSize() {
+ val thumbnailHeight = taskViewSizeProvider.size.height()
+ val itemHeight =
+ thumbnailHeight +
+ context.resources.getDimensionPixelSize(
+ R.dimen.media_projection_app_selector_task_icon_size
+ ) +
+ context.resources.getDimensionPixelSize(
+ R.dimen.media_projection_app_selector_task_icon_margin
+ ) * 2
+
+ layoutParams = layoutParams.apply { height = itemHeight }
+ }
+
+ private class Views(
+ val root: ViewGroup,
+ val recentsContainer: View,
+ val progress: View,
+ val recycler: RecyclerView
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt
new file mode 100644
index 000000000000..b682bd172837
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionTaskView.kt
@@ -0,0 +1,175 @@
+/*
+ * 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.content.Context
+import android.graphics.BitmapShader
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.Shader
+import android.util.AttributeSet
+import android.view.View
+import android.view.WindowManager
+import androidx.core.content.getSystemService
+import androidx.core.content.res.use
+import com.android.internal.R as AndroidR
+import com.android.systemui.R
+import com.android.systemui.mediaprojection.appselector.data.RecentTask
+import com.android.systemui.shared.recents.model.ThumbnailData
+import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
+import com.android.systemui.shared.recents.utilities.Utilities.isTablet
+
+/**
+ * Custom view that shows a thumbnail preview of one recent task based on [ThumbnailData].
+ * It handles proper cropping and positioning of the thumbnail using [PreviewPositionHelper].
+ */
+class MediaProjectionTaskView
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
+ View(context, attrs, defStyleAttr) {
+
+ private val defaultBackgroundColor: Int
+
+ init {
+ val backgroundColorAttribute = intArrayOf(android.R.attr.colorBackgroundFloating)
+ defaultBackgroundColor =
+ context.obtainStyledAttributes(backgroundColorAttribute).use {
+ it.getColor(/* index= */ 0, /* defValue= */ Color.BLACK)
+ }
+ }
+
+ private val windowManager: WindowManager = context.getSystemService()!!
+ private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ private val backgroundPaint =
+ Paint(Paint.ANTI_ALIAS_FLAG).apply { color = defaultBackgroundColor }
+ private val cornerRadius =
+ context.resources.getDimensionPixelSize(
+ R.dimen.media_projection_app_selector_task_rounded_corners
+ )
+ private val previewPositionHelper = PreviewPositionHelper()
+ private val previewRect = Rect()
+
+ private var task: RecentTask? = null
+ private var thumbnailData: ThumbnailData? = null
+
+ private var bitmapShader: BitmapShader? = null
+
+ fun bindTask(task: RecentTask?, thumbnailData: ThumbnailData?) {
+ this.task = task
+ this.thumbnailData = thumbnailData
+
+ // Strip alpha channel to make sure that the color is not semi-transparent
+ val color = (task?.colorBackground ?: Color.BLACK) or 0xFF000000.toInt()
+
+ paint.color = color
+ backgroundPaint.color = color
+
+ refresh()
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ updateThumbnailMatrix()
+ invalidate()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ // Always draw the background since the snapshots might be translucent or partially empty
+ // (For example, tasks been reparented out of dismissing split root when drag-to-dismiss
+ // split screen).
+ canvas.drawRoundRect(
+ 0f,
+ 1f,
+ width.toFloat(),
+ (height - 1).toFloat(),
+ cornerRadius.toFloat(),
+ cornerRadius.toFloat(),
+ backgroundPaint
+ )
+
+ val drawBackgroundOnly = task == null || bitmapShader == null || thumbnailData == null
+ if (drawBackgroundOnly) {
+ return
+ }
+
+ // Draw the task thumbnail using bitmap shader in the paint
+ canvas.drawRoundRect(
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat(),
+ cornerRadius.toFloat(),
+ cornerRadius.toFloat(),
+ paint
+ )
+ }
+
+ private fun refresh() {
+ val thumbnailBitmap = thumbnailData?.thumbnail
+
+ if (thumbnailBitmap != null) {
+ thumbnailBitmap.prepareToDraw()
+ bitmapShader =
+ BitmapShader(thumbnailBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+ paint.shader = bitmapShader
+ updateThumbnailMatrix()
+ } else {
+ bitmapShader = null
+ paint.shader = null
+ }
+
+ invalidate()
+ }
+
+ private fun updateThumbnailMatrix() {
+ previewPositionHelper.isOrientationChanged = false
+
+ val bitmapShader = bitmapShader ?: return
+ val thumbnailData = thumbnailData ?: return
+ val display = context.display ?: return
+ val windowMetrics = windowManager.maximumWindowMetrics
+
+ previewRect.set(0, 0, thumbnailData.thumbnail.width, thumbnailData.thumbnail.height)
+
+ val currentRotation: Int = display.rotation
+ val displayWidthPx = windowMetrics.bounds.width()
+ val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
+ val isTablet = isTablet(context)
+ val taskbarSize =
+ if (isTablet) {
+ resources.getDimensionPixelSize(AndroidR.dimen.taskbar_frame_height)
+ } else {
+ 0
+ }
+
+ previewPositionHelper.updateThumbnailMatrix(
+ previewRect,
+ thumbnailData,
+ measuredWidth,
+ measuredHeight,
+ displayWidthPx,
+ taskbarSize,
+ isTablet,
+ currentRotation,
+ isRtl
+ )
+
+ bitmapShader.setLocalMatrix(previewPositionHelper.matrix)
+ paint.shader = bitmapShader
+ }
+}
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
index ec5abc7a12f4..15cfeee5174e 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt
@@ -16,15 +16,17 @@
package com.android.systemui.mediaprojection.appselector.view
+import android.graphics.Rect
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.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 com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -32,19 +34,27 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-class RecentTaskViewHolder @AssistedInject constructor(
- @Assisted root: ViewGroup,
+class RecentTaskViewHolder
+@AssistedInject
+constructor(
+ @Assisted private val root: ViewGroup,
private val iconLoader: AppIconLoader,
private val thumbnailLoader: RecentTaskThumbnailLoader,
+ private val taskViewSizeProvider: TaskPreviewSizeProvider,
@MediaProjectionAppSelector private val scope: CoroutineScope
-) : RecyclerView.ViewHolder(root) {
+) : RecyclerView.ViewHolder(root), ConfigurationListener, TaskPreviewSizeProvider.TaskPreviewSizeListener {
+ val thumbnailView: MediaProjectionTaskView = root.requireViewById(R.id.task_thumbnail)
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
+ init {
+ updateThumbnailSize()
+ }
+
fun bind(task: RecentTask, onClick: (View) -> Unit) {
+ taskViewSizeProvider.addCallback(this)
job?.cancel()
job =
@@ -57,20 +67,33 @@ class RecentTaskViewHolder @AssistedInject constructor(
}
launch {
val thumbnail = thumbnailLoader.loadThumbnail(task.taskId)
- thumbnailView.setImageBitmap(thumbnail?.thumbnail)
+ thumbnailView.bindTask(task, thumbnail)
}
}
- thumbnailView.setOnClickListener(onClick)
+ root.setOnClickListener(onClick)
}
fun onRecycled() {
+ taskViewSizeProvider.removeCallback(this)
iconView.setImageDrawable(null)
- thumbnailView.setImageBitmap(null)
+ thumbnailView.bindTask(null, null)
job?.cancel()
job = null
}
+ override fun onTaskSizeChanged(size: Rect) {
+ updateThumbnailSize()
+ }
+
+ private fun updateThumbnailSize() {
+ thumbnailView.layoutParams =
+ thumbnailView.layoutParams.apply {
+ width = taskViewSizeProvider.size.width()
+ height = taskViewSizeProvider.size.height()
+ }
+ }
+
@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
index 51b4012f88c5..6af50a0eb699 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt
@@ -26,7 +26,9 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-class RecentTasksAdapter @AssistedInject constructor(
+class RecentTasksAdapter
+@AssistedInject
+constructor(
@Assisted private val items: List<RecentTask>,
@Assisted private val listener: RecentTaskClickListener,
private val viewHolderFactory: RecentTaskViewHolder.Factory
@@ -34,8 +36,8 @@ class RecentTasksAdapter @AssistedInject constructor(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentTaskViewHolder {
val taskItem =
- LayoutInflater.from(parent.context)
- .inflate(R.layout.media_projection_task_item, null) as ViewGroup
+ LayoutInflater.from(parent.context)
+ .inflate(R.layout.media_projection_task_item, parent, false) as ViewGroup
return viewHolderFactory.create(taskItem)
}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt
new file mode 100644
index 000000000000..88d5eaaff216
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProvider.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.content.Context
+import android.content.res.Configuration
+import android.graphics.Rect
+import android.view.WindowManager
+import com.android.internal.R as AndroidR
+import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorScope
+import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener
+import com.android.systemui.shared.recents.utilities.Utilities.isTablet
+import com.android.systemui.statusbar.policy.CallbackController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+import javax.inject.Inject
+
+@MediaProjectionAppSelectorScope
+class TaskPreviewSizeProvider
+@Inject
+constructor(
+ private val context: Context,
+ private val windowManager: WindowManager,
+ configurationController: ConfigurationController
+) : CallbackController<TaskPreviewSizeListener>, ConfigurationListener {
+
+ /** Returns the size of the task preview on the screen in pixels */
+ val size: Rect = calculateSize()
+
+ private val listeners = arrayListOf<TaskPreviewSizeListener>()
+
+ init {
+ configurationController.addCallback(this)
+ }
+
+ override fun onConfigChanged(newConfig: Configuration) {
+ val newSize = calculateSize()
+ if (newSize != size) {
+ size.set(newSize)
+ listeners.forEach { it.onTaskSizeChanged(size) }
+ }
+ }
+
+ private fun calculateSize(): Rect {
+ val windowMetrics = windowManager.maximumWindowMetrics
+ val maximumWindowHeight = windowMetrics.bounds.height()
+ val width = windowMetrics.bounds.width()
+ var height = maximumWindowHeight
+
+ val isTablet = isTablet(context)
+ if (isTablet) {
+ val taskbarSize =
+ context.resources.getDimensionPixelSize(AndroidR.dimen.taskbar_frame_height)
+ height -= taskbarSize
+ }
+
+ val previewSize = Rect(0, 0, width, height)
+ val scale = (height / maximumWindowHeight.toFloat()) / SCREEN_HEIGHT_TO_TASK_HEIGHT_RATIO
+ previewSize.scale(scale)
+
+ return previewSize
+ }
+
+ override fun addCallback(listener: TaskPreviewSizeListener) {
+ listeners += listener
+ }
+
+ override fun removeCallback(listener: TaskPreviewSizeListener) {
+ listeners -= listener
+ }
+
+ interface TaskPreviewSizeListener {
+ fun onTaskSizeChanged(size: Rect)
+ }
+}
+
+/**
+ * How many times smaller the task preview should be on the screen comparing to the height of the
+ * screen
+ */
+private const val SCREEN_HEIGHT_TO_TASK_HEIGHT_RATIO = 4f
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 00b1f3268bae..19d2d334b884 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
@@ -25,6 +25,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
private val controller = MediaProjectionAppSelectorController(
taskListProvider,
+ view,
scope,
appSelectorComponentName
)
@@ -33,7 +34,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
fun initNoRecentTasks_bindsEmptyList() {
taskListProvider.tasks = emptyList()
- controller.init(view)
+ controller.init()
verify(view).bind(emptyList())
}
@@ -44,7 +45,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
createRecentTask(taskId = 1)
)
- controller.init(view)
+ controller.init()
verify(view).bind(
listOf(
@@ -62,7 +63,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
)
taskListProvider.tasks = tasks
- controller.init(view)
+ controller.init()
verify(view).bind(
listOf(
@@ -84,7 +85,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
)
taskListProvider.tasks = tasks
- controller.init(view)
+ controller.init()
verify(view).bind(
listOf(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt
new file mode 100644
index 000000000000..464acb68fb07
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/TaskPreviewSizeProviderTest.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.content.Context
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.graphics.Rect
+import android.util.DisplayMetrics.DENSITY_DEFAULT
+import android.view.WindowManager
+import android.view.WindowMetrics
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlin.math.min
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class TaskPreviewSizeProviderTest : SysuiTestCase() {
+
+ private val mockContext: Context = mock()
+ private val resources: Resources = mock()
+ private val windowManager: WindowManager = mock()
+ private val sizeUpdates = arrayListOf<Rect>()
+ private val testConfigurationController = FakeConfigurationController()
+
+ @Before
+ fun setup() {
+ whenever(mockContext.getSystemService(eq(WindowManager::class.java)))
+ .thenReturn(windowManager)
+ whenever(mockContext.resources).thenReturn(resources)
+ }
+
+ @Test
+ fun size_phoneDisplay_thumbnailSizeIsSmallerAndProportionalToTheScreenSize() {
+ givenDisplay(width = 400, height = 600, isTablet = false)
+
+ val size = createSizeProvider().size
+
+ assertThat(size).isEqualTo(Rect(0, 0, 100, 150))
+ }
+
+ @Test
+ fun size_tabletDisplay_thumbnailSizeProportionalToTheScreenSizeExcludingTaskbar() {
+ givenDisplay(width = 400, height = 600, isTablet = true)
+ givenTaskbarSize(20)
+
+ val size = createSizeProvider().size
+
+ assertThat(size).isEqualTo(Rect(0, 0, 97, 140))
+ }
+
+ @Test
+ fun size_phoneDisplayAndRotate_emitsSizeUpdate() {
+ givenDisplay(width = 400, height = 600, isTablet = false)
+ createSizeProvider()
+
+ givenDisplay(width = 600, height = 400, isTablet = false)
+ testConfigurationController.onConfigurationChanged(Configuration())
+
+ assertThat(sizeUpdates).containsExactly(Rect(0, 0, 150, 100))
+ }
+
+ @Test
+ fun size_phoneDisplayAndRotateConfigurationChange_returnsUpdatedSize() {
+ givenDisplay(width = 400, height = 600, isTablet = false)
+ val sizeProvider = createSizeProvider()
+
+ givenDisplay(width = 600, height = 400, isTablet = false)
+ testConfigurationController.onConfigurationChanged(Configuration())
+
+ assertThat(sizeProvider.size).isEqualTo(Rect(0, 0, 150, 100))
+ }
+
+ private fun givenTaskbarSize(size: Int) {
+ whenever(resources.getDimensionPixelSize(eq(R.dimen.taskbar_frame_height))).thenReturn(size)
+ }
+
+ private fun givenDisplay(width: Int, height: Int, isTablet: Boolean = false) {
+ val bounds = Rect(0, 0, width, height)
+ val windowMetrics = WindowMetrics(bounds, null)
+ whenever(windowManager.maximumWindowMetrics).thenReturn(windowMetrics)
+ whenever(windowManager.currentWindowMetrics).thenReturn(windowMetrics)
+
+ val minDimension = min(width, height)
+
+ // Calculate DPI so the smallest width is either considered as tablet or as phone
+ val targetSmallestWidthDpi =
+ if (isTablet) SMALLEST_WIDTH_DPI_TABLET else SMALLEST_WIDTH_DPI_PHONE
+ val densityDpi = minDimension * DENSITY_DEFAULT / targetSmallestWidthDpi
+
+ val configuration = Configuration(context.resources.configuration)
+ configuration.densityDpi = densityDpi
+ whenever(resources.configuration).thenReturn(configuration)
+ }
+
+ private fun createSizeProvider(): TaskPreviewSizeProvider {
+ val listener =
+ object : TaskPreviewSizeListener {
+ override fun onTaskSizeChanged(size: Rect) {
+ sizeUpdates.add(size)
+ }
+ }
+
+ return TaskPreviewSizeProvider(mockContext, windowManager, testConfigurationController)
+ .also { it.addCallback(listener) }
+ }
+
+ private companion object {
+ private const val SMALLEST_WIDTH_DPI_TABLET = 800
+ private const val SMALLEST_WIDTH_DPI_PHONE = 400
+ }
+}