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:
@@ -49,6 +49,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||||
|
android:focusable="false"
|
||||||
|
android:clickable="false"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:paddingBottom="@*android:dimen/chooser_view_spacing"
|
android:paddingBottom="@*android:dimen/chooser_view_spacing"
|
||||||
android:paddingLeft="24dp"
|
android:paddingLeft="24dp"
|
||||||
|
|||||||
@@ -23,8 +23,9 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
|
android:id="@+id/media_projection_recent_tasks_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="256dp">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/media_projection_recent_tasks_recycler"
|
android:id="@+id/media_projection_recent_tasks_recycler"
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical"
|
||||||
|
android:clickable="true"
|
||||||
|
>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/task_icon"
|
android:id="@+id/task_icon"
|
||||||
@@ -27,12 +29,12 @@
|
|||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
android:importantForAccessibility="no" />
|
android:importantForAccessibility="no" />
|
||||||
|
|
||||||
<!-- TODO(b/240924926) use a custom view that will handle thumbnail cropping correctly -->
|
<!-- This view size will be calculated in runtime -->
|
||||||
<!-- TODO(b/240924926) dynamically change the view size based on the screen size -->
|
<com.android.systemui.mediaprojection.appselector.view.MediaProjectionTaskView
|
||||||
<ImageView
|
|
||||||
android:id="@+id/task_thumbnail"
|
android:id="@+id/task_thumbnail"
|
||||||
android:layout_width="100dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="216dp"
|
android:layout_height="wrap_content"
|
||||||
android:scaleType="centerCrop"
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
/>
|
/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1457,6 +1457,9 @@
|
|||||||
<dimen name="media_projection_app_selector_icon_size">32dp</dimen>
|
<dimen name="media_projection_app_selector_icon_size">32dp</dimen>
|
||||||
<dimen name="media_projection_app_selector_recents_padding">16dp</dimen>
|
<dimen name="media_projection_app_selector_recents_padding">16dp</dimen>
|
||||||
<dimen name="media_projection_app_selector_loader_size">32dp</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 -->
|
<!-- Dream overlay related dimensions -->
|
||||||
<dimen name="dream_overlay_status_bar_height">60dp</dimen>
|
<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.fragments.FragmentService;
|
||||||
import com.android.systemui.keyguard.data.BouncerViewModule;
|
import com.android.systemui.keyguard.data.BouncerViewModule;
|
||||||
import com.android.systemui.log.dagger.LogModule;
|
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.model.SysUiState;
|
||||||
import com.android.systemui.navigationbar.NavigationBarComponent;
|
import com.android.systemui.navigationbar.NavigationBarComponent;
|
||||||
import com.android.systemui.people.PeopleModule;
|
import com.android.systemui.people.PeopleModule;
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
package com.android.systemui.media
|
package com.android.systemui.media
|
||||||
|
|
||||||
import android.app.ActivityOptions
|
import android.app.ActivityOptions
|
||||||
import android.app.IActivityTaskManager
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.media.projection.IMediaProjection
|
import android.media.projection.IMediaProjection
|
||||||
import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
|
import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
|
||||||
import android.os.Binder
|
import android.os.Binder
|
||||||
@@ -25,87 +25,73 @@ import android.os.Bundle
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.ResultReceiver
|
import android.os.ResultReceiver
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.android.internal.annotations.VisibleForTesting
|
import com.android.internal.annotations.VisibleForTesting
|
||||||
import com.android.internal.app.ChooserActivity
|
import com.android.internal.app.ChooserActivity
|
||||||
import com.android.internal.app.ResolverListController
|
import com.android.internal.app.ResolverListController
|
||||||
import com.android.internal.app.chooser.NotSelectableTargetInfo
|
import com.android.internal.app.chooser.NotSelectableTargetInfo
|
||||||
import com.android.internal.app.chooser.TargetInfo
|
import com.android.internal.app.chooser.TargetInfo
|
||||||
import com.android.systemui.R
|
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.MediaProjectionAppSelectorController
|
||||||
|
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
|
||||||
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorView
|
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorView
|
||||||
import com.android.systemui.mediaprojection.appselector.data.RecentTask
|
import com.android.systemui.mediaprojection.appselector.data.RecentTask
|
||||||
import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter
|
import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
|
||||||
import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter.RecentTaskClickListener
|
import com.android.systemui.statusbar.policy.ConfigurationController
|
||||||
import com.android.systemui.util.AsyncActivityLauncher
|
import com.android.systemui.util.AsyncActivityLauncher
|
||||||
import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MediaProjectionAppSelectorActivity(
|
class MediaProjectionAppSelectorActivity(
|
||||||
|
private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
|
||||||
private val activityLauncher: AsyncActivityLauncher,
|
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 */
|
/** This is used to override the dependency in a screenshot test */
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
|
private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
|
||||||
) : ChooserActivity(), MediaProjectionAppSelectorView, RecentTaskClickListener {
|
) : ChooserActivity(), MediaProjectionAppSelectorView, MediaProjectionAppSelectorResultHandler {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
componentFactory: MediaProjectionAppSelectorComponent.Factory,
|
||||||
activityLauncher: AsyncActivityLauncher,
|
activityLauncher: AsyncActivityLauncher,
|
||||||
activityTaskManager: IActivityTaskManager,
|
) : this(componentFactory, activityLauncher, null)
|
||||||
controller: MediaProjectionAppSelectorController,
|
|
||||||
recentTasksAdapterFactory: RecentTasksAdapter.Factory,
|
|
||||||
) : this(activityLauncher, activityTaskManager, controller, recentTasksAdapterFactory, null)
|
|
||||||
|
|
||||||
private var recentsRoot: ViewGroup? = null
|
private lateinit var configurationController: ConfigurationController
|
||||||
private var recentsProgress: View? = null
|
private lateinit var controller: MediaProjectionAppSelectorController
|
||||||
private var recentsRecycler: RecyclerView? = null
|
private lateinit var recentsViewController: MediaProjectionRecentsViewController
|
||||||
|
|
||||||
override fun getLayoutResource() =
|
override fun getLayoutResource() = R.layout.media_projection_app_selector
|
||||||
R.layout.media_projection_app_selector
|
|
||||||
|
|
||||||
public override fun onCreate(bundle: Bundle?) {
|
public override fun onCreate(bundle: Bundle?) {
|
||||||
val queryIntent = Intent(Intent.ACTION_MAIN)
|
val component =
|
||||||
.addCategory(Intent.CATEGORY_LAUNCHER)
|
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)
|
intent.putExtra(Intent.EXTRA_INTENT, queryIntent)
|
||||||
|
|
||||||
// TODO(b/240939253): update copies
|
// TODO(b/240939253): update copies
|
||||||
val title = getString(R.string.media_projection_dialog_service_title)
|
val title = getString(R.string.media_projection_dialog_service_title)
|
||||||
intent.putExtra(Intent.EXTRA_TITLE, title)
|
intent.putExtra(Intent.EXTRA_TITLE, title)
|
||||||
super.onCreate(bundle)
|
super.onCreate(bundle)
|
||||||
controller.init(this)
|
controller.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRecentsView(parent: ViewGroup): ViewGroup {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
val recentsRoot = LayoutInflater.from(this)
|
super.onConfigurationChanged(newConfig)
|
||||||
.inflate(R.layout.media_projection_recent_tasks, parent,
|
configurationController.onConfigurationChanged(newConfig)
|
||||||
/* attachToRoot= */ false) as ViewGroup
|
|
||||||
|
|
||||||
recentsProgress = recentsRoot.requireViewById(R.id.media_projection_recent_tasks_loader)
|
|
||||||
recentsRecycler = recentsRoot.requireViewById(R.id.media_projection_recent_tasks_recycler)
|
|
||||||
recentsRecycler?.layoutManager = LinearLayoutManager(
|
|
||||||
this, LinearLayoutManager.HORIZONTAL,
|
|
||||||
/* reverseLayout= */false
|
|
||||||
)
|
|
||||||
|
|
||||||
val itemDecoration = HorizontalSpacerItemDecoration(
|
|
||||||
resources.getDimensionPixelOffset(
|
|
||||||
R.dimen.media_projection_app_selector_recents_padding
|
|
||||||
)
|
|
||||||
)
|
|
||||||
recentsRecycler?.addItemDecoration(itemDecoration)
|
|
||||||
|
|
||||||
return recentsRoot
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun appliedThemeResId(): Int =
|
override fun appliedThemeResId(): Int = R.style.Theme_SystemUI_MediaProjectionAppSelector
|
||||||
R.style.Theme_SystemUI_MediaProjectionAppSelector
|
|
||||||
|
|
||||||
override fun createListController(userHandle: UserHandle): ResolverListController =
|
override fun createListController(userHandle: UserHandle): ResolverListController =
|
||||||
listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
|
listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
|
||||||
@@ -127,9 +113,9 @@ class MediaProjectionAppSelectorActivity(
|
|||||||
// is typically very fast, so we don't show any loaders.
|
// 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
|
// We wait for the activity to be launched to make sure that the window of the activity
|
||||||
// is created and ready to be captured.
|
// is created and ready to be captured.
|
||||||
val activityStarted = activityLauncher
|
val activityStarted =
|
||||||
.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
|
activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
|
||||||
onTargetActivityLaunched(launchToken)
|
returnSelectedApp(launchToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rely on the ActivityManager to pop up a dialog regarding app suspension
|
// Rely on the ActivityManager to pop up a dialog regarding app suspension
|
||||||
@@ -163,50 +149,27 @@ class MediaProjectionAppSelectorActivity(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(recentTasks: List<RecentTask>) {
|
override fun bind(recentTasks: List<RecentTask>) {
|
||||||
val recents = recentsRoot ?: return
|
recentsViewController.bind(recentTasks)
|
||||||
val progress = recentsProgress ?: return
|
|
||||||
val recycler = recentsRecycler ?: return
|
|
||||||
|
|
||||||
if (recentTasks.isEmpty()) {
|
|
||||||
recents.visibility = View.GONE
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.visibility = View.GONE
|
override fun returnSelectedApp(launchCookie: IBinder) {
|
||||||
recycler.visibility = View.VISIBLE
|
|
||||||
recents.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
recycler.adapter = recentTasksAdapterFactory.create(recentTasks, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
|
if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
|
||||||
// The client requested to return the result in the result receiver instead of
|
// 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
|
// activity result, let's send the media projection to the result receiver
|
||||||
val resultReceiver = intent
|
val resultReceiver =
|
||||||
.getParcelableExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
|
intent.getParcelableExtra(
|
||||||
ResultReceiver::class.java) as ResultReceiver
|
EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
|
||||||
val captureRegion = MediaProjectionCaptureTarget(launchToken)
|
ResultReceiver::class.java
|
||||||
val data = Bundle().apply {
|
) as ResultReceiver
|
||||||
putParcelable(KEY_CAPTURE_TARGET, captureRegion)
|
val captureRegion = MediaProjectionCaptureTarget(launchCookie)
|
||||||
}
|
val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
|
||||||
resultReceiver.send(RESULT_OK, data)
|
resultReceiver.send(RESULT_OK, data)
|
||||||
} else {
|
} else {
|
||||||
// Return the media projection instance as activity result
|
// Return the media projection instance as activity result
|
||||||
val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
|
val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
|
||||||
val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
|
val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
|
||||||
|
|
||||||
projection.launchCookie = launchToken
|
projection.launchCookie = launchCookie
|
||||||
|
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
|
intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
|
||||||
@@ -223,15 +186,13 @@ class MediaProjectionAppSelectorActivity(
|
|||||||
override fun shouldShowContentPreview() = false
|
override fun shouldShowContentPreview() = false
|
||||||
|
|
||||||
override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
|
override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
|
||||||
recentsRoot ?: createRecentsView(parent).also {
|
recentsViewController.createView(parent)
|
||||||
recentsRoot = it
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra
|
* When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra the activity will
|
||||||
* the activity will send the [CaptureRegion] to the result receiver
|
* send the [CaptureRegion] to the result receiver instead of returning media projection
|
||||||
* instead of returning media projection instance through activity result.
|
* instance through activity result.
|
||||||
*/
|
*/
|
||||||
const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver"
|
const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver"
|
||||||
const val KEY_CAPTURE_TARGET = "capture_region"
|
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
|
package com.android.systemui.mediaprojection.appselector
|
||||||
|
|
||||||
import android.content.ComponentName
|
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.RecentTask
|
||||||
import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
|
import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MediaProjectionAppSelectorController(
|
@MediaProjectionAppSelectorScope
|
||||||
|
class MediaProjectionAppSelectorController @Inject constructor(
|
||||||
private val recentTaskListProvider: RecentTaskListProvider,
|
private val recentTaskListProvider: RecentTaskListProvider,
|
||||||
|
private val view: MediaProjectionAppSelectorView,
|
||||||
@MediaProjectionAppSelector private val scope: CoroutineScope,
|
@MediaProjectionAppSelector private val scope: CoroutineScope,
|
||||||
private val appSelectorComponentName: ComponentName
|
@MediaProjectionAppSelector private val appSelectorComponentName: ComponentName
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun init(view: MediaProjectionAppSelectorView) {
|
fun init() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val tasks = recentTaskListProvider.loadRecentTasks().sortTasks()
|
val tasks = recentTaskListProvider.loadRecentTasks().sortTasks()
|
||||||
view.bind(tasks)
|
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
|
package com.android.systemui.mediaprojection.appselector.view
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.android.systemui.R
|
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.AppIconLoader
|
||||||
import com.android.systemui.mediaprojection.appselector.data.RecentTask
|
import com.android.systemui.mediaprojection.appselector.data.RecentTask
|
||||||
import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
|
import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
|
||||||
|
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
@@ -32,19 +34,27 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class RecentTaskViewHolder @AssistedInject constructor(
|
class RecentTaskViewHolder
|
||||||
@Assisted root: ViewGroup,
|
@AssistedInject
|
||||||
|
constructor(
|
||||||
|
@Assisted private val root: ViewGroup,
|
||||||
private val iconLoader: AppIconLoader,
|
private val iconLoader: AppIconLoader,
|
||||||
private val thumbnailLoader: RecentTaskThumbnailLoader,
|
private val thumbnailLoader: RecentTaskThumbnailLoader,
|
||||||
|
private val taskViewSizeProvider: TaskPreviewSizeProvider,
|
||||||
@MediaProjectionAppSelector private val scope: CoroutineScope
|
@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 iconView: ImageView = root.requireViewById(R.id.task_icon)
|
||||||
private val thumbnailView: ImageView = root.requireViewById(R.id.task_thumbnail)
|
|
||||||
|
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
updateThumbnailSize()
|
||||||
|
}
|
||||||
|
|
||||||
fun bind(task: RecentTask, onClick: (View) -> Unit) {
|
fun bind(task: RecentTask, onClick: (View) -> Unit) {
|
||||||
|
taskViewSizeProvider.addCallback(this)
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
|
|
||||||
job =
|
job =
|
||||||
@@ -57,20 +67,33 @@ class RecentTaskViewHolder @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
val thumbnail = thumbnailLoader.loadThumbnail(task.taskId)
|
val thumbnail = thumbnailLoader.loadThumbnail(task.taskId)
|
||||||
thumbnailView.setImageBitmap(thumbnail?.thumbnail)
|
thumbnailView.bindTask(task, thumbnail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbnailView.setOnClickListener(onClick)
|
root.setOnClickListener(onClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onRecycled() {
|
fun onRecycled() {
|
||||||
|
taskViewSizeProvider.removeCallback(this)
|
||||||
iconView.setImageDrawable(null)
|
iconView.setImageDrawable(null)
|
||||||
thumbnailView.setImageBitmap(null)
|
thumbnailView.bindTask(null, null)
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
job = null
|
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
|
@AssistedFactory
|
||||||
fun interface Factory {
|
fun interface Factory {
|
||||||
fun create(root: ViewGroup): RecentTaskViewHolder
|
fun create(root: ViewGroup): RecentTaskViewHolder
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ import dagger.assisted.Assisted
|
|||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
||||||
class RecentTasksAdapter @AssistedInject constructor(
|
class RecentTasksAdapter
|
||||||
|
@AssistedInject
|
||||||
|
constructor(
|
||||||
@Assisted private val items: List<RecentTask>,
|
@Assisted private val items: List<RecentTask>,
|
||||||
@Assisted private val listener: RecentTaskClickListener,
|
@Assisted private val listener: RecentTaskClickListener,
|
||||||
private val viewHolderFactory: RecentTaskViewHolder.Factory
|
private val viewHolderFactory: RecentTaskViewHolder.Factory
|
||||||
@@ -35,7 +37,7 @@ class RecentTasksAdapter @AssistedInject constructor(
|
|||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentTaskViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentTaskViewHolder {
|
||||||
val taskItem =
|
val taskItem =
|
||||||
LayoutInflater.from(parent.context)
|
LayoutInflater.from(parent.context)
|
||||||
.inflate(R.layout.media_projection_task_item, null) as ViewGroup
|
.inflate(R.layout.media_projection_task_item, parent, false) as ViewGroup
|
||||||
|
|
||||||
return viewHolderFactory.create(taskItem)
|
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(
|
private val controller = MediaProjectionAppSelectorController(
|
||||||
taskListProvider,
|
taskListProvider,
|
||||||
|
view,
|
||||||
scope,
|
scope,
|
||||||
appSelectorComponentName
|
appSelectorComponentName
|
||||||
)
|
)
|
||||||
@@ -33,7 +34,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
|
|||||||
fun initNoRecentTasks_bindsEmptyList() {
|
fun initNoRecentTasks_bindsEmptyList() {
|
||||||
taskListProvider.tasks = emptyList()
|
taskListProvider.tasks = emptyList()
|
||||||
|
|
||||||
controller.init(view)
|
controller.init()
|
||||||
|
|
||||||
verify(view).bind(emptyList())
|
verify(view).bind(emptyList())
|
||||||
}
|
}
|
||||||
@@ -44,7 +45,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
|
|||||||
createRecentTask(taskId = 1)
|
createRecentTask(taskId = 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
controller.init(view)
|
controller.init()
|
||||||
|
|
||||||
verify(view).bind(
|
verify(view).bind(
|
||||||
listOf(
|
listOf(
|
||||||
@@ -62,7 +63,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
|
|||||||
)
|
)
|
||||||
taskListProvider.tasks = tasks
|
taskListProvider.tasks = tasks
|
||||||
|
|
||||||
controller.init(view)
|
controller.init()
|
||||||
|
|
||||||
verify(view).bind(
|
verify(view).bind(
|
||||||
listOf(
|
listOf(
|
||||||
@@ -84,7 +85,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
|
|||||||
)
|
)
|
||||||
taskListProvider.tasks = tasks
|
taskListProvider.tasks = tasks
|
||||||
|
|
||||||
controller.init(view)
|
controller.init()
|
||||||
|
|
||||||
verify(view).bind(
|
verify(view).bind(
|
||||||
listOf(
|
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