Merge "[Partial Screensharing] Calculate recents thumbnail size" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
c0c99d69fb
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user