1 /* 2 * Copyright (C) 2014 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 17 package android.media.projection; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.SuppressLint; 22 import android.annotation.SystemService; 23 import android.annotation.TestApi; 24 import android.app.Activity; 25 import android.app.ActivityOptions.LaunchCookie; 26 import android.compat.annotation.ChangeId; 27 import android.compat.annotation.Disabled; 28 import android.compat.annotation.Overridable; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.os.Handler; 33 import android.os.IBinder; 34 import android.os.RemoteException; 35 import android.os.ServiceManager; 36 import android.util.ArrayMap; 37 import android.util.Log; 38 import android.view.ContentRecordingSession; 39 import android.view.Surface; 40 41 import java.util.Map; 42 43 /** 44 * Manages the retrieval of certain types of {@link MediaProjection} tokens. 45 * 46 * <p><ol>An example flow of starting a media projection will be: 47 * <li>Declare a foreground service with the type {@code mediaProjection} in 48 * the {@code AndroidManifest.xml}. 49 * </li> 50 * <li>Create an intent by calling {@link MediaProjectionManager#createScreenCaptureIntent()} 51 * and pass this intent to {@link Activity#startActivityForResult(Intent, int)}. 52 * </li> 53 * <li>On getting {@link Activity#onActivityResult(int, int, Intent)}, 54 * start the foreground service with the type 55 * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}. 56 * </li> 57 * <li>Retrieve the media projection token by calling 58 * {@link MediaProjectionManager#getMediaProjection(int, Intent)} with the result code and 59 * intent from the {@link Activity#onActivityResult(int, int, Intent)} above. 60 * </li> 61 * <li>Start the screen capture session for media projection by calling 62 * {@link MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface, 63 * android.hardware.display.VirtualDisplay.Callback, Handler)}. 64 * </li> 65 * </ol> 66 */ 67 @SystemService(Context.MEDIA_PROJECTION_SERVICE) 68 public final class MediaProjectionManager { 69 private static final String TAG = "MediaProjectionManager"; 70 71 /** 72 * This change id ensures that users are presented with a choice of capturing a single app 73 * or the entire screen when initiating a MediaProjection session, overriding the usage of 74 * MediaProjectionConfig#createConfigForDefaultDisplay. 75 * 76 * @hide 77 */ 78 @ChangeId 79 @Overridable 80 @Disabled 81 public static final long OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION = 316897322L; 82 83 /** 84 * Intent extra to customize the permission dialog based on the host app's preferences. 85 * @hide 86 */ 87 public static final String EXTRA_MEDIA_PROJECTION_CONFIG = 88 "android.media.projection.extra.EXTRA_MEDIA_PROJECTION_CONFIG"; 89 /** @hide */ 90 public static final String EXTRA_APP_TOKEN = "android.media.projection.extra.EXTRA_APP_TOKEN"; 91 /** @hide */ 92 public static final String EXTRA_MEDIA_PROJECTION = 93 "android.media.projection.extra.EXTRA_MEDIA_PROJECTION"; 94 /** @hide */ 95 public static final String EXTRA_LAUNCH_COOKIE = 96 "android.media.projection.extra.EXTRA_LAUNCH_COOKIE"; 97 98 /** @hide */ 99 public static final int TYPE_SCREEN_CAPTURE = 0; 100 /** @hide */ 101 public static final int TYPE_MIRRORING = 1; 102 /** @hide */ 103 public static final int TYPE_PRESENTATION = 2; 104 105 private Context mContext; 106 private Map<Callback, CallbackDelegate> mCallbacks; 107 private IMediaProjectionManager mService; 108 109 /** @hide */ MediaProjectionManager(Context context)110 public MediaProjectionManager(Context context) { 111 mContext = context; 112 IBinder b = ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE); 113 mService = IMediaProjectionManager.Stub.asInterface(b); 114 mCallbacks = new ArrayMap<>(); 115 } 116 117 /** 118 * Returns an {@link Intent} that <b>must</b> be passed to 119 * {@link Activity#startActivityForResult(Intent, int)} (or similar) in order to start screen 120 * capture. The activity will prompt the user whether to allow screen capture. The result of 121 * this activity (received by overriding {@link Activity#onActivityResult(int, int, Intent) 122 * onActivityResult(int, int, Intent)}) should be passed to 123 * {@link #getMediaProjection(int, Intent)}. 124 * <p> 125 * Identical to calling {@link #createScreenCaptureIntent(MediaProjectionConfig)} with 126 * a {@link MediaProjectionConfig#createConfigForUserChoice()}. 127 * </p> 128 * <p> 129 * Should be used instead of {@link #createScreenCaptureIntent(MediaProjectionConfig)} when the 130 * calling app does not want to customize the activity shown to the user. 131 * </p> 132 */ 133 @NonNull createScreenCaptureIntent()134 public Intent createScreenCaptureIntent() { 135 Intent i = new Intent(); 136 final ComponentName mediaProjectionPermissionDialogComponent = 137 ComponentName.unflattenFromString(mContext.getResources().getString( 138 com.android.internal.R.string 139 .config_mediaProjectionPermissionDialogComponent)); 140 i.setComponent(mediaProjectionPermissionDialogComponent); 141 return i; 142 } 143 144 /** 145 * Returns an {@link Intent} that <b>must</b> be passed to 146 * {@link Activity#startActivityForResult(Intent, int)} (or similar) in order to start screen 147 * capture. Customizes the activity and resulting {@link MediaProjection} session based up 148 * the provided {@code config}. The activity will prompt the user whether to allow screen 149 * capture. The result of this activity (received by overriding 150 * {@link Activity#onActivityResult(int, int, Intent) onActivityResult(int, int, Intent)}) 151 * should be passed to {@link #getMediaProjection(int, Intent)}. 152 * 153 * <p> 154 * If {@link MediaProjectionConfig} was created from: 155 * <ul> 156 * <li> 157 * {@link MediaProjectionConfig#createConfigForDefaultDisplay()}, then creates an 158 * {@link Intent} for capturing the default display. The activity limits the user's 159 * choice to just the display specified. 160 * </li> 161 * <li> 162 * {@link MediaProjectionConfig#createConfigForUserChoice()}, then creates an 163 * {@link Intent} for deferring which region to capture to the user. This gives the 164 * user the same behaviour as calling {@link #createScreenCaptureIntent()}. The 165 * activity gives the user the choice between 166 * {@link android.view.Display#DEFAULT_DISPLAY}, or a different region. 167 * </li> 168 * </ul> 169 * </p> 170 * <p> 171 * Should be used instead of {@link #createScreenCaptureIntent()} when the calling app wants to 172 * customize the activity shown to the user. 173 * </p> 174 * 175 * @param config Customization for the {@link MediaProjection} that this {@link Intent} requests 176 * the user's consent for. 177 * @return An {@link Intent} requesting the user's consent, specialized based upon the given 178 * configuration. 179 */ 180 @NonNull createScreenCaptureIntent(@onNull MediaProjectionConfig config)181 public Intent createScreenCaptureIntent(@NonNull MediaProjectionConfig config) { 182 Intent i = createScreenCaptureIntent(); 183 i.putExtra(EXTRA_MEDIA_PROJECTION_CONFIG, config); 184 return i; 185 } 186 187 /** 188 * Returns an intent similar to {@link #createScreenCaptureIntent()} that will enable screen 189 * recording of the task with the specified launch cookie. This method should only be used for 190 * testing. 191 * 192 * @param launchCookie the launch cookie corresponding to the task to record. 193 * @hide 194 */ 195 @SuppressLint("UnflaggedApi") 196 @TestApi 197 @NonNull createScreenCaptureIntent(@onNull LaunchCookie launchCookie)198 public Intent createScreenCaptureIntent(@NonNull LaunchCookie launchCookie) { 199 Intent i = createScreenCaptureIntent(); 200 i.putExtra(EXTRA_LAUNCH_COOKIE, launchCookie); 201 return i; 202 } 203 204 /** 205 * Retrieves the {@link MediaProjection} obtained from a successful screen 206 * capture request. The result code and data from the request are provided by overriding 207 * {@link Activity#onActivityResult(int, int, Intent) onActivityResult(int, int, Intent)}, 208 * which is called after starting an activity using {@link #createScreenCaptureIntent()}. 209 * <p> 210 * Starting from Android {@link android.os.Build.VERSION_CODES#R R}, if your application 211 * requests the {@link android.Manifest.permission#SYSTEM_ALERT_WINDOW SYSTEM_ALERT_WINDOW} 212 * permission, and the user has not explicitly denied it, the permission will be automatically 213 * granted until the projection is stopped. The permission allows your app to display user 214 * controls on top of the screen being captured. 215 * </p> 216 * <p> 217 * An app targeting SDK version {@link android.os.Build.VERSION_CODES#Q Q} or later must 218 * invoke {@code getMediaProjection} and maintain the capture session 219 * ({@link MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface, 220 * android.hardware.display.VirtualDisplay.Callback, Handler) 221 * MediaProjection#createVirtualDisplay}) while running a foreground service. The app must set 222 * the {@link android.R.attr#foregroundServiceType foregroundServiceType} attribute to 223 * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION 224 * FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION} in the 225 * <a href="/guide/topics/manifest/service-element"><code><service></code></a> element of 226 * the app's manifest file. 227 * </p> 228 * <p> 229 * For an app targeting SDK version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} or 230 * later, the user must have granted the app with the permission to start a projection, 231 * before the app starts a foreground service with the type 232 * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}. 233 * Additionally, the app must have started the foreground service with that type before calling 234 * this API here, or else it'll receive a {@link SecurityException} from this API call, unless 235 * it's a privileged app. Apps can request the permission via the 236 * {@link #createScreenCaptureIntent()} and {@link Activity#startActivityForResult(Intent, int)} 237 * (or similar APIs). 238 * </p> 239 * 240 * @param resultCode The result code from {@link Activity#onActivityResult(int, int, Intent) 241 * onActivityResult(int, int, Intent)}. 242 * @param resultData The result data from {@link Activity#onActivityResult(int, int, Intent) 243 * onActivityResult(int, int, Intent)}. 244 * @return The media projection obtained from a successful screen capture request, or null if 245 * the result of the screen capture request is not {@link Activity#RESULT_OK RESULT_OK}. 246 * @throws IllegalStateException On 247 * pre-{@link android.os.Build.VERSION_CODES#Q Q} devices if a 248 * previously obtained {@code MediaProjection} from the same 249 * {@code resultData} has not yet been stopped. 250 * @throws SecurityException On {@link android.os.Build.VERSION_CODES#Q Q}+ devices if not 251 * invoked from a foreground service with type 252 * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION 253 * FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}, unless caller is a 254 * privileged app. 255 * @see <a href="/guide/components/foreground-services"> 256 * Foreground services developer guide</a> 257 * @see <a href="/guide/topics/large-screens/media-projection"> 258 * Media projection developer guide</a> 259 */ getMediaProjection(int resultCode, @NonNull Intent resultData)260 public MediaProjection getMediaProjection(int resultCode, @NonNull Intent resultData) { 261 if (resultCode != Activity.RESULT_OK || resultData == null) { 262 return null; 263 } 264 IBinder projection = resultData.getIBinderExtra(EXTRA_MEDIA_PROJECTION); 265 if (projection == null) { 266 return null; 267 } 268 // Don't do anything here if app is re-using the token; we check how often 269 // IMediaProjection#start is invoked. Fail to the app when they start recording. 270 return new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection)); 271 } 272 273 /** 274 * Get the {@link MediaProjectionInfo} for the active {@link MediaProjection}. 275 * @hide 276 */ getActiveProjectionInfo()277 public MediaProjectionInfo getActiveProjectionInfo() { 278 try { 279 return mService.getActiveProjectionInfo(); 280 } catch (RemoteException e) { 281 Log.e(TAG, "Unable to get the active projection info", e); 282 } 283 return null; 284 } 285 286 /** 287 * Stop the current projection if there is one. 288 * @hide 289 */ stopActiveProjection()290 public void stopActiveProjection() { 291 try { 292 Log.d(TAG, "Content Recording: stopping active projection"); 293 mService.stopActiveProjection(); 294 } catch (RemoteException e) { 295 Log.e(TAG, "Unable to stop the currently active media projection", e); 296 } 297 } 298 299 /** 300 * Add a callback to monitor all of the {@link MediaProjection}s activity. 301 * Not for use by regular applications, must have the MANAGE_MEDIA_PROJECTION permission. 302 * @hide 303 */ addCallback(@onNull Callback callback, @Nullable Handler handler)304 public void addCallback(@NonNull Callback callback, @Nullable Handler handler) { 305 if (callback == null) { 306 Log.w(TAG, "Content Recording: cannot add null callback"); 307 throw new IllegalArgumentException("callback must not be null"); 308 } 309 CallbackDelegate delegate = new CallbackDelegate(callback, handler); 310 mCallbacks.put(callback, delegate); 311 try { 312 mService.addCallback(delegate); 313 } catch (RemoteException e) { 314 Log.e(TAG, "Unable to add callbacks to MediaProjection service", e); 315 } 316 } 317 318 /** 319 * Remove a MediaProjection monitoring callback. 320 * @hide 321 */ removeCallback(@onNull Callback callback)322 public void removeCallback(@NonNull Callback callback) { 323 if (callback == null) { 324 Log.w(TAG, "ContentRecording: cannot remove null callback"); 325 throw new IllegalArgumentException("callback must not be null"); 326 } 327 CallbackDelegate delegate = mCallbacks.remove(callback); 328 try { 329 if (delegate != null) { 330 mService.removeCallback(delegate); 331 } 332 } catch (RemoteException e) { 333 Log.e(TAG, "Unable to add callbacks to MediaProjection service", e); 334 } 335 } 336 337 /** @hide */ 338 public static abstract class Callback { onStart(MediaProjectionInfo info)339 public abstract void onStart(MediaProjectionInfo info); 340 onStop(MediaProjectionInfo info)341 public abstract void onStop(MediaProjectionInfo info); 342 343 /** 344 * Called when the {@link ContentRecordingSession} was set for the current media 345 * projection. 346 * 347 * @param info always present and contains information about the media projection host. 348 * @param session the recording session for the current media projection. Can be 349 * {@code null} when the recording will stop. 350 */ onRecordingSessionSet( @onNull MediaProjectionInfo info, @Nullable ContentRecordingSession session )351 public void onRecordingSessionSet( 352 @NonNull MediaProjectionInfo info, 353 @Nullable ContentRecordingSession session 354 ) { 355 } 356 } 357 358 /** @hide */ 359 private final static class CallbackDelegate extends IMediaProjectionWatcherCallback.Stub { 360 private Callback mCallback; 361 private Handler mHandler; 362 CallbackDelegate(Callback callback, Handler handler)363 public CallbackDelegate(Callback callback, Handler handler) { 364 mCallback = callback; 365 if (handler == null) { 366 handler = new Handler(); 367 } 368 mHandler = handler; 369 } 370 371 @Override onStart(final MediaProjectionInfo info)372 public void onStart(final MediaProjectionInfo info) { 373 mHandler.post(new Runnable() { 374 @Override 375 public void run() { 376 mCallback.onStart(info); 377 } 378 }); 379 } 380 381 @Override onStop(final MediaProjectionInfo info)382 public void onStop(final MediaProjectionInfo info) { 383 mHandler.post(new Runnable() { 384 @Override 385 public void run() { 386 mCallback.onStop(info); 387 } 388 }); 389 } 390 391 @Override onRecordingSessionSet( @onNull final MediaProjectionInfo info, @Nullable final ContentRecordingSession session )392 public void onRecordingSessionSet( 393 @NonNull final MediaProjectionInfo info, 394 @Nullable final ContentRecordingSession session 395 ) { 396 mHandler.post(() -> mCallback.onRecordingSessionSet(info, session)); 397 } 398 } 399 } 400