Merge "[Partial Screensharing] Add initial recents selector UI" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
66e9106619
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
38
packages/SystemUI/res/layout/media_projection_task_item.xml
Normal file
38
packages/SystemUI/res/layout/media_projection_task_item.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user