1 /*
2  * 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.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
21 import android.app.IActivityTaskManager
22 import android.graphics.Rect
23 import android.os.Binder
24 import android.view.LayoutInflater
25 import android.view.View
26 import android.view.ViewGroup
27 import androidx.recyclerview.widget.LinearLayoutManager
28 import androidx.recyclerview.widget.RecyclerView
29 import com.android.systemui.R
30 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
31 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorScope
32 import com.android.systemui.mediaprojection.appselector.data.RecentTask
33 import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter.RecentTaskClickListener
34 import com.android.systemui.mediaprojection.appselector.view.TaskPreviewSizeProvider.TaskPreviewSizeListener
35 import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration
36 import javax.inject.Inject
37 
38 /**
39  * Controller that handles view of the recent apps selector in the media projection activity. It is
40  * responsible for creating and updating recent apps view.
41  */
42 @MediaProjectionAppSelectorScope
43 class MediaProjectionRecentsViewController
44 @Inject
45 constructor(
46     private val recentTasksAdapterFactory: RecentTasksAdapter.Factory,
47     private val taskViewSizeProvider: TaskPreviewSizeProvider,
48     private val activityTaskManager: IActivityTaskManager,
49     private val resultHandler: MediaProjectionAppSelectorResultHandler,
50 ) : RecentTaskClickListener, TaskPreviewSizeListener {
51 
52     private var views: Views? = null
53     private var lastBoundData: List<RecentTask>? = null
54 
55     val hasRecentTasks: Boolean
56         get() = lastBoundData?.isNotEmpty() ?: false
57 
58     init {
59         taskViewSizeProvider.addCallback(this)
60     }
61 
62     fun createView(parent: ViewGroup): ViewGroup =
63         views?.root
64             ?: createRecentViews(parent)
65                 .also {
66                     views = it
67                     lastBoundData?.let { recents -> bind(recents) }
68                 }
69                 .root
70 
71     fun bind(recentTasks: List<RecentTask>) {
72         views?.apply {
73             if (recentTasks.isEmpty()) {
74                 root.visibility = View.GONE
75                 return
76             }
77 
78             progress.visibility = View.GONE
79             recycler.visibility = View.VISIBLE
80             root.visibility = View.VISIBLE
81 
82             recycler.adapter =
83                 recentTasksAdapterFactory.create(
84                     recentTasks,
85                     this@MediaProjectionRecentsViewController
86                 )
87         }
88 
89         lastBoundData = recentTasks
90     }
91 
92     private fun createRecentViews(parent: ViewGroup): Views {
93         val recentsRoot =
94             LayoutInflater.from(parent.context)
95                 .inflate(R.layout.media_projection_recent_tasks, parent, /* attachToRoot= */ false)
96                 as ViewGroup
97 
98         val container =
99             recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_container)
100         container.setTaskHeightSize()
101 
102         val progress = recentsRoot.requireViewById<View>(R.id.media_projection_recent_tasks_loader)
103         val recycler =
104             recentsRoot.requireViewById<RecyclerView>(R.id.media_projection_recent_tasks_recycler)
105         recycler.layoutManager =
106             LinearLayoutManager(
107                 parent.context,
108                 LinearLayoutManager.HORIZONTAL,
109                 /* reverseLayout= */ false
110             )
111 
112         val itemDecoration =
113             HorizontalSpacerItemDecoration(
114                 parent.resources.getDimensionPixelOffset(
115                     R.dimen.media_projection_app_selector_recents_padding
116                 )
117             )
118         recycler.addItemDecoration(itemDecoration)
119 
120         return Views(recentsRoot, container, progress, recycler)
121     }
122 
123     override fun onRecentAppClicked(task: RecentTask, view: View) {
124         val launchCookie = Binder()
125         val activityOptions =
126             ActivityOptions.makeScaleUpAnimation(
127                 view,
128                 /* startX= */ 0,
129                 /* startY= */ 0,
130                 view.width,
131                 view.height
132             )
133         activityOptions.setPendingIntentBackgroundActivityStartMode(
134             MODE_BACKGROUND_ACTIVITY_START_ALLOWED
135         )
136         activityOptions.launchCookie = launchCookie
137 
138         activityTaskManager.startActivityFromRecents(task.taskId, activityOptions.toBundle())
139         resultHandler.returnSelectedApp(launchCookie)
140     }
141 
142     override fun onTaskSizeChanged(size: Rect) {
143         views?.recentsContainer?.setTaskHeightSize()
144     }
145 
146     private fun View.setTaskHeightSize() {
147         val thumbnailHeight = taskViewSizeProvider.size.height()
148         val itemHeight =
149             thumbnailHeight +
150                 context.resources.getDimensionPixelSize(
151                     R.dimen.media_projection_app_selector_task_icon_size
152                 ) +
153                 context.resources.getDimensionPixelSize(
154                     R.dimen.media_projection_app_selector_task_icon_margin
155                 ) * 2
156 
157         layoutParams = layoutParams.apply { height = itemHeight }
158     }
159 
160     private class Views(
161         val root: ViewGroup,
162         val recentsContainer: View,
163         val progress: View,
164         val recycler: RecyclerView
165     )
166 }
167