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