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 package com.android.systemui.media
17 
18 import android.app.ActivityOptions
19 import android.content.Intent
20 import android.content.res.Configuration
21 import android.content.res.Resources
22 import android.media.projection.IMediaProjection
23 import android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT
24 import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
25 import android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL
26 import android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK
27 import android.os.Binder
28 import android.os.Bundle
29 import android.os.IBinder
30 import android.os.ResultReceiver
31 import android.os.UserHandle
32 import android.util.Log
33 import android.view.View
34 import android.view.ViewGroup
35 import android.view.accessibility.AccessibilityEvent
36 import androidx.lifecycle.Lifecycle
37 import androidx.lifecycle.LifecycleOwner
38 import androidx.lifecycle.LifecycleRegistry
39 import com.android.internal.annotations.VisibleForTesting
40 import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider
41 import com.android.internal.app.AbstractMultiProfilePagerAdapter.MyUserIdProvider
42 import com.android.internal.app.ChooserActivity
43 import com.android.internal.app.ResolverListController
44 import com.android.internal.app.chooser.NotSelectableTargetInfo
45 import com.android.internal.app.chooser.TargetInfo
46 import com.android.internal.widget.RecyclerView
47 import com.android.internal.widget.RecyclerViewAccessibilityDelegate
48 import com.android.internal.widget.ResolverDrawerLayout
49 import com.android.systemui.R
50 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorComponent
51 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
52 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
53 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorView
54 import com.android.systemui.mediaprojection.appselector.data.RecentTask
55 import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
56 import com.android.systemui.statusbar.policy.ConfigurationController
57 import com.android.systemui.util.AsyncActivityLauncher
58 import javax.inject.Inject
59 
60 class MediaProjectionAppSelectorActivity(
61     private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
62     private val activityLauncher: AsyncActivityLauncher,
63     /** This is used to override the dependency in a screenshot test */
64     @VisibleForTesting
65     private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
66 ) :
67     ChooserActivity(),
68     MediaProjectionAppSelectorView,
69     MediaProjectionAppSelectorResultHandler,
70     LifecycleOwner {
71 
72     @Inject
73     constructor(
74         componentFactory: MediaProjectionAppSelectorComponent.Factory,
75         activityLauncher: AsyncActivityLauncher
76     ) : this(componentFactory, activityLauncher, listControllerFactory = null)
77 
78     private val lifecycleRegistry = LifecycleRegistry(this)
79     override val lifecycle = lifecycleRegistry
80     private lateinit var configurationController: ConfigurationController
81     private lateinit var controller: MediaProjectionAppSelectorController
82     private lateinit var recentsViewController: MediaProjectionRecentsViewController
83     private lateinit var component: MediaProjectionAppSelectorComponent
84     // Indicate if we are under the media projection security flow
85     // i.e. when a host app reuses consent token, review the permission and update it to the service
86     private var reviewGrantedConsentRequired = false
87     // If an app is selected, set to true so that we don't send RECORD_CANCEL in onDestroy
88     private var taskSelected = false
89 
90     override fun getLayoutResource() = R.layout.media_projection_app_selector
91 
92     public override fun onCreate(bundle: Bundle?) {
93         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
94         component = componentFactory.create(activity = this, view = this, resultHandler = this)
95         component.lifecycleObservers.forEach { lifecycle.addObserver(it) }
96 
97         // Create a separate configuration controller for this activity as the configuration
98         // might be different from the global one
99         configurationController = component.configurationController
100         controller = component.controller
101         recentsViewController = component.recentsViewController
102 
103         intent.configureChooserIntent(
104             resources,
105             component.hostUserHandle,
106             component.personalProfileUserHandle
107         )
108 
109         reviewGrantedConsentRequired =
110             intent.getBooleanExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, false)
111 
112         super.onCreate(bundle)
113         controller.init()
114         // we override AppList's AccessibilityDelegate set in ResolverActivity.onCreate because in
115         // our case this delegate must extend RecyclerViewAccessibilityDelegate, otherwise
116         // RecyclerView scrolling is broken
117         setAppListAccessibilityDelegate()
118     }
119 
120     override fun onStart() {
121         super.onStart()
122         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
123     }
124 
125     override fun onResume() {
126         super.onResume()
127         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
128     }
129 
130     override fun onPause() {
131         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
132         super.onPause()
133     }
134 
135     override fun onStop() {
136         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
137         super.onStop()
138     }
139 
140     override fun onConfigurationChanged(newConfig: Configuration) {
141         super.onConfigurationChanged(newConfig)
142         configurationController.onConfigurationChanged(newConfig)
143     }
144 
145     override fun appliedThemeResId(): Int = R.style.Theme_SystemUI_MediaProjectionAppSelector
146 
147     override fun createBlockerEmptyStateProvider(): EmptyStateProvider =
148         component.emptyStateProvider
149 
150     override fun createListController(userHandle: UserHandle): ResolverListController =
151         listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
152 
153     override fun startSelected(which: Int, always: Boolean, filtered: Boolean) {
154         val currentListAdapter = mChooserMultiProfilePagerAdapter.activeListAdapter
155         val targetInfo = currentListAdapter.targetInfoForPosition(which, filtered) ?: return
156         if (targetInfo is NotSelectableTargetInfo) return
157 
158         val intent = createIntent(targetInfo)
159 
160         val launchToken: IBinder = Binder("media_projection_launch_token")
161         val activityOptions = ActivityOptions.makeBasic()
162         activityOptions.launchCookie = launchToken
163 
164         val userHandle = mMultiProfilePagerAdapter.activeListAdapter.userHandle
165 
166         // Launch activity asynchronously and wait for the result, launching of an activity
167         // is typically very fast, so we don't show any loaders.
168         // We wait for the activity to be launched to make sure that the window of the activity
169         // is created and ready to be captured.
170         val activityStarted =
171             activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
172                 returnSelectedApp(launchToken)
173             }
174 
175         // Rely on the ActivityManager to pop up a dialog regarding app suspension
176         // and return false if suspended
177         if (!targetInfo.isSuspended && activityStarted) {
178             // TODO(b/222078415) track activity launch
179         }
180     }
181 
182     private fun createIntent(target: TargetInfo): Intent {
183         val intent = Intent(target.resolvedIntent)
184 
185         // Launch the app in a new task, so it won't be in the host's app task
186         intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
187 
188         // Remove activity forward result flag as this activity will
189         // return the media projection session
190         intent.flags = intent.flags and Intent.FLAG_ACTIVITY_FORWARD_RESULT.inv()
191 
192         return intent
193     }
194 
195     override fun onDestroy() {
196         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
197         component.lifecycleObservers.forEach { lifecycle.removeObserver(it) }
198         // onDestroy is also called when an app is selected, in that case we only want to send
199         // RECORD_CONTENT_TASK but not RECORD_CANCEL
200         if (!taskSelected) {
201             // TODO(b/272010156): Return result to PermissionActivity and update service there
202             MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
203                 RECORD_CANCEL,
204                 reviewGrantedConsentRequired,
205                 /* projection= */ null
206             )
207         }
208         activityLauncher.destroy()
209         controller.destroy()
210         super.onDestroy()
211     }
212 
213     override fun onActivityStarted(cti: TargetInfo) {
214         // do nothing
215     }
216 
217     override fun bind(recentTasks: List<RecentTask>) {
218         recentsViewController.bind(recentTasks)
219         if (!hasWorkProfile()) {
220             // Make sure to refresh the adapter, to show/hide the recents view depending on whether
221             // there are recents or not.
222             mMultiProfilePagerAdapter.personalListAdapter.notifyDataSetChanged()
223         }
224     }
225 
226     override fun returnSelectedApp(launchCookie: IBinder) {
227         taskSelected = true
228         if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
229             // The client requested to return the result in the result receiver instead of
230             // activity result, let's send the media projection to the result receiver
231             val resultReceiver =
232                 intent.getParcelableExtra(
233                     EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
234                     ResultReceiver::class.java
235                 ) as ResultReceiver
236             val captureRegion = MediaProjectionCaptureTarget(launchCookie)
237             val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
238             resultReceiver.send(RESULT_OK, data)
239             // TODO(b/279175710): Ensure consent result is always set here. Skipping this for now
240             //  in ScreenMediaRecorder, since we know the permission grant (projection) is never
241             //  reused in that scenario.
242         } else {
243             // TODO(b/272010156): Return result to PermissionActivity and update service there
244             // Return the media projection instance as activity result
245             val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
246             val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
247 
248             projection.launchCookie = launchCookie
249 
250             val intent = Intent()
251             intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
252             setResult(RESULT_OK, intent)
253             setForceSendResultForMediaProjection()
254             MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
255                 RECORD_CONTENT_TASK,
256                 reviewGrantedConsentRequired,
257                 projection
258             )
259         }
260 
261         finish()
262     }
263 
264     override fun shouldGetOnlyDefaultActivities() = false
265 
266     override fun shouldShowContentPreview() =
267         if (hasWorkProfile()) {
268             // When the user has a work profile, we can always set this to true, and the layout is
269             // adjusted automatically, and hide the recents view.
270             true
271         } else {
272             // When there is no work profile, we should only show the content preview if there are
273             // recents, otherwise the collapsed app selector will look empty.
274             recentsViewController.hasRecentTasks
275         }
276 
277     override fun shouldShowStickyContentPreviewWhenEmpty() = shouldShowContentPreview()
278 
279     override fun shouldShowServiceTargets() = false
280 
281     private fun hasWorkProfile() = mMultiProfilePagerAdapter.count > 1
282 
283     override fun createMyUserIdProvider(): MyUserIdProvider =
284         object : MyUserIdProvider() {
285             override fun getMyUserId(): Int = component.hostUserHandle.identifier
286         }
287 
288     override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
289         recentsViewController.createView(parent)
290 
291     companion object {
292         const val TAG = "MediaProjectionAppSelectorActivity"
293 
294         /**
295          * When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra the activity will
296          * send the [CaptureRegion] to the result receiver instead of returning media projection
297          * instance through activity result.
298          */
299         const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver"
300 
301         /** UID of the app that originally launched the media projection flow (host app user) */
302         const val EXTRA_HOST_APP_USER_HANDLE = "launched_from_user_handle"
303         const val KEY_CAPTURE_TARGET = "capture_region"
304 
305         /** Set up intent for the [ChooserActivity] */
306         private fun Intent.configureChooserIntent(
307             resources: Resources,
308             hostUserHandle: UserHandle,
309             personalProfileUserHandle: UserHandle
310         ) {
311             // Specify the query intent to show icons for all apps on the chooser screen
312             val queryIntent =
313                 Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
314             putExtra(Intent.EXTRA_INTENT, queryIntent)
315 
316             // Update the title of the chooser
317             val title = resources.getString(R.string.screen_share_permission_app_selector_title)
318             putExtra(Intent.EXTRA_TITLE, title)
319 
320             // Select host app's profile tab by default
321             val selectedProfile =
322                 if (hostUserHandle == personalProfileUserHandle) {
323                     PROFILE_PERSONAL
324                 } else {
325                     PROFILE_WORK
326                 }
327             putExtra(EXTRA_SELECTED_PROFILE, selectedProfile)
328         }
329     }
330 
331     private fun setAppListAccessibilityDelegate() {
332         val rdl = requireViewById<ResolverDrawerLayout>(com.android.internal.R.id.contentPanel)
333         for (i in 0 until mMultiProfilePagerAdapter.count) {
334             val list =
335                 mMultiProfilePagerAdapter
336                     .getItem(i)
337                     .rootView
338                     .findViewById<View>(com.android.internal.R.id.resolver_list)
339             if (list == null || list !is RecyclerView) {
340                 Log.wtf(TAG, "MediaProjection only supports RecyclerView")
341             } else {
342                 list.accessibilityDelegate = RecyclerViewExpandingAccessibilityDelegate(rdl, list)
343             }
344         }
345     }
346 
347     /**
348      * An a11y delegate propagating all a11y events to [AppListAccessibilityDelegate] so that it can
349      * expand drawer when needed. It needs to extend [RecyclerViewAccessibilityDelegate] because
350      * that superclass handles RecyclerView scrolling while using a11y services.
351      */
352     private class RecyclerViewExpandingAccessibilityDelegate(
353         rdl: ResolverDrawerLayout,
354         view: RecyclerView
355     ) : RecyclerViewAccessibilityDelegate(view) {
356 
357         private val delegate = AppListAccessibilityDelegate(rdl)
358 
359         override fun onRequestSendAccessibilityEvent(
360             host: ViewGroup,
361             child: View,
362             event: AccessibilityEvent
363         ): Boolean {
364             super.onRequestSendAccessibilityEvent(host, child, event)
365             return delegate.onRequestSendAccessibilityEvent(host, child, event)
366         }
367     }
368 }
369