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

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

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

View File

@@ -49,6 +49,8 @@
android:layout_height="wrap_content" android:layout_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"

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
recycler.visibility = View.VISIBLE
recents.visibility = View.VISIBLE
recycler.adapter = recentTasksAdapterFactory.create(recentTasks, this)
} }
override fun onRecentAppClicked(task: RecentTask, view: View) { override fun returnSelectedApp(launchCookie: IBinder) {
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"

View File

@@ -1,89 +0,0 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.dagger
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.media.MediaProjectionAppSelectorActivity
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader
import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
import com.android.systemui.mediaprojection.appselector.data.IconLoaderLibAppIconLoader
import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
import com.android.systemui.mediaprojection.appselector.data.ShellRecentTaskListProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MediaProjectionAppSelector
@Module
abstract class MediaProjectionModule {
@Binds
@IntoMap
@ClassKey(MediaProjectionAppSelectorActivity::class)
abstract fun provideMediaProjectionAppSelectorActivity(
activity: MediaProjectionAppSelectorActivity
): Activity
@Binds
abstract fun bindRecentTaskThumbnailLoader(
impl: ActivityTaskManagerThumbnailLoader
): RecentTaskThumbnailLoader
@Binds
abstract fun bindRecentTaskListProvider(
impl: ShellRecentTaskListProvider
): RecentTaskListProvider
@Binds
abstract fun bindAppIconLoader(impl: IconLoaderLibAppIconLoader): AppIconLoader
companion object {
@Provides
fun provideController(
recentTaskListProvider: RecentTaskListProvider,
context: Context,
@MediaProjectionAppSelector scope: CoroutineScope
): MediaProjectionAppSelectorController {
val appSelectorComponentName =
ComponentName(context, MediaProjectionAppSelectorActivity::class.java)
return MediaProjectionAppSelectorController(
recentTaskListProvider,
scope,
appSelectorComponentName
)
}
@MediaProjectionAppSelector
@Provides
fun provideCoroutineScope(@Application applicationScope: CoroutineScope): CoroutineScope =
CoroutineScope(applicationScope.coroutineContext + SupervisorJob())
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.mediaprojection.appselector
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.media.MediaProjectionAppSelectorActivity
import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader
import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
import com.android.systemui.mediaprojection.appselector.data.IconLoaderLibAppIconLoader
import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
import com.android.systemui.mediaprojection.appselector.data.ShellRecentTaskListProvider
import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl
import com.android.systemui.statusbar.policy.ConfigurationController
import dagger.Binds
import dagger.BindsInstance
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Qualifier
import javax.inject.Scope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class MediaProjectionAppSelector
@Retention(AnnotationRetention.RUNTIME) @Scope annotation class MediaProjectionAppSelectorScope
@Module(subcomponents = [MediaProjectionAppSelectorComponent::class])
interface MediaProjectionModule {
@Binds
@IntoMap
@ClassKey(MediaProjectionAppSelectorActivity::class)
fun provideMediaProjectionAppSelectorActivity(
activity: MediaProjectionAppSelectorActivity
): Activity
}
/** Scoped values for [MediaProjectionAppSelectorComponent].
* We create a scope for the activity so certain dependencies like [TaskPreviewSizeProvider]
* could be reused. */
@Module
interface MediaProjectionAppSelectorModule {
@Binds
@MediaProjectionAppSelectorScope
fun bindRecentTaskThumbnailLoader(
impl: ActivityTaskManagerThumbnailLoader
): RecentTaskThumbnailLoader
@Binds
@MediaProjectionAppSelectorScope
fun bindRecentTaskListProvider(impl: ShellRecentTaskListProvider): RecentTaskListProvider
@Binds
@MediaProjectionAppSelectorScope
fun bindAppIconLoader(impl: IconLoaderLibAppIconLoader): AppIconLoader
companion object {
@Provides
@MediaProjectionAppSelector
@MediaProjectionAppSelectorScope
fun provideAppSelectorComponentName(context: Context): ComponentName =
ComponentName(context, MediaProjectionAppSelectorActivity::class.java)
@Provides
@MediaProjectionAppSelector
@MediaProjectionAppSelectorScope
fun bindConfigurationController(
activity: MediaProjectionAppSelectorActivity
): ConfigurationController = ConfigurationControllerImpl(activity)
@Provides
@MediaProjectionAppSelector
@MediaProjectionAppSelectorScope
fun provideCoroutineScope(@Application applicationScope: CoroutineScope): CoroutineScope =
CoroutineScope(applicationScope.coroutineContext + SupervisorJob())
}
}
@Subcomponent(modules = [MediaProjectionAppSelectorModule::class])
@MediaProjectionAppSelectorScope
interface MediaProjectionAppSelectorComponent {
/** Generates [MediaProjectionAppSelectorComponent]. */
@Subcomponent.Factory
interface Factory {
/**
* Create a factory to inject the activity into the graph
*/
fun create(
@BindsInstance activity: MediaProjectionAppSelectorActivity,
@BindsInstance view: MediaProjectionAppSelectorView,
@BindsInstance resultHandler: MediaProjectionAppSelectorResultHandler,
): MediaProjectionAppSelectorComponent
}
val controller: MediaProjectionAppSelectorController
val recentsViewController: MediaProjectionRecentsViewController
@MediaProjectionAppSelector val configurationController: ConfigurationController
}

View File

@@ -17,20 +17,22 @@
package com.android.systemui.mediaprojection.appselector 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)

View File

@@ -0,0 +1,15 @@
package com.android.systemui.mediaprojection.appselector
import android.os.IBinder
/**
* Interface that allows to continue the media projection flow and return the selected app
* result to the original caller.
*/
interface MediaProjectionAppSelectorResultHandler {
/**
* Return selected app to the original caller of the media projection app picker.
* @param launchCookie launch cookie of the launched activity of the target app
*/
fun returnSelectedApp(launchCookie: IBinder)
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.mediaprojection.appselector.view
import android.app.ActivityOptions
import android.app.IActivityTaskManager
import android.graphics.Rect
import android.os.Binder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorScope
import com.android.systemui.mediaprojection.appselector.data.RecentTask
import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter.RecentTaskClickListener
import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener
import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration
import javax.inject.Inject
/**
* Controller that handles view of the recent apps selector in the media projection activity.
* It is responsible for creating and updating recent apps view.
*/
@MediaProjectionAppSelectorScope
class MediaProjectionRecentsViewController
@Inject
constructor(
private val recentTasksAdapterFactory: RecentTasksAdapter.Factory,
private val taskViewSizeProvider: TaskPreviewSizeProvider,
private val activityTaskManager: IActivityTaskManager,
private val resultHandler: MediaProjectionAppSelectorResultHandler,
) : RecentTaskClickListener, TaskPreviewSizeListener {
private var views: Views? = null
private var lastBoundData: List<RecentTask>? = null
init {
taskViewSizeProvider.addCallback(this)
}
fun createView(parent: ViewGroup): ViewGroup =
views?.root ?: createRecentViews(parent).also {
views = it
lastBoundData?.let { recents -> bind(recents) }
}.root
fun bind(recentTasks: List<RecentTask>) {
views?.apply {
if (recentTasks.isEmpty()) {
root.visibility = View.GONE
return
}
progress.visibility = View.GONE
recycler.visibility = View.VISIBLE
root.visibility = View.VISIBLE
recycler.adapter =
recentTasksAdapterFactory.create(
recentTasks,
this@MediaProjectionRecentsViewController
)
}
lastBoundData = recentTasks
}
private fun createRecentViews(parent: ViewGroup): Views {
val recentsRoot =
LayoutInflater.from(parent.context)
.inflate(R.layout.media_projection_recent_tasks, parent, /* attachToRoot= */ false)
as ViewGroup
val container = recentsRoot.findViewById<View>(R.id.media_projection_recent_tasks_container)
container.setTaskHeightSize()
val progress = recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_loader)
val recycler =
recentsRoot.requireViewById<RecyclerView>(R.id.media_projection_recent_tasks_recycler)
recycler.layoutManager =
LinearLayoutManager(
parent.context,
LinearLayoutManager.HORIZONTAL,
/* reverseLayout= */ false
)
val itemDecoration =
HorizontalSpacerItemDecoration(
parent.resources.getDimensionPixelOffset(
R.dimen.media_projection_app_selector_recents_padding
)
)
recycler.addItemDecoration(itemDecoration)
return Views(recentsRoot, container, progress, recycler)
}
override fun onRecentAppClicked(task: RecentTask, view: View) {
val launchCookie = Binder()
val activityOptions =
ActivityOptions.makeScaleUpAnimation(
view,
/* startX= */ 0,
/* startY= */ 0,
view.width,
view.height
)
activityOptions.launchCookie = launchCookie
activityTaskManager.startActivityFromRecents(task.taskId, activityOptions.toBundle())
resultHandler.returnSelectedApp(launchCookie)
}
override fun onTaskSizeChanged(size: Rect) {
views?.recentsContainer?.setTaskHeightSize()
}
private fun View.setTaskHeightSize() {
val thumbnailHeight = taskViewSizeProvider.size.height()
val itemHeight =
thumbnailHeight +
context.resources.getDimensionPixelSize(
R.dimen.media_projection_app_selector_task_icon_size
) +
context.resources.getDimensionPixelSize(
R.dimen.media_projection_app_selector_task_icon_margin
) * 2
layoutParams = layoutParams.apply { height = itemHeight }
}
private class Views(
val root: ViewGroup,
val recentsContainer: View,
val progress: View,
val recycler: RecyclerView
)
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.mediaprojection.appselector.view
import android.content.Context
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Shader
import android.util.AttributeSet
import android.view.View
import android.view.WindowManager
import androidx.core.content.getSystemService
import androidx.core.content.res.use
import com.android.internal.R as AndroidR
import com.android.systemui.R
import com.android.systemui.mediaprojection.appselector.data.RecentTask
import com.android.systemui.shared.recents.model.ThumbnailData
import com.android.systemui.shared.recents.utilities.PreviewPositionHelper
import com.android.systemui.shared.recents.utilities.Utilities.isTablet
/**
* Custom view that shows a thumbnail preview of one recent task based on [ThumbnailData].
* It handles proper cropping and positioning of the thumbnail using [PreviewPositionHelper].
*/
class MediaProjectionTaskView
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
View(context, attrs, defStyleAttr) {
private val defaultBackgroundColor: Int
init {
val backgroundColorAttribute = intArrayOf(android.R.attr.colorBackgroundFloating)
defaultBackgroundColor =
context.obtainStyledAttributes(backgroundColorAttribute).use {
it.getColor(/* index= */ 0, /* defValue= */ Color.BLACK)
}
}
private val windowManager: WindowManager = context.getSystemService()!!
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val backgroundPaint =
Paint(Paint.ANTI_ALIAS_FLAG).apply { color = defaultBackgroundColor }
private val cornerRadius =
context.resources.getDimensionPixelSize(
R.dimen.media_projection_app_selector_task_rounded_corners
)
private val previewPositionHelper = PreviewPositionHelper()
private val previewRect = Rect()
private var task: RecentTask? = null
private var thumbnailData: ThumbnailData? = null
private var bitmapShader: BitmapShader? = null
fun bindTask(task: RecentTask?, thumbnailData: ThumbnailData?) {
this.task = task
this.thumbnailData = thumbnailData
// Strip alpha channel to make sure that the color is not semi-transparent
val color = (task?.colorBackground ?: Color.BLACK) or 0xFF000000.toInt()
paint.color = color
backgroundPaint.color = color
refresh()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
updateThumbnailMatrix()
invalidate()
}
override fun onDraw(canvas: Canvas) {
// Always draw the background since the snapshots might be translucent or partially empty
// (For example, tasks been reparented out of dismissing split root when drag-to-dismiss
// split screen).
canvas.drawRoundRect(
0f,
1f,
width.toFloat(),
(height - 1).toFloat(),
cornerRadius.toFloat(),
cornerRadius.toFloat(),
backgroundPaint
)
val drawBackgroundOnly = task == null || bitmapShader == null || thumbnailData == null
if (drawBackgroundOnly) {
return
}
// Draw the task thumbnail using bitmap shader in the paint
canvas.drawRoundRect(
0f,
0f,
width.toFloat(),
height.toFloat(),
cornerRadius.toFloat(),
cornerRadius.toFloat(),
paint
)
}
private fun refresh() {
val thumbnailBitmap = thumbnailData?.thumbnail
if (thumbnailBitmap != null) {
thumbnailBitmap.prepareToDraw()
bitmapShader =
BitmapShader(thumbnailBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
paint.shader = bitmapShader
updateThumbnailMatrix()
} else {
bitmapShader = null
paint.shader = null
}
invalidate()
}
private fun updateThumbnailMatrix() {
previewPositionHelper.isOrientationChanged = false
val bitmapShader = bitmapShader ?: return
val thumbnailData = thumbnailData ?: return
val display = context.display ?: return
val windowMetrics = windowManager.maximumWindowMetrics
previewRect.set(0, 0, thumbnailData.thumbnail.width, thumbnailData.thumbnail.height)
val currentRotation: Int = display.rotation
val displayWidthPx = windowMetrics.bounds.width()
val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL
val isTablet = isTablet(context)
val taskbarSize =
if (isTablet) {
resources.getDimensionPixelSize(AndroidR.dimen.taskbar_frame_height)
} else {
0
}
previewPositionHelper.updateThumbnailMatrix(
previewRect,
thumbnailData,
measuredWidth,
measuredHeight,
displayWidthPx,
taskbarSize,
isTablet,
currentRotation,
isRtl
)
bitmapShader.setLocalMatrix(previewPositionHelper.matrix)
paint.shader = bitmapShader
}
}

View File

@@ -16,15 +16,17 @@
package com.android.systemui.mediaprojection.appselector.view 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

View File

@@ -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
@@ -34,8 +36,8 @@ 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)
} }

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.mediaprojection.appselector.view
import android.content.Context
import android.content.res.Configuration
import android.graphics.Rect
import android.view.WindowManager
import com.android.internal.R as AndroidR
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorScope
import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener
import com.android.systemui.shared.recents.utilities.Utilities.isTablet
import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
import javax.inject.Inject
@MediaProjectionAppSelectorScope
class TaskPreviewSizeProvider
@Inject
constructor(
private val context: Context,
private val windowManager: WindowManager,
configurationController: ConfigurationController
) : CallbackController<TaskPreviewSizeListener>, ConfigurationListener {
/** Returns the size of the task preview on the screen in pixels */
val size: Rect = calculateSize()
private val listeners = arrayListOf<TaskPreviewSizeListener>()
init {
configurationController.addCallback(this)
}
override fun onConfigChanged(newConfig: Configuration) {
val newSize = calculateSize()
if (newSize != size) {
size.set(newSize)
listeners.forEach { it.onTaskSizeChanged(size) }
}
}
private fun calculateSize(): Rect {
val windowMetrics = windowManager.maximumWindowMetrics
val maximumWindowHeight = windowMetrics.bounds.height()
val width = windowMetrics.bounds.width()
var height = maximumWindowHeight
val isTablet = isTablet(context)
if (isTablet) {
val taskbarSize =
context.resources.getDimensionPixelSize(AndroidR.dimen.taskbar_frame_height)
height -= taskbarSize
}
val previewSize = Rect(0, 0, width, height)
val scale = (height / maximumWindowHeight.toFloat()) / SCREEN_HEIGHT_TO_TASK_HEIGHT_RATIO
previewSize.scale(scale)
return previewSize
}
override fun addCallback(listener: TaskPreviewSizeListener) {
listeners += listener
}
override fun removeCallback(listener: TaskPreviewSizeListener) {
listeners -= listener
}
interface TaskPreviewSizeListener {
fun onTaskSizeChanged(size: Rect)
}
}
/**
* How many times smaller the task preview should be on the screen comparing to the height of the
* screen
*/
private const val SCREEN_HEIGHT_TO_TASK_HEIGHT_RATIO = 4f

View File

@@ -25,6 +25,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
private val controller = MediaProjectionAppSelectorController( 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(

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.mediaprojection.appselector.view
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Rect
import android.util.DisplayMetrics.DENSITY_DEFAULT
import android.view.WindowManager
import android.view.WindowMetrics
import androidx.test.filters.SmallTest
import com.android.internal.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener
import com.android.systemui.statusbar.policy.FakeConfigurationController
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlin.math.min
import org.junit.Before
import org.junit.Test
@SmallTest
class TaskPreviewSizeProviderTest : SysuiTestCase() {
private val mockContext: Context = mock()
private val resources: Resources = mock()
private val windowManager: WindowManager = mock()
private val sizeUpdates = arrayListOf<Rect>()
private val testConfigurationController = FakeConfigurationController()
@Before
fun setup() {
whenever(mockContext.getSystemService(eq(WindowManager::class.java)))
.thenReturn(windowManager)
whenever(mockContext.resources).thenReturn(resources)
}
@Test
fun size_phoneDisplay_thumbnailSizeIsSmallerAndProportionalToTheScreenSize() {
givenDisplay(width = 400, height = 600, isTablet = false)
val size = createSizeProvider().size
assertThat(size).isEqualTo(Rect(0, 0, 100, 150))
}
@Test
fun size_tabletDisplay_thumbnailSizeProportionalToTheScreenSizeExcludingTaskbar() {
givenDisplay(width = 400, height = 600, isTablet = true)
givenTaskbarSize(20)
val size = createSizeProvider().size
assertThat(size).isEqualTo(Rect(0, 0, 97, 140))
}
@Test
fun size_phoneDisplayAndRotate_emitsSizeUpdate() {
givenDisplay(width = 400, height = 600, isTablet = false)
createSizeProvider()
givenDisplay(width = 600, height = 400, isTablet = false)
testConfigurationController.onConfigurationChanged(Configuration())
assertThat(sizeUpdates).containsExactly(Rect(0, 0, 150, 100))
}
@Test
fun size_phoneDisplayAndRotateConfigurationChange_returnsUpdatedSize() {
givenDisplay(width = 400, height = 600, isTablet = false)
val sizeProvider = createSizeProvider()
givenDisplay(width = 600, height = 400, isTablet = false)
testConfigurationController.onConfigurationChanged(Configuration())
assertThat(sizeProvider.size).isEqualTo(Rect(0, 0, 150, 100))
}
private fun givenTaskbarSize(size: Int) {
whenever(resources.getDimensionPixelSize(eq(R.dimen.taskbar_frame_height))).thenReturn(size)
}
private fun givenDisplay(width: Int, height: Int, isTablet: Boolean = false) {
val bounds = Rect(0, 0, width, height)
val windowMetrics = WindowMetrics(bounds, null)
whenever(windowManager.maximumWindowMetrics).thenReturn(windowMetrics)
whenever(windowManager.currentWindowMetrics).thenReturn(windowMetrics)
val minDimension = min(width, height)
// Calculate DPI so the smallest width is either considered as tablet or as phone
val targetSmallestWidthDpi =
if (isTablet) SMALLEST_WIDTH_DPI_TABLET else SMALLEST_WIDTH_DPI_PHONE
val densityDpi = minDimension * DENSITY_DEFAULT / targetSmallestWidthDpi
val configuration = Configuration(context.resources.configuration)
configuration.densityDpi = densityDpi
whenever(resources.configuration).thenReturn(configuration)
}
private fun createSizeProvider(): TaskPreviewSizeProvider {
val listener =
object : TaskPreviewSizeListener {
override fun onTaskSizeChanged(size: Rect) {
sizeUpdates.add(size)
}
}
return TaskPreviewSizeProvider(mockContext, windowManager, testConfigurationController)
.also { it.addCallback(listener) }
}
private companion object {
private const val SMALLEST_WIDTH_DPI_TABLET = 800
private const val SMALLEST_WIDTH_DPI_PHONE = 400
}
}