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