1 /* <lambda>null2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.mediaprojection.appselector.view 18 19 import android.app.ActivityOptions 20 import android.app.ActivityOptions.LaunchCookie 21 import android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED 22 import android.app.IActivityTaskManager 23 import android.graphics.Rect 24 import android.view.LayoutInflater 25 import android.view.View 26 import android.view.ViewGroup 27 import android.window.RemoteTransition 28 import androidx.recyclerview.widget.LinearLayoutManager 29 import androidx.recyclerview.widget.RecyclerView 30 import com.android.systemui.Flags.pssAppSelectorRecentsSplitScreen 31 import com.android.systemui.display.naturalBounds 32 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler 33 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorScope 34 import com.android.systemui.mediaprojection.appselector.data.RecentTask 35 import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter.RecentTaskClickListener 36 import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener 37 import com.android.systemui.res.R 38 import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration 39 import com.android.wm.shell.shared.split.SplitBounds 40 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT 41 import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT 42 import com.android.wm.shell.splitscreen.SplitScreen 43 import java.util.Optional 44 import javax.inject.Inject 45 46 /** 47 * Controller that handles view of the recent apps selector in the media projection activity. It is 48 * responsible for creating and updating recent apps view. 49 */ 50 @MediaProjectionAppSelectorScope 51 class MediaProjectionRecentsViewController 52 @Inject 53 constructor( 54 private val recentTasksAdapterFactory: RecentTasksAdapter.Factory, 55 private val taskViewSizeProvider: TaskPreviewSizeProvider, 56 private val activityTaskManager: IActivityTaskManager, 57 private val resultHandler: MediaProjectionAppSelectorResultHandler, 58 private val splitScreen: Optional<SplitScreen>, 59 ) : RecentTaskClickListener, TaskPreviewSizeListener { 60 61 private var views: Views? = null 62 private var lastBoundData: List<RecentTask>? = null 63 64 val hasRecentTasks: Boolean 65 get() = lastBoundData?.isNotEmpty() ?: false 66 67 init { 68 taskViewSizeProvider.addCallback(this) 69 } 70 71 fun createView(parent: ViewGroup): ViewGroup = 72 views?.root 73 ?: createRecentViews(parent) 74 .also { 75 views = it 76 lastBoundData?.let { recents -> bind(recents) } 77 } 78 .root 79 80 fun bind(recentTasks: List<RecentTask>) { 81 views?.apply { 82 if (recentTasks.isEmpty()) { 83 root.visibility = View.GONE 84 return 85 } 86 87 progress.visibility = View.GONE 88 recycler.visibility = View.VISIBLE 89 root.visibility = View.VISIBLE 90 91 recycler.adapter = 92 recentTasksAdapterFactory.create( 93 recentTasks, 94 this@MediaProjectionRecentsViewController 95 ) 96 } 97 98 lastBoundData = recentTasks 99 } 100 101 private fun createRecentViews(parent: ViewGroup): Views { 102 val recentsRoot = 103 LayoutInflater.from(parent.context) 104 .inflate(R.layout.media_projection_recent_tasks, 105 parent, /* attachToRoot= */ 106 false) 107 as ViewGroup 108 109 val container = 110 recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_container) 111 container.setTaskHeightSize() 112 113 val progress = recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_loader) 114 val recycler = 115 recentsRoot.requireViewById<RecyclerView>(R.id.media_projection_recent_tasks_recycler) 116 recycler.layoutManager = 117 LinearLayoutManager( 118 parent.context, 119 LinearLayoutManager.HORIZONTAL, 120 /* reverseLayout= */ false 121 ) 122 123 val itemDecoration = 124 HorizontalSpacerItemDecoration( 125 parent.resources.getDimensionPixelOffset( 126 R.dimen.media_projection_app_selector_recents_padding 127 ) 128 ) 129 recycler.addItemDecoration(itemDecoration) 130 131 return Views(recentsRoot, container, progress, recycler) 132 } 133 134 private fun RecentTask.isLaunchingInSplitScreen(): Boolean { 135 return splitScreen.isPresent && splitBounds != null 136 } 137 138 override fun onRecentAppClicked(task: RecentTask, view: View) { 139 val launchCookie = LaunchCookie() 140 val activityOptions = createAnimation(task, view) 141 activityOptions.pendingIntentBackgroundActivityStartMode = 142 MODE_BACKGROUND_ACTIVITY_START_ALLOWED 143 activityOptions.launchDisplayId = task.displayId 144 activityOptions.setLaunchCookie(launchCookie) 145 146 val taskId = task.taskId 147 val splitBounds = task.splitBounds 148 val handleResult: () -> Unit = { resultHandler.returnSelectedApp(launchCookie, taskId)} 149 150 if (pssAppSelectorRecentsSplitScreen() && 151 task.isLaunchingInSplitScreen() && 152 !task.isForegroundTask) { 153 startSplitScreenTask(view, taskId, splitBounds!!, handleResult, activityOptions) 154 } else { 155 activityTaskManager.startActivityFromRecents(taskId, activityOptions.toBundle()) 156 handleResult() 157 } 158 } 159 160 161 private fun createAnimation(task: RecentTask, view: View): ActivityOptions = 162 if (task.isForegroundTask) { 163 // When the selected task is in the foreground, the scale up animation doesn't work. 164 // We fallback to the default close animation. 165 ActivityOptions.makeCustomTaskAnimation( 166 view.context, 167 /* enterResId= */ 0, 168 /* exitResId= */ com.android.internal.R.anim.resolver_close_anim, 169 /* handler = */ null, 170 /* startedListener = */ null, 171 /* finishedListener = */ null 172 ) 173 } else if (task.isLaunchingInSplitScreen()) { 174 // When the selected task isn't in the foreground, but is launching in split screen, 175 // then we don't need to specify an animation, since we'll already be passing a 176 // manually built remote animation to SplitScreenController 177 ActivityOptions.makeBasic() 178 } else { 179 // The default case is a selected task not in the foreground and launching fullscreen, 180 // so for this we can use the default ActivityOptions animation 181 ActivityOptions.makeScaleUpAnimation( 182 view, 183 /* startX= */ 0, 184 /* startY= */ 0, 185 view.width, 186 view.height 187 ) 188 } 189 190 private fun startSplitScreenTask( 191 view: View, 192 taskId: Int, 193 splitBounds: SplitBounds, 194 handleResult: () -> Unit, 195 activityOptions: ActivityOptions, 196 ) { 197 val isLeftTopTask = taskId == splitBounds.leftTopTaskId 198 val task2Id = 199 if (isLeftTopTask) splitBounds.rightBottomTaskId else splitBounds.leftTopTaskId 200 val splitPosition = 201 if (isLeftTopTask) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT 202 203 val animationRunner = RemoteRecentSplitTaskTransitionRunner(taskId, task2Id, 204 view.locationOnScreen, view.context.display.naturalBounds, handleResult) 205 val remoteTransition = RemoteTransition(animationRunner, 206 view.context.iApplicationThread, "startSplitScreenTask") 207 208 splitScreen.get().startTasks(taskId, activityOptions.toBundle(), task2Id, null, 209 splitPosition, splitBounds.snapPosition, remoteTransition, null) 210 } 211 212 213 override fun onTaskSizeChanged(size: Rect) { 214 views?.recentsContainer?.setTaskHeightSize() 215 } 216 217 private fun View.setTaskHeightSize() { 218 val thumbnailHeight = taskViewSizeProvider.size.height() 219 val itemHeight = 220 thumbnailHeight + 221 context.resources.getDimensionPixelSize( 222 R.dimen.media_projection_app_selector_task_icon_size 223 ) + 224 context.resources.getDimensionPixelSize( 225 R.dimen.media_projection_app_selector_task_icon_margin 226 ) * 2 227 228 layoutParams = layoutParams.apply { height = itemHeight } 229 } 230 231 private class Views( 232 val root: ViewGroup, 233 val recentsContainer: View, 234 val progress: View, 235 val recycler: RecyclerView 236 ) 237 } 238