• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.widget.ImageView
36 import androidx.annotation.ColorRes
37 import androidx.annotation.DrawableRes
38 import androidx.annotation.StringRes
39 import androidx.lifecycle.Lifecycle
40 import androidx.lifecycle.LifecycleOwner
41 import androidx.lifecycle.LifecycleRegistry
42 import com.android.internal.annotations.VisibleForTesting
43 import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider
44 import com.android.internal.app.AbstractMultiProfilePagerAdapter.MyUserIdProvider
45 import com.android.internal.app.ChooserActivity
46 import com.android.internal.app.ResolverListController
47 import com.android.internal.app.chooser.NotSelectableTargetInfo
48 import com.android.internal.app.chooser.TargetInfo
49 import com.android.internal.widget.RecyclerView
50 import com.android.internal.widget.RecyclerViewAccessibilityDelegate
51 import com.android.internal.widget.ResolverDrawerLayout
52 import com.android.systemui.mediaprojection.MediaProjectionCaptureTarget
53 import com.android.systemui.mediaprojection.MediaProjectionServiceHelper
54 import com.android.systemui.mediaprojection.appselector.data.RecentTask
55 import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
56 import com.android.systemui.res.R
57 import com.android.systemui.shared.system.ActivityManagerWrapper
58 import com.android.systemui.statusbar.policy.ConfigurationController
59 import com.android.systemui.util.AsyncActivityLauncher
60 import java.lang.IllegalArgumentException
61 import javax.inject.Inject
62 
63 class MediaProjectionAppSelectorActivity(
64     private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
65     private val activityLauncher: AsyncActivityLauncher,
66     private val activityManager: ActivityManagerWrapper,
67     /** This is used to override the dependency in a screenshot test */
68     @VisibleForTesting
69     private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?,
70 ) :
71     ChooserActivity(),
72     MediaProjectionAppSelectorView,
73     MediaProjectionAppSelectorResultHandler,
74     LifecycleOwner {
75 
76     @Inject
77     constructor(
78         componentFactory: MediaProjectionAppSelectorComponent.Factory,
79         activityLauncher: AsyncActivityLauncher,
80         activityManager: ActivityManagerWrapper,
81     ) : this(componentFactory, activityLauncher, activityManager, listControllerFactory = null)
82 
83     private val lifecycleRegistry = LifecycleRegistry(this)
84     override val lifecycle = lifecycleRegistry
85     private lateinit var configurationController: ConfigurationController
86     private lateinit var controller: MediaProjectionAppSelectorController
87     private lateinit var recentsViewController: MediaProjectionRecentsViewController
88     private lateinit var component: MediaProjectionAppSelectorComponent
89     // Indicate if we are under the media projection security flow
90     // i.e. when a host app reuses consent token, review the permission and update it to the service
91     private var reviewGrantedConsentRequired = false
92     // If an app is selected, set to true so that we don't send RECORD_CANCEL in onDestroy
93     private var taskSelected = false
94 
getLayoutResourcenull95     override fun getLayoutResource() = R.layout.media_projection_app_selector
96 
97     public override fun onCreate(savedInstanceState: Bundle?) {
98         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
99         component =
100             componentFactory.create(
101                 hostUserHandle = hostUserHandle,
102                 hostUid = hostUid,
103                 callingPackage = callingPackage,
104                 view = this,
105                 resultHandler = this,
106                 isFirstStart = savedInstanceState == null,
107             )
108         component.lifecycleObservers.forEach { lifecycle.addObserver(it) }
109 
110         // Create a separate configuration controller for this activity as the configuration
111         // might be different from the global one
112         configurationController = component.configurationController
113         controller = component.controller
114         recentsViewController = component.recentsViewController
115 
116         intent.configureChooserIntent(
117             resources,
118             component.hostUserHandle,
119             component.personalProfileUserHandle,
120         )
121 
122         reviewGrantedConsentRequired =
123             intent.getBooleanExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, false)
124 
125         super.onCreate(savedInstanceState)
126         controller.init()
127         setIcon()
128         // we override AppList's AccessibilityDelegate set in ResolverActivity.onCreate because in
129         // our case this delegate must extend RecyclerViewAccessibilityDelegate, otherwise
130         // RecyclerView scrolling is broken
131         setAppListAccessibilityDelegate()
132     }
133 
onStartnull134     override fun onStart() {
135         super.onStart()
136         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
137     }
138 
onResumenull139     override fun onResume() {
140         super.onResume()
141         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
142     }
143 
onPausenull144     override fun onPause() {
145         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
146         super.onPause()
147     }
148 
onStopnull149     override fun onStop() {
150         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
151         super.onStop()
152     }
153 
onConfigurationChangednull154     override fun onConfigurationChanged(newConfig: Configuration) {
155         super.onConfigurationChanged(newConfig)
156         configurationController.onConfigurationChanged(newConfig)
157     }
158 
appliedThemeResIdnull159     override fun appliedThemeResId(): Int = R.style.Theme_SystemUI_MediaProjectionAppSelector
160 
161     override fun createBlockerEmptyStateProvider(): EmptyStateProvider =
162         component.emptyStateProvider
163 
164     override fun createListController(userHandle: UserHandle): ResolverListController =
165         listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
166 
167     override fun startSelected(which: Int, always: Boolean, filtered: Boolean) {
168         val currentListAdapter = mChooserMultiProfilePagerAdapter.activeListAdapter
169         val targetInfo = currentListAdapter.targetInfoForPosition(which, filtered) ?: return
170         if (targetInfo is NotSelectableTargetInfo) return
171 
172         val intent = createIntent(targetInfo)
173 
174         val launchCookie = LaunchCookie("media_projection_launch_token")
175         val activityOptions = ActivityOptions.makeBasic()
176         activityOptions.setLaunchCookie(launchCookie)
177 
178         val userHandle = mMultiProfilePagerAdapter.activeListAdapter.userHandle
179 
180         // Launch activity asynchronously and wait for the result, launching of an activity
181         // is typically very fast, so we don't show any loaders.
182         // We wait for the activity to be launched to make sure that the window of the activity
183         // is created and ready to be captured.
184         val activityStarted =
185             activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
186                 if (targetInfo.resolvedComponentName == callingActivity) {
187                     // If attempting to launch the app used to launch the MediaProjection, then
188                     // provide the task id since the launch cookie won't match the existing task
189                     returnSelectedApp(launchCookie, taskId = activityManager.runningTask.taskId)
190                 } else {
191                     returnSelectedApp(launchCookie, taskId = -1)
192                 }
193             }
194 
195         // Rely on the ActivityManager to pop up a dialog regarding app suspension
196         // and return false if suspended
197         if (!targetInfo.isSuspended && activityStarted) {
198             // TODO(b/222078415) track activity launch
199         }
200     }
201 
createIntentnull202     private fun createIntent(target: TargetInfo): Intent {
203         val intent = Intent(target.resolvedIntent)
204 
205         // Launch the app in a new task, so it won't be in the host's app task
206         intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
207 
208         // Remove activity forward result flag as this activity will
209         // return the media projection session
210         intent.flags = intent.flags and Intent.FLAG_ACTIVITY_FORWARD_RESULT.inv()
211 
212         return intent
213     }
214 
onDestroynull215     override fun onDestroy() {
216         lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
217         component.lifecycleObservers.forEach { lifecycle.removeObserver(it) }
218         // onDestroy is also called when an app is selected, in that case we only want to send
219         // RECORD_CONTENT_TASK but not RECORD_CANCEL
220         if (!taskSelected) {
221             // TODO(b/272010156): Return result to PermissionActivity and update service there
222             MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
223                 RECORD_CANCEL,
224                 reviewGrantedConsentRequired,
225                 /* projection= */ null,
226             )
227             if (isFinishing) {
228                 // Only log dismissed when actually finishing, and not when changing configuration.
229                 controller.onSelectorDismissed()
230             }
231         }
232         activityLauncher.destroy()
233         controller.destroy()
234         super.onDestroy()
235     }
236 
onActivityStartednull237     override fun onActivityStarted(cti: TargetInfo) {
238         // do nothing
239     }
240 
bindnull241     override fun bind(recentTasks: List<RecentTask>) {
242         recentsViewController.bind(recentTasks)
243         if (!hasWorkProfile()) {
244             // Make sure to refresh the adapter, to show/hide the recents view depending on whether
245             // there are recents or not.
246             mMultiProfilePagerAdapter.personalListAdapter.notifyDataSetChanged()
247         }
248     }
249 
returnSelectedAppnull250     override fun returnSelectedApp(launchCookie: LaunchCookie, taskId: Int) {
251         taskSelected = true
252         if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
253             // The client requested to return the result in the result receiver instead of
254             // activity result, let's send the media projection to the result receiver
255             val resultReceiver =
256                 intent.getParcelableExtra(
257                     EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
258                     ResultReceiver::class.java,
259                 ) as ResultReceiver
260             val captureRegion = MediaProjectionCaptureTarget(launchCookie, taskId)
261             val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
262             resultReceiver.send(RESULT_OK, data)
263             // TODO(b/279175710): Ensure consent result is always set here. Skipping this for now
264             //  in ScreenMediaRecorder, since we know the permission grant (projection) is never
265             //  reused in that scenario.
266         } else {
267             // TODO(b/272010156): Return result to PermissionActivity and update service there
268             // Return the media projection instance as activity result
269             val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
270             val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
271 
272             projection.launchCookie = launchCookie
273             projection.taskId = taskId
274 
275             val intent = Intent()
276             intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
277             setResult(RESULT_OK, intent)
278             setForceSendResultForMediaProjection()
279             MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
280                 RECORD_CONTENT_TASK,
281                 reviewGrantedConsentRequired,
282                 projection,
283             )
284         }
285 
286         finish()
287     }
288 
shouldGetOnlyDefaultActivitiesnull289     override fun shouldGetOnlyDefaultActivities() = false
290 
291     override fun shouldShowContentPreview() =
292         if (hasWorkProfile()) {
293             // When the user has a work profile, we can always set this to true, and the layout is
294             // adjusted automatically, and hide the recents view.
295             true
296         } else {
297             // When there is no work profile, we should only show the content preview if there are
298             // recents, otherwise the collapsed app selector will look empty.
299             recentsViewController.hasRecentTasks
300         }
301 
shouldShowStickyContentPreviewWhenEmptynull302     override fun shouldShowStickyContentPreviewWhenEmpty() = shouldShowContentPreview()
303 
304     override fun shouldShowServiceTargets() = false
305 
306     private fun hasWorkProfile() = mMultiProfilePagerAdapter.count > 1
307 
308     override fun createMyUserIdProvider(): MyUserIdProvider =
309         object : MyUserIdProvider() {
310             override fun getMyUserId(): Int = component.hostUserHandle.identifier
311         }
312 
createContentPreviewViewnull313     override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
314         recentsViewController.createView(parent)
315 
316     /** Set up intent for the [ChooserActivity] */
317     private fun Intent.configureChooserIntent(
318         resources: Resources,
319         hostUserHandle: UserHandle,
320         personalProfileUserHandle: UserHandle,
321     ) {
322         // Specify the query intent to show icons for all apps on the chooser screen
323         val queryIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
324         putExtra(Intent.EXTRA_INTENT, queryIntent)
325 
326         // Update the title of the chooser
327         putExtra(Intent.EXTRA_TITLE, resources.getString(titleResId))
328 
329         // Select host app's profile tab by default
330         val selectedProfile =
331             if (hostUserHandle == personalProfileUserHandle) {
332                 PROFILE_PERSONAL
333             } else {
334                 PROFILE_WORK
335             }
336         putExtra(EXTRA_SELECTED_PROFILE, selectedProfile)
337     }
338 
339     private val hostUserHandle: UserHandle
340         get() {
341             val extras =
342                 intent.extras
343                     ?: error("MediaProjectionAppSelectorActivity should be launched with extras")
344             return extras.getParcelable(EXTRA_HOST_APP_USER_HANDLE)
345                 ?: error(
346                     "MediaProjectionAppSelectorActivity should be provided with " +
347                         "$EXTRA_HOST_APP_USER_HANDLE extra"
348                 )
349         }
350 
351     private val hostUid: Int
352         get() {
353             if (!intent.hasExtra(EXTRA_HOST_APP_UID)) {
354                 error(
355                     "MediaProjectionAppSelectorActivity should be provided with " +
356                         "$EXTRA_HOST_APP_UID extra"
357                 )
358             }
359             return intent.getIntExtra(EXTRA_HOST_APP_UID, /* defaultValue= */ -1)
360         }
361 
362     /**
363      * The type of screen sharing being performed. Used to show the right text and icon in the
364      * activity.
365      */
366     private val screenShareType: ScreenShareType?
367         get() {
368             if (!intent.hasExtra(EXTRA_SCREEN_SHARE_TYPE)) {
369                 return null
370             } else {
371                 val type = intent.getStringExtra(EXTRA_SCREEN_SHARE_TYPE) ?: return null
372                 return try {
373                     enumValueOf<ScreenShareType>(type)
374                 } catch (e: IllegalArgumentException) {
375                     null
376                 }
377             }
378         }
379 
380     @get:StringRes
381     private val titleResId: Int
382         get() =
383             when (screenShareType) {
384                 ScreenShareType.ShareToApp ->
385                     R.string.media_projection_entry_share_app_selector_title
386                 ScreenShareType.SystemCast ->
387                     R.string.media_projection_entry_cast_app_selector_title
388                 ScreenShareType.ScreenRecord -> R.string.screenrecord_app_selector_title
389                 null -> R.string.screen_share_generic_app_selector_title
390             }
391 
392     @get:DrawableRes
393     private val iconResId: Int
394         get() =
395             when (screenShareType) {
396                 ScreenShareType.ShareToApp -> R.drawable.ic_present_to_all
397                 ScreenShareType.SystemCast -> R.drawable.ic_cast_connected
398                 ScreenShareType.ScreenRecord -> R.drawable.ic_screenrecord
399                 null -> R.drawable.ic_present_to_all
400             }
401 
402     @get:ColorRes
403     private val iconTintResId: Int?
404         get() =
405             when (screenShareType) {
406                 ScreenShareType.ScreenRecord -> R.color.screenrecord_icon_color
407                 else -> null
408             }
409 
410     companion object {
411         const val TAG = "MediaProjectionAppSelectorActivity"
412 
413         /**
414          * When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra the activity will
415          * send the [CaptureRegion] to the result receiver instead of returning media projection
416          * instance through activity result.
417          */
418         const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver"
419 
420         /**
421          * User on the device that launched the media projection flow. (Primary, Secondary, Guest,
422          * Work, etc)
423          */
424         const val EXTRA_HOST_APP_USER_HANDLE = "launched_from_user_handle"
425         /**
426          * The kernel user-ID that has been assigned to the app that originally launched the media
427          * projection flow.
428          */
429         const val EXTRA_HOST_APP_UID = "launched_from_host_uid"
430         const val KEY_CAPTURE_TARGET = "capture_region"
431 
432         /**
433          * The type of screen sharing being performed.
434          *
435          * The value set for this extra should match the name of a [ScreenShareType].
436          */
437         const val EXTRA_SCREEN_SHARE_TYPE = "screen_share_type"
438     }
439 
setIconnull440     private fun setIcon() {
441         val iconView = findViewById<ImageView>(R.id.media_projection_app_selector_icon) ?: return
442         iconView.setImageResource(iconResId)
443         iconTintResId?.let { iconView.setColorFilter(this.resources.getColor(it, this.theme)) }
444     }
445 
setAppListAccessibilityDelegatenull446     private fun setAppListAccessibilityDelegate() {
447         val rdl = requireViewById<ResolverDrawerLayout>(com.android.internal.R.id.contentPanel)
448         for (i in 0 until mMultiProfilePagerAdapter.count) {
449             val list =
450                 mMultiProfilePagerAdapter
451                     .getItem(i)
452                     .rootView
453                     .findViewById<View>(com.android.internal.R.id.resolver_list)
454             if (list == null || list !is RecyclerView) {
455                 Log.wtf(TAG, "MediaProjection only supports RecyclerView")
456             } else {
457                 list.accessibilityDelegate = RecyclerViewExpandingAccessibilityDelegate(rdl, list)
458             }
459         }
460     }
461 
462     /**
463      * An a11y delegate propagating all a11y events to [AppListAccessibilityDelegate] so that it can
464      * expand drawer when needed. It needs to extend [RecyclerViewAccessibilityDelegate] because
465      * that superclass handles RecyclerView scrolling while using a11y services.
466      */
467     private class RecyclerViewExpandingAccessibilityDelegate(
468         rdl: ResolverDrawerLayout,
469         view: RecyclerView,
470     ) : RecyclerViewAccessibilityDelegate(view) {
471 
472         private val delegate = AppListAccessibilityDelegate(rdl)
473 
onRequestSendAccessibilityEventnull474         override fun onRequestSendAccessibilityEvent(
475             host: ViewGroup,
476             child: View,
477             event: AccessibilityEvent,
478         ): Boolean {
479             super.onRequestSendAccessibilityEvent(host, child, event)
480             return delegate.onRequestSendAccessibilityEvent(host, child, event)
481         }
482     }
483 
484     /** Enum describing what type of app screen sharing is being performed. */
485     enum class ScreenShareType {
486         /** The selected app will be cast to another device. */
487         SystemCast,
488         /** The selected app will be shared to another app on the device. */
489         ShareToApp,
490         /** The selected app will be recorded. */
491         ScreenRecord,
492     }
493 }
494