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>&lt;service&gt;</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