Merge "[Partial Screensharing] Calculate recents thumbnail size" into tm-qpr-dev am: c0c99d69fb

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20066070

Change-Id: I10f664b3b825f093ed36bb17a30e496eb16c1199
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Nick Chameyev
2022-10-10 09:54:58 +00:00
committed by Automerger Merge Worker
17 changed files with 810 additions and 205 deletions

View File

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

View File

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

View File

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

View File

@@ -1457,6 +1457,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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