• 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.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.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
24 import android.os.Binder
25 import android.os.Bundle
26 import android.os.IBinder
27 import android.os.ResultReceiver
28 import android.os.UserHandle
29 import android.view.ViewGroup
30 import com.android.internal.annotations.VisibleForTesting
31 import com.android.internal.app.AbstractMultiProfilePagerAdapter.EmptyStateProvider
32 import com.android.internal.app.AbstractMultiProfilePagerAdapter.MyUserIdProvider
33 import com.android.internal.app.ChooserActivity
34 import com.android.internal.app.ResolverListController
35 import com.android.internal.app.chooser.NotSelectableTargetInfo
36 import com.android.internal.app.chooser.TargetInfo
37 import com.android.systemui.R
38 import com.android.systemui.flags.FeatureFlags
39 import com.android.systemui.flags.Flags
40 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorComponent
41 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
42 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
43 import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorView
44 import com.android.systemui.mediaprojection.appselector.data.RecentTask
45 import com.android.systemui.mediaprojection.appselector.view.MediaProjectionRecentsViewController
46 import com.android.systemui.statusbar.policy.ConfigurationController
47 import com.android.systemui.util.AsyncActivityLauncher
48 import javax.inject.Inject
49 
50 class MediaProjectionAppSelectorActivity(
51     private val componentFactory: MediaProjectionAppSelectorComponent.Factory,
52     private val activityLauncher: AsyncActivityLauncher,
53     private val featureFlags: FeatureFlags,
54     /** This is used to override the dependency in a screenshot test */
55     @VisibleForTesting
56     private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
57 ) : ChooserActivity(), MediaProjectionAppSelectorView, MediaProjectionAppSelectorResultHandler {
58 
59     @Inject
60     constructor(
61         componentFactory: MediaProjectionAppSelectorComponent.Factory,
62         activityLauncher: AsyncActivityLauncher,
63         featureFlags: FeatureFlags
64     ) : this(componentFactory, activityLauncher, featureFlags, listControllerFactory = null)
65 
66     private lateinit var configurationController: ConfigurationController
67     private lateinit var controller: MediaProjectionAppSelectorController
68     private lateinit var recentsViewController: MediaProjectionRecentsViewController
69     private lateinit var component: MediaProjectionAppSelectorComponent
70 
getLayoutResourcenull71     override fun getLayoutResource() = R.layout.media_projection_app_selector
72 
73     public override fun onCreate(bundle: Bundle?) {
74         component = componentFactory.create(activity = this, view = this, resultHandler = this)
75 
76         // Create a separate configuration controller for this activity as the configuration
77         // might be different from the global one
78         configurationController = component.configurationController
79         controller = component.controller
80         recentsViewController = component.recentsViewController
81 
82         intent.configureChooserIntent(
83             resources,
84             component.hostUserHandle,
85             component.personalProfileUserHandle
86         )
87 
88         super.onCreate(bundle)
89         controller.init()
90     }
91 
onConfigurationChangednull92     override fun onConfigurationChanged(newConfig: Configuration) {
93         super.onConfigurationChanged(newConfig)
94         configurationController.onConfigurationChanged(newConfig)
95     }
96 
appliedThemeResIdnull97     override fun appliedThemeResId(): Int = R.style.Theme_SystemUI_MediaProjectionAppSelector
98 
99     override fun createBlockerEmptyStateProvider(): EmptyStateProvider =
100         if (featureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
101             component.emptyStateProvider
102         } else {
103             super.createBlockerEmptyStateProvider()
104         }
105 
createListControllernull106     override fun createListController(userHandle: UserHandle): ResolverListController =
107         listControllerFactory?.invoke(userHandle) ?: super.createListController(userHandle)
108 
109     override fun startSelected(which: Int, always: Boolean, filtered: Boolean) {
110         val currentListAdapter = mChooserMultiProfilePagerAdapter.activeListAdapter
111         val targetInfo = currentListAdapter.targetInfoForPosition(which, filtered) ?: return
112         if (targetInfo is NotSelectableTargetInfo) return
113 
114         val intent = createIntent(targetInfo)
115 
116         val launchToken: IBinder = Binder("media_projection_launch_token")
117         val activityOptions = ActivityOptions.makeBasic()
118         activityOptions.launchCookie = launchToken
119 
120         val userHandle = mMultiProfilePagerAdapter.activeListAdapter.userHandle
121 
122         // Launch activity asynchronously and wait for the result, launching of an activity
123         // is typically very fast, so we don't show any loaders.
124         // We wait for the activity to be launched to make sure that the window of the activity
125         // is created and ready to be captured.
126         val activityStarted =
127             activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) {
128                 returnSelectedApp(launchToken)
129             }
130 
131         // Rely on the ActivityManager to pop up a dialog regarding app suspension
132         // and return false if suspended
133         if (!targetInfo.isSuspended && activityStarted) {
134             // TODO(b/222078415) track activity launch
135         }
136     }
137 
createIntentnull138     private fun createIntent(target: TargetInfo): Intent {
139         val intent = Intent(target.resolvedIntent)
140 
141         // Launch the app in a new task, so it won't be in the host's app task
142         intent.flags = intent.flags or Intent.FLAG_ACTIVITY_NEW_TASK
143 
144         // Remove activity forward result flag as this activity will
145         // return the media projection session
146         intent.flags = intent.flags and Intent.FLAG_ACTIVITY_FORWARD_RESULT.inv()
147 
148         return intent
149     }
150 
onDestroynull151     override fun onDestroy() {
152         activityLauncher.destroy()
153         controller.destroy()
154         super.onDestroy()
155     }
156 
onActivityStartednull157     override fun onActivityStarted(cti: TargetInfo) {
158         // do nothing
159     }
160 
bindnull161     override fun bind(recentTasks: List<RecentTask>) {
162         recentsViewController.bind(recentTasks)
163     }
164 
returnSelectedAppnull165     override fun returnSelectedApp(launchCookie: IBinder) {
166         if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
167             // The client requested to return the result in the result receiver instead of
168             // activity result, let's send the media projection to the result receiver
169             val resultReceiver =
170                 intent.getParcelableExtra(
171                     EXTRA_CAPTURE_REGION_RESULT_RECEIVER,
172                     ResultReceiver::class.java
173                 ) as ResultReceiver
174             val captureRegion = MediaProjectionCaptureTarget(launchCookie)
175             val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
176             resultReceiver.send(RESULT_OK, data)
177         } else {
178             // Return the media projection instance as activity result
179             val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
180             val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
181 
182             projection.launchCookie = launchCookie
183 
184             val intent = Intent()
185             intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
186             setResult(RESULT_OK, intent)
187             setForceSendResultForMediaProjection()
188         }
189 
190         finish()
191     }
192 
shouldGetOnlyDefaultActivitiesnull193     override fun shouldGetOnlyDefaultActivities() = false
194 
195     override fun shouldShowContentPreview() = true
196 
197     override fun shouldShowContentPreviewWhenEmpty(): Boolean = true
198 
199     override fun createMyUserIdProvider(): MyUserIdProvider =
200         object : MyUserIdProvider() {
201             override fun getMyUserId(): Int = component.hostUserHandle.identifier
202         }
203 
createContentPreviewViewnull204     override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
205         recentsViewController.createView(parent)
206 
207     companion object {
208         /**
209          * When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra the activity will
210          * send the [CaptureRegion] to the result receiver instead of returning media projection
211          * instance through activity result.
212          */
213         const val EXTRA_CAPTURE_REGION_RESULT_RECEIVER = "capture_region_result_receiver"
214 
215         /** UID of the app that originally launched the media projection flow (host app user) */
216         const val EXTRA_HOST_APP_USER_HANDLE = "launched_from_user_handle"
217         const val KEY_CAPTURE_TARGET = "capture_region"
218 
219         /** Set up intent for the [ChooserActivity] */
220         private fun Intent.configureChooserIntent(
221             resources: Resources,
222             hostUserHandle: UserHandle,
223             personalProfileUserHandle: UserHandle
224         ) {
225             // Specify the query intent to show icons for all apps on the chooser screen
226             val queryIntent =
227                 Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
228             putExtra(Intent.EXTRA_INTENT, queryIntent)
229 
230             // Update the title of the chooser
231             val title = resources.getString(R.string.media_projection_permission_app_selector_title)
232             putExtra(Intent.EXTRA_TITLE, title)
233 
234             // Select host app's profile tab by default
235             val selectedProfile =
236                 if (hostUserHandle == personalProfileUserHandle) {
237                     PROFILE_PERSONAL
238                 } else {
239                     PROFILE_WORK
240                 }
241             putExtra(EXTRA_SELECTED_PROFILE, selectedProfile)
242         }
243     }
244 }
245