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