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