Merge "[Partial Screensharing] Add initial recents selector UI" into tm-qpr-dev

This commit is contained in:
Nick Chameyev
2022-09-23 13:20:23 +00:00
committed by Android (Google) Code Review
17 changed files with 769 additions and 24 deletions

View File

@@ -1069,7 +1069,12 @@ public class ChooserActivity extends ResolverActivity implements
}
}
private ViewGroup createContentPreviewView(ViewGroup parent) {
/**
* Create a view that will be shown in the content preview area
* @param parent reference to the parent container where the view should be attached to
* @return content preview view
*/
protected ViewGroup createContentPreviewView(ViewGroup parent) {
Intent targetIntent = getTargetIntent();
int previewType = findPreferredContentPreview(targetIntent, getContentResolver());
return displayContentPreview(previewType, targetIntent, getLayoutInflater(), parent);
@@ -2640,7 +2645,7 @@ public class ChooserActivity extends ResolverActivity implements
boolean isExpandable = getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode();
if (directShareHeight != 0 && isSendAction(getTargetIntent())
if (directShareHeight != 0 && shouldShowContentPreview()
&& isExpandable) {
// make sure to leave room for direct share 4->8 expansion
int requiredExpansionHeight =
@@ -2888,7 +2893,14 @@ public class ChooserActivity extends ResolverActivity implements
return shouldShowTabs()
&& mMultiProfilePagerAdapter.getListAdapterForUserHandle(
UserHandle.of(UserHandle.myUserId())).getCount() > 0
&& isSendAction(getTargetIntent());
&& shouldShowContentPreview();
}
/**
* @return true if we want to show the content preview area
*/
protected boolean shouldShowContentPreview() {
return isSendAction(getTargetIntent());
}
private void updateStickyContentPreview() {
@@ -3221,7 +3233,7 @@ public class ChooserActivity extends ResolverActivity implements
return 0;
}
if (!isSendAction(getTargetIntent())) {
if (!shouldShowContentPreview()) {
return 0;
}
@@ -3252,7 +3264,7 @@ public class ChooserActivity extends ResolverActivity implements
// There can be at most one row in the listview, that is internally
// a ViewGroup with 2 rows
public int getServiceTargetRowCount() {
if (isSendAction(getTargetIntent())
if (shouldShowContentPreview()
&& !ActivityManager.isLowRamDeviceStatic()) {
return 1;
}

View File

@@ -36,13 +36,14 @@
android:background="@*android:drawable/bottomsheet_background">
<ImageView
android:id="@*android:id/icon"
android:layout_width="@dimen/media_projection_app_selector_icon_size"
android:layout_height="@dimen/media_projection_app_selector_icon_size"
android:layout_marginTop="@*android:dimen/chooser_edge_margin_normal"
android:layout_marginBottom="@*android:dimen/chooser_edge_margin_normal"
android:importantForAccessibility="no"
android:tint="?android:attr/textColorPrimary"/>
android:tint="?android:attr/textColorPrimary"
android:src="@drawable/ic_present_to_all"
/>
<TextView android:id="@*android:id/title"
android:layout_height="wrap_content"

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:background="?android:attr/colorBackground"
>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="256dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/media_projection_recent_tasks_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
/>
<ProgressBar
android:id="@+id/media_projection_recent_tasks_loader"
android:layout_width="@dimen/media_projection_app_selector_loader_size"
android:layout_height="@dimen/media_projection_app_selector_loader_size"
android:layout_gravity="center"
android:indeterminate="true"
android:indeterminateOnly="true" />
</FrameLayout>
<!-- Divider line -->
<ImageView
android:layout_width="72dp"
android:layout_height="2dp"
android:layout_marginBottom="8dp"
android:layout_marginTop="24dp"
android:importantForAccessibility="no"
android:src="@*android:drawable/ic_drag_handle"
android:tint="?android:attr/textColorSecondary" />
</LinearLayout>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/task_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:importantForAccessibility="no" />
<!-- TODO(b/240924926) use a custom view that will handle thumbnail cropping correctly -->
<!-- TODO(b/240924926) dynamically change the view size based on the screen size -->
<ImageView
android:id="@+id/task_thumbnail"
android:layout_width="100dp"
android:layout_height="216dp"
android:scaleType="centerCrop"
/>
</LinearLayout>

View File

@@ -1454,6 +1454,8 @@
<dimen name="fgs_manager_list_top_spacing">12dp</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_loader_size">32dp</dimen>
<!-- Dream overlay related dimensions -->
<dimen name="dream_overlay_status_bar_height">60dp</dimen>

View File

@@ -24,22 +24,45 @@ import android.os.Bundle
import android.os.IBinder
import android.os.ResultReceiver
import android.os.UserHandle
import android.widget.ImageView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.app.ChooserActivity
import com.android.internal.app.ResolverListController
import com.android.internal.app.chooser.NotSelectableTargetInfo
import com.android.internal.app.chooser.TargetInfo
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.util.AsyncActivityLauncher
import com.android.systemui.R
import com.android.internal.R as AndroidR
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorView
import com.android.systemui.mediaprojection.appselector.data.RecentTask
import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter
import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter.RecentTaskClickListener
import com.android.systemui.util.AsyncActivityLauncher
import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration
import javax.inject.Inject
class MediaProjectionAppSelectorActivity constructor(
class MediaProjectionAppSelectorActivity(
private val activityLauncher: AsyncActivityLauncher,
private val controller: MediaProjectionAppSelectorController,
private val recentTasksAdapterFactory: RecentTasksAdapter.Factory,
/** This is used to override the dependency in a screenshot test */
@VisibleForTesting
private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)? = null
) : ChooserActivity() {
private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
) : ChooserActivity(), MediaProjectionAppSelectorView, RecentTaskClickListener {
@Inject
constructor(
activityLauncher: AsyncActivityLauncher,
controller: MediaProjectionAppSelectorController,
recentTasksAdapterFactory: RecentTasksAdapter.Factory,
) : this(activityLauncher, controller, recentTasksAdapterFactory, null)
private var recentsRoot: ViewGroup? = null
private var recentsProgress: View? = null
private var recentsRecycler: RecyclerView? = null
override fun getLayoutResource() =
R.layout.media_projection_app_selector
@@ -52,10 +75,30 @@ class MediaProjectionAppSelectorActivity constructor(
// TODO(b/240939253): update copies
val title = getString(R.string.media_projection_dialog_service_title)
intent.putExtra(Intent.EXTRA_TITLE, title)
super.onCreate(bundle)
controller.init(this)
}
requireViewById<ImageView>(AndroidR.id.icon).setImageResource(R.drawable.ic_present_to_all)
private fun createRecentsView(parent: ViewGroup): ViewGroup {
val recentsRoot = LayoutInflater.from(this)
.inflate(R.layout.media_projection_recent_tasks, parent,
/* attachToRoot= */ false) as ViewGroup
recentsProgress = recentsRoot.requireViewById(R.id.media_projection_recent_tasks_loader)
recentsRecycler = recentsRoot.requireViewById(R.id.media_projection_recent_tasks_recycler)
recentsRecycler?.layoutManager = LinearLayoutManager(
this, LinearLayoutManager.HORIZONTAL,
/* reverseLayout= */false
)
val itemDecoration = HorizontalSpacerItemDecoration(
resources.getDimensionPixelOffset(
R.dimen.media_projection_app_selector_recents_padding
)
)
recentsRecycler?.addItemDecoration(itemDecoration)
return recentsRoot
}
override fun appliedThemeResId(): Int =
@@ -108,6 +151,7 @@ class MediaProjectionAppSelectorActivity constructor(
override fun onDestroy() {
activityLauncher.destroy()
controller.destroy()
super.onDestroy()
}
@@ -115,6 +159,27 @@ class MediaProjectionAppSelectorActivity constructor(
// do nothing
}
override fun bind(recentTasks: List<RecentTask>) {
val recents = recentsRoot ?: return
val progress = recentsProgress ?: return
val recycler = recentsRecycler ?: return
if (recentTasks.isEmpty()) {
recents.visibility = View.GONE
return
}
progress.visibility = View.GONE
recycler.visibility = View.VISIBLE
recents.visibility = View.VISIBLE
recycler.adapter = recentTasksAdapterFactory.create(recentTasks, this)
}
override fun onRecentClicked(task: RecentTask, view: View) {
// TODO(b/240924732) Handle clicking on a recent task
}
private fun onTargetActivityLaunched(launchToken: IBinder) {
if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
// The client requested to return the result in the result receiver instead of
@@ -145,6 +210,14 @@ class MediaProjectionAppSelectorActivity constructor(
override fun shouldGetOnlyDefaultActivities() = false
// TODO(b/240924732) flip the flag when the recents selector is ready
override fun shouldShowContentPreview() = false
override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
recentsRoot ?: createRecentsView(parent).also {
recentsRoot = it
}
companion object {
/**
* When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra

View File

@@ -17,13 +17,29 @@
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.util.AsyncActivityLauncher
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 {
@@ -31,17 +47,43 @@ abstract class MediaProjectionModule {
@Binds
@IntoMap
@ClassKey(MediaProjectionAppSelectorActivity::class)
abstract fun bindMediaProjectionAppSelectorActivity(
activity: MediaProjectionAppSelectorActivity): Activity
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 provideMediaProjectionAppSelectorActivity(
activityLauncher: AsyncActivityLauncher
): MediaProjectionAppSelectorActivity {
return MediaProjectionAppSelectorActivity(
activityLauncher
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,49 @@
/*
* 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.content.ComponentName
import com.android.systemui.media.dagger.MediaProjectionAppSelector
import com.android.systemui.mediaprojection.appselector.data.RecentTask
import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class MediaProjectionAppSelectorController(
private val recentTaskListProvider: RecentTaskListProvider,
@MediaProjectionAppSelector private val scope: CoroutineScope,
private val appSelectorComponentName: ComponentName
) {
fun init(view: MediaProjectionAppSelectorView) {
scope.launch {
val tasks = recentTaskListProvider.loadRecentTasks().sortTasks()
view.bind(tasks)
}
}
fun destroy() {
scope.cancel()
}
private fun List<RecentTask>.sortTasks(): List<RecentTask> =
asReversed().sortedBy {
// Show normal tasks first and only then tasks with opened app selector
it.topActivityComponent == appSelectorComponentName
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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 com.android.systemui.mediaprojection.appselector.data.RecentTask
interface MediaProjectionAppSelectorView {
fun bind(recentTasks: List<RecentTask>)
}

View File

@@ -0,0 +1,41 @@
/*
* 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.data
import android.content.ComponentName
import android.graphics.drawable.Drawable
import com.android.systemui.dagger.qualifiers.Background
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import javax.inject.Inject
interface AppIconLoader {
suspend fun loadIcon(userId: Int, component: ComponentName): Drawable?
}
class IconLoaderLibAppIconLoader
@Inject
constructor(
@Background private val backgroundDispatcher: CoroutineDispatcher,
) : AppIconLoader {
override suspend fun loadIcon(userId: Int, component: ComponentName): Drawable? =
withContext(backgroundDispatcher) {
// TODO(b/240924731): add a blocking call to load an icon using iconloaderlib
null
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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.data
import android.content.ComponentName
data class RecentTask(
val taskId: Int,
val userId: Int,
val topActivityComponent: ComponentName?,
val baseIntentComponent: ComponentName?
)

View File

@@ -0,0 +1,38 @@
/*
* 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.data
import com.android.systemui.dagger.qualifiers.Background
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import javax.inject.Inject
interface RecentTaskListProvider {
suspend fun loadRecentTasks(): List<RecentTask>
}
class ShellRecentTaskListProvider
@Inject
constructor(@Background private val coroutineDispatcher: CoroutineDispatcher) :
RecentTaskListProvider {
override suspend fun loadRecentTasks(): List<RecentTask> =
withContext(coroutineDispatcher) {
// TODO(b/240924731): add blocking call to load the recents
emptyList()
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.data
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.shared.recents.model.ThumbnailData
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
interface RecentTaskThumbnailLoader {
suspend fun loadThumbnail(taskId: Int): ThumbnailData?
}
class ActivityTaskManagerThumbnailLoader
@Inject
constructor(
@Background private val coroutineDispatcher: CoroutineDispatcher,
) :
RecentTaskThumbnailLoader {
override suspend fun loadThumbnail(taskId: Int): ThumbnailData? =
withContext(coroutineDispatcher) {
// TODO(b/240924731): add blocking call to load a thumbnail
null
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
import com.android.systemui.media.dagger.MediaProjectionAppSelector
import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
import com.android.systemui.mediaprojection.appselector.data.RecentTask
import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
class RecentTaskViewHolder @AssistedInject constructor(
@Assisted root: ViewGroup,
private val iconLoader: AppIconLoader,
private val thumbnailLoader: RecentTaskThumbnailLoader,
@MediaProjectionAppSelector private val scope: CoroutineScope
) : RecyclerView.ViewHolder(root) {
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
fun bind(task: RecentTask, onClick: (View) -> Unit) {
job?.cancel()
job =
scope.launch {
task.baseIntentComponent?.let { component ->
launch {
val icon = iconLoader.loadIcon(task.userId, component)
iconView.setImageDrawable(icon)
}
}
launch {
val thumbnail = thumbnailLoader.loadThumbnail(task.taskId)
thumbnailView.setImageBitmap(thumbnail?.thumbnail)
}
}
thumbnailView.setOnClickListener(onClick)
}
fun onRecycled() {
iconView.setImageDrawable(null)
thumbnailView.setImageBitmap(null)
job?.cancel()
job = null
}
@AssistedFactory
fun interface Factory {
fun create(root: ViewGroup): RecentTaskViewHolder
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.android.systemui.R
import com.android.systemui.mediaprojection.appselector.data.RecentTask
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
class RecentTasksAdapter @AssistedInject constructor(
@Assisted private val items: List<RecentTask>,
@Assisted private val listener: RecentTaskClickListener,
private val viewHolderFactory: RecentTaskViewHolder.Factory
) : RecyclerView.Adapter<RecentTaskViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentTaskViewHolder {
val taskItem =
LayoutInflater.from(parent.context)
.inflate(R.layout.media_projection_task_item, null) as ViewGroup
return viewHolderFactory.create(taskItem)
}
override fun onBindViewHolder(holder: RecentTaskViewHolder, position: Int) {
val task = items[position]
holder.bind(task, onClick = {
listener.onRecentClicked(task, holder.itemView)
})
}
override fun getItemCount(): Int = items.size
override fun onViewRecycled(holder: RecentTaskViewHolder) {
holder.onRecycled()
}
interface RecentTaskClickListener {
fun onRecentClicked(task: RecentTask, view: View)
}
@AssistedFactory
fun interface Factory {
fun create(items: List<RecentTask>, listener: RecentTaskClickListener): RecentTasksAdapter
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.util.recycler
import android.graphics.Rect
import android.view.View
import androidx.annotation.Dimension
import androidx.recyclerview.widget.RecyclerView
/**
* RecyclerView ItemDecorator that adds a horizontal space of the given size between items
* and double that space on the ends.
*/
class HorizontalSpacerItemDecoration(@Dimension private val offset: Int) :
RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position: Int = parent.getChildAdapterPosition(view)
val itemCount = parent.adapter?.itemCount ?: 0
val left = if (position == 0) offset * 2 else offset
val right = if (position == itemCount - 1) offset * 2 else offset
outRect.set(left, 0, right, 0)
}
}

View File

@@ -0,0 +1,119 @@
package com.android.systemui.mediaprojection.appselector
import android.content.ComponentName
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.mediaprojection.appselector.data.RecentTask
import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
import com.android.systemui.util.mockito.mock
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
@RunWith(AndroidTestingRunner::class)
@SmallTest
class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
private val taskListProvider = TestRecentTaskListProvider()
private val scope = CoroutineScope(Dispatchers.Unconfined)
private val appSelectorComponentName = ComponentName("com.test", "AppSelector")
private val view: MediaProjectionAppSelectorView = mock()
private val controller = MediaProjectionAppSelectorController(
taskListProvider,
scope,
appSelectorComponentName
)
@Test
fun initNoRecentTasks_bindsEmptyList() {
taskListProvider.tasks = emptyList()
controller.init(view)
verify(view).bind(emptyList())
}
@Test
fun initOneRecentTask_bindsList() {
taskListProvider.tasks = listOf(
createRecentTask(taskId = 1)
)
controller.init(view)
verify(view).bind(
listOf(
createRecentTask(taskId = 1)
)
)
}
@Test
fun initMultipleRecentTasksWithoutAppSelectorTask_bindsListInReverse() {
val tasks = listOf(
createRecentTask(taskId = 1),
createRecentTask(taskId = 2),
createRecentTask(taskId = 3),
)
taskListProvider.tasks = tasks
controller.init(view)
verify(view).bind(
listOf(
createRecentTask(taskId = 3),
createRecentTask(taskId = 2),
createRecentTask(taskId = 1),
)
)
}
@Test
fun initRecentTasksWithAppSelectorTasks_bindsListInReverseAndAppSelectorTasksAtTheEnd() {
val tasks = listOf(
createRecentTask(taskId = 1),
createRecentTask(taskId = 2, topActivityComponent = appSelectorComponentName),
createRecentTask(taskId = 3),
createRecentTask(taskId = 4, topActivityComponent = appSelectorComponentName),
createRecentTask(taskId = 5),
)
taskListProvider.tasks = tasks
controller.init(view)
verify(view).bind(
listOf(
createRecentTask(taskId = 5),
createRecentTask(taskId = 3),
createRecentTask(taskId = 1),
createRecentTask(taskId = 4, topActivityComponent = appSelectorComponentName),
createRecentTask(taskId = 2, topActivityComponent = appSelectorComponentName),
)
)
}
private fun createRecentTask(
taskId: Int,
topActivityComponent: ComponentName? = null
): RecentTask {
return RecentTask(
taskId = taskId,
topActivityComponent = topActivityComponent,
baseIntentComponent = ComponentName("com", "Test"),
userId = 0
)
}
private class TestRecentTaskListProvider : RecentTaskListProvider {
var tasks: List<RecentTask> = emptyList()
override suspend fun loadRecentTasks(): List<RecentTask> = tasks
}
}