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