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