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