blob: 9790d02025b7039415a78f9e2dc88cf61444f8b6 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media.projection;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.ArrayMap;
import android.util.Log;
import android.view.ContentRecordingSession;
import android.view.Surface;
import java.util.Map;
/**
* Manages the retrieval of certain types of {@link MediaProjection} tokens.
*
* <p><ol>An example flow of starting a media projection will be:
* <li>Declare a foreground service with the type {@code mediaProjection} in
* the {@code AndroidManifest.xml}.
* </li>
* <li>Create an intent by calling {@link MediaProjectionManager#createScreenCaptureIntent()}
* and pass this intent to {@link Activity#startActivityForResult(Intent, int)}.
* </li>
* <li>On getting {@link Activity#onActivityResult(int, int, Intent)},
* start the foreground service with the type
* {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}.
* </li>
* <li>Retrieve the media projection token by calling
* {@link MediaProjectionManager#getMediaProjection(int, Intent)} with the result code and
* intent from the {@link Activity#onActivityResult(int, int, Intent)} above.
* </li>
* <li>Start the screen capture session for media projection by calling
* {@link MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
* android.hardware.display.VirtualDisplay.Callback, Handler)}.
* </li>
* </ol>
*/
@SystemService(Context.MEDIA_PROJECTION_SERVICE)
public final class MediaProjectionManager {
private static final String TAG = "MediaProjectionManager";
/**
* Intent extra to customize the permission dialog based on the host app's preferences.
* @hide
*/
public static final String EXTRA_MEDIA_PROJECTION_CONFIG =
"android.media.projection.extra.EXTRA_MEDIA_PROJECTION_CONFIG";
/** @hide */
public static final String EXTRA_APP_TOKEN = "android.media.projection.extra.EXTRA_APP_TOKEN";
/** @hide */
public static final String EXTRA_MEDIA_PROJECTION =
"android.media.projection.extra.EXTRA_MEDIA_PROJECTION";
/** @hide */
public static final int TYPE_SCREEN_CAPTURE = 0;
/** @hide */
public static final int TYPE_MIRRORING = 1;
/** @hide */
public static final int TYPE_PRESENTATION = 2;
private Context mContext;
private Map<Callback, CallbackDelegate> mCallbacks;
private IMediaProjectionManager mService;
/** @hide */
public MediaProjectionManager(Context context) {
mContext = context;
IBinder b = ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE);
mService = IMediaProjectionManager.Stub.asInterface(b);
mCallbacks = new ArrayMap<>();
}
/**
* Returns an {@link Intent} that <b>must</b> be passed to
* {@link Activity#startActivityForResult(Intent, int)} (or similar) in order to start screen
* capture. The activity will prompt the user whether to allow screen capture. The result of
* this activity (received by overriding {@link Activity#onActivityResult(int, int, Intent)
* onActivityResult(int, int, Intent)}) should be passed to
* {@link #getMediaProjection(int, Intent)}.
* <p>
* Identical to calling {@link #createScreenCaptureIntent(MediaProjectionConfig)} with
* a {@link MediaProjectionConfig#createConfigForUserChoice()}.
* </p>
* <p>
* Should be used instead of {@link #createScreenCaptureIntent(MediaProjectionConfig)} when the
* calling app does not want to customize the activity shown to the user.
* </p>
*/
@NonNull
public Intent createScreenCaptureIntent() {
Intent i = new Intent();
final ComponentName mediaProjectionPermissionDialogComponent =
ComponentName.unflattenFromString(mContext.getResources().getString(
com.android.internal.R.string
.config_mediaProjectionPermissionDialogComponent));
i.setComponent(mediaProjectionPermissionDialogComponent);
return i;
}
/**
* Returns an {@link Intent} that <b>must</b> be passed to
* {@link Activity#startActivityForResult(Intent, int)} (or similar) in order to start screen
* capture. Customizes the activity and resulting {@link MediaProjection} session based up
* the provided {@code config}. The activity will prompt the user whether to allow screen
* capture. The result of this activity (received by overriding
* {@link Activity#onActivityResult(int, int, Intent) onActivityResult(int, int, Intent)})
* should be passed to {@link #getMediaProjection(int, Intent)}.
*
* <p>
* If {@link MediaProjectionConfig} was created from:
* <ul>
* <li>
* {@link MediaProjectionConfig#createConfigForDefaultDisplay()}, then creates an
* {@link Intent} for capturing the default display. The activity limits the user's
* choice to just the display specified.
* </li>
* <li>
* {@link MediaProjectionConfig#createConfigForUserChoice()}, then creates an
* {@link Intent} for deferring which region to capture to the user. This gives the
* user the same behaviour as calling {@link #createScreenCaptureIntent()}. The
* activity gives the user the choice between
* {@link android.view.Display#DEFAULT_DISPLAY}, or a different region.
* </li>
* </ul>
* </p>
* <p>
* Should be used instead of {@link #createScreenCaptureIntent()} when the calling app wants to
* customize the activity shown to the user.
* </p>
*
* @param config Customization for the {@link MediaProjection} that this {@link Intent} requests
* the user's consent for.
* @return An {@link Intent} requesting the user's consent, specialized based upon the given
* configuration.
*/
@NonNull
public Intent createScreenCaptureIntent(@NonNull MediaProjectionConfig config) {
Intent i = new Intent();
final ComponentName mediaProjectionPermissionDialogComponent =
ComponentName.unflattenFromString(mContext.getResources()
.getString(com.android.internal.R.string
.config_mediaProjectionPermissionDialogComponent));
i.setComponent(mediaProjectionPermissionDialogComponent);
i.putExtra(EXTRA_MEDIA_PROJECTION_CONFIG, config);
return i;
}
/**
* Retrieves the {@link MediaProjection} obtained from a successful screen
* capture request. The result code and data from the request are provided by overriding
* {@link Activity#onActivityResult(int, int, Intent) onActivityResult(int, int, Intent)},
* which is called after starting an activity using {@link #createScreenCaptureIntent()}.
* <p>
* Starting from Android {@link android.os.Build.VERSION_CODES#R R}, if your application
* requests the {@link android.Manifest.permission#SYSTEM_ALERT_WINDOW SYSTEM_ALERT_WINDOW}
* permission, and the user has not explicitly denied it, the permission will be automatically
* granted until the projection is stopped. The permission allows your app to display user
* controls on top of the screen being captured.
* </p>
* <p>
* An app targeting SDK version {@link android.os.Build.VERSION_CODES#Q Q} or later must
* invoke {@code getMediaProjection} and maintain the capture session
* ({@link MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
* android.hardware.display.VirtualDisplay.Callback, Handler)
* MediaProjection#createVirtualDisplay}) while running a foreground service. The app must set
* the {@link android.R.attr#foregroundServiceType foregroundServiceType} attribute to
* {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
* FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION} in the
* <a href="/guide/topics/manifest/service-element"><code>&lt;service&gt;</code></a> element of
* the app's manifest file.
* </p>
* <p>
* For an app targeting SDK version {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE U} or
* later, the user must have granted the app with the permission to start a projection,
* before the app starts a foreground service with the type
* {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}.
* Additionally, the app must have started the foreground service with that type before calling
* this API here, or else it'll receive a {@link SecurityException} from this API call, unless
* it's a privileged app. Apps can request the permission via the
* {@link #createScreenCaptureIntent()} and {@link Activity#startActivityForResult(Intent, int)}
* (or similar APIs).
* </p>
*
* @param resultCode The result code from {@link Activity#onActivityResult(int, int, Intent)
* onActivityResult(int, int, Intent)}.
* @param resultData The result data from {@link Activity#onActivityResult(int, int, Intent)
* onActivityResult(int, int, Intent)}.
* @return The media projection obtained from a successful screen capture request, or null if
* the result of the screen capture request is not {@link Activity#RESULT_OK RESULT_OK}.
* @throws IllegalStateException On
* pre-{@link android.os.Build.VERSION_CODES#Q Q} devices if a
* previously obtained {@code MediaProjection} from the same
* {@code resultData} has not yet been stopped.
* @throws SecurityException On {@link android.os.Build.VERSION_CODES#Q Q}+ devices if not
* invoked from a foreground service with type
* {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
* FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}, unless caller is a
* privileged app.
* @see <a href="/guide/components/foreground-services">
* Foreground services developer guide</a>
* @see <a href="/guide/topics/large-screens/media-projection">
* Media projection developer guide</a>
*/
public MediaProjection getMediaProjection(int resultCode, @NonNull Intent resultData) {
if (resultCode != Activity.RESULT_OK || resultData == null) {
return null;
}
IBinder projection = resultData.getIBinderExtra(EXTRA_MEDIA_PROJECTION);
if (projection == null) {
return null;
}
// Don't do anything here if app is re-using the token; we check how often
// IMediaProjection#start is invoked. Fail to the app when they start recording.
return new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection));
}
/**
* Get the {@link MediaProjectionInfo} for the active {@link MediaProjection}.
* @hide
*/
public MediaProjectionInfo getActiveProjectionInfo() {
try {
return mService.getActiveProjectionInfo();
} catch (RemoteException e) {
Log.e(TAG, "Unable to get the active projection info", e);
}
return null;
}
/**
* Stop the current projection if there is one.
* @hide
*/
public void stopActiveProjection() {
try {
Log.d(TAG, "Content Recording: stopping active projection");
mService.stopActiveProjection();
} catch (RemoteException e) {
Log.e(TAG, "Unable to stop the currently active media projection", e);
}
}
/**
* Add a callback to monitor all of the {@link MediaProjection}s activity.
* Not for use by regular applications, must have the MANAGE_MEDIA_PROJECTION permission.
* @hide
*/
public void addCallback(@NonNull Callback callback, @Nullable Handler handler) {
if (callback == null) {
Log.w(TAG, "Content Recording: cannot add null callback");
throw new IllegalArgumentException("callback must not be null");
}
CallbackDelegate delegate = new CallbackDelegate(callback, handler);
mCallbacks.put(callback, delegate);
try {
mService.addCallback(delegate);
} catch (RemoteException e) {
Log.e(TAG, "Unable to add callbacks to MediaProjection service", e);
}
}
/**
* Remove a MediaProjection monitoring callback.
* @hide
*/
public void removeCallback(@NonNull Callback callback) {
if (callback == null) {
Log.w(TAG, "ContentRecording: cannot remove null callback");
throw new IllegalArgumentException("callback must not be null");
}
CallbackDelegate delegate = mCallbacks.remove(callback);
try {
if (delegate != null) {
mService.removeCallback(delegate);
}
} catch (RemoteException e) {
Log.e(TAG, "Unable to add callbacks to MediaProjection service", e);
}
}
/** @hide */
public static abstract class Callback {
public abstract void onStart(MediaProjectionInfo info);
public abstract void onStop(MediaProjectionInfo info);
/**
* Called when the {@link ContentRecordingSession} was set for the current media
* projection.
*
* @param info always present and contains information about the media projection host.
* @param session the recording session for the current media projection. Can be
* {@code null} when the recording will stop.
*/
public void onRecordingSessionSet(
@NonNull MediaProjectionInfo info,
@Nullable ContentRecordingSession session
) {
}
}
/** @hide */
private final static class CallbackDelegate extends IMediaProjectionWatcherCallback.Stub {
private Callback mCallback;
private Handler mHandler;
public CallbackDelegate(Callback callback, Handler handler) {
mCallback = callback;
if (handler == null) {
handler = new Handler();
}
mHandler = handler;
}
@Override
public void onStart(final MediaProjectionInfo info) {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onStart(info);
}
});
}
@Override
public void onStop(final MediaProjectionInfo info) {
mHandler.post(new Runnable() {
@Override
public void run() {
mCallback.onStop(info);
}
});
}
@Override
public void onRecordingSessionSet(
@NonNull final MediaProjectionInfo info,
@Nullable final ContentRecordingSession session
) {
mHandler.post(() -> mCallback.onRecordingSessionSet(info, session));
}
}
}