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