• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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>&lt;service&gt;</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