diff --git a/Android.mk b/Android.mk index 7a8907e02bdf7..79500eaab5bec 100644 --- a/Android.mk +++ b/Android.mk @@ -148,6 +148,7 @@ LOCAL_SRC_FILES += \ core/java/android/hardware/ISerialManager.aidl \ core/java/android/hardware/display/IDisplayManager.aidl \ core/java/android/hardware/display/IDisplayManagerCallback.aidl \ + core/java/android/hardware/display/IVirtualDisplayCallbacks.aidl \ core/java/android/hardware/hdmi/IHdmiControlCallback.aidl \ core/java/android/hardware/hdmi/IHdmiControlService.aidl \ core/java/android/hardware/hdmi/IHdmiDeviceEventListener.aidl \ @@ -325,6 +326,9 @@ LOCAL_SRC_FILES += \ media/java/android/media/routing/IMediaRouterDelegate.aidl \ media/java/android/media/routing/IMediaRouterRoutingCallback.aidl \ media/java/android/media/routing/IMediaRouterStateCallback.aidl \ + media/java/android/media/projection/IMediaProjection.aidl \ + media/java/android/media/projection/IMediaProjectionCallback.aidl \ + media/java/android/media/projection/IMediaProjectionManager.aidl \ media/java/android/media/session/IActiveSessionsListener.aidl \ media/java/android/media/session/ISessionController.aidl \ media/java/android/media/session/ISessionControllerCallback.aidl \ diff --git a/api/current.txt b/api/current.txt index 633d0b246e3b4..2204fbb86fef6 100644 --- a/api/current.txt +++ b/api/current.txt @@ -7191,6 +7191,7 @@ package android.content { field public static final java.lang.String LAUNCHER_APPS_SERVICE = "launcherapps"; field public static final java.lang.String LAYOUT_INFLATER_SERVICE = "layout_inflater"; field public static final java.lang.String LOCATION_SERVICE = "location"; + field public static final java.lang.String MEDIA_PROJECTION_SERVICE = "media_projection"; field public static final java.lang.String MEDIA_ROUTER_SERVICE = "media_router"; field public static final java.lang.String MEDIA_SESSION_SERVICE = "media_session"; field public static final int MODE_APPEND = 32768; // 0x8000 @@ -13107,6 +13108,7 @@ package android.hardware.display { public final class DisplayManager { method public android.hardware.display.VirtualDisplay createVirtualDisplay(java.lang.String, int, int, int, android.view.Surface, int); + method public android.hardware.display.VirtualDisplay createVirtualDisplay(java.lang.String, int, int, int, android.view.Surface, int, android.hardware.display.VirtualDisplay.Callbacks, android.os.Handler); method public android.view.Display getDisplay(int); method public android.view.Display[] getDisplays(); method public android.view.Display[] getDisplays(java.lang.String); @@ -13116,6 +13118,7 @@ package android.hardware.display { field public static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = 8; // 0x8 field public static final int VIRTUAL_DISPLAY_FLAG_PRESENTATION = 2; // 0x2 field public static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = 1; // 0x1 + field public static final int VIRTUAL_DISPLAY_FLAG_SCREEN_SHARE = 16; // 0x10 field public static final int VIRTUAL_DISPLAY_FLAG_SECURE = 4; // 0x4 } @@ -13132,6 +13135,13 @@ package android.hardware.display { method public void setSurface(android.view.Surface); } + public static abstract class VirtualDisplay.Callbacks { + ctor public VirtualDisplay.Callbacks(); + method public void onDisplayPaused(); + method public void onDisplayResumed(); + method public void onDisplayStopped(); + } + } package android.hardware.input { @@ -16008,6 +16018,28 @@ package android.media.effect { } +package android.media.projection { + + public final class MediaProjection { + method public void addCallback(android.media.projection.MediaProjection.Callback, android.os.Handler); + method public android.media.AudioRecord createAudioRecord(int, int, int, int); + method public android.hardware.display.VirtualDisplay createVirtualDisplay(java.lang.String, int, int, int, boolean, android.view.Surface, android.hardware.display.VirtualDisplay.Callbacks, android.os.Handler); + method public void removeCallback(android.media.projection.MediaProjection.Callback); + method public void stop(); + } + + public static abstract class MediaProjection.Callback { + ctor public MediaProjection.Callback(); + method public void onStop(); + } + + public final class MediaProjectionManager { + method public android.media.projection.MediaProjection getMediaProjection(int, android.content.Intent); + method public android.content.Intent getScreenCaptureIntent(); + } + +} + package android.media.routing { public final class MediaRouteSelector implements android.os.Parcelable { diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 739b81c1c001e..c91c90c7f6c1e 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -192,8 +192,10 @@ public class AppOpsManager { public static final int OP_MUTE_MICROPHONE = 44; /** @hide */ public static final int OP_TOAST_WINDOW = 45; + /** @hide Capture the device's display contents and/or audio */ + public static final int OP_PROJECT_MEDIA = 46; /** @hide */ - public static final int _NUM_OP = 46; + public static final int _NUM_OP = 47; /** Access to coarse location information. */ public static final String OPSTR_COARSE_LOCATION = @@ -263,6 +265,7 @@ public class AppOpsManager { OP_GET_USAGE_STATS, OP_MUTE_MICROPHONE, OP_TOAST_WINDOW, + OP_PROJECT_MEDIA, }; /** @@ -316,6 +319,7 @@ public class AppOpsManager { null, null, null, + null, }; /** @@ -367,8 +371,9 @@ public class AppOpsManager { "MONITOR_LOCATION", "MONITOR_HIGH_POWER_LOCATION", "GET_USAGE_STATS", - "OP_MUTE_MICROPHONE", + "MUTE_MICROPHONE", "TOAST_WINDOW", + "PROJECT_MEDIA", }; /** @@ -422,6 +427,7 @@ public class AppOpsManager { android.Manifest.permission.PACKAGE_USAGE_STATS, null, // no permission for muting/unmuting microphone null, // no permission for displaying toasts + null, // no permission for projecting media }; /** @@ -476,6 +482,7 @@ public class AppOpsManager { null, //GET_USAGE_STATS UserManager.DISALLOW_UNMUTE_MICROPHONE, // MUTE_MICROPHONE UserManager.DISALLOW_CREATE_WINDOWS, // TOAST_WINDOW + null, //PROJECT_MEDIA }; /** @@ -527,8 +534,9 @@ public class AppOpsManager { false, //MONITOR_LOCATION false, //MONITOR_HIGH_POWER_LOCATION false, //GET_USAGE_STATS - false, // MUTE_MICROPHONE - true, // TOAST_WINDOW + false, //MUTE_MICROPHONE + true, //TOAST_WINDOW + false, //PROJECT_MEDIA }; /** @@ -581,6 +589,7 @@ public class AppOpsManager { AppOpsManager.MODE_IGNORED, // OP_GET_USAGE_STATS AppOpsManager.MODE_ALLOWED, AppOpsManager.MODE_ALLOWED, + AppOpsManager.MODE_IGNORED, // OP_PROJECT_MEDIA }; /** @@ -637,6 +646,7 @@ public class AppOpsManager { false, false, false, + false, }; private static HashMap sOpStrToOp = new HashMap(); diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index cbfde14fa3616..ee6cdb743a856 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -72,6 +72,7 @@ import android.location.ILocationManager; import android.location.LocationManager; import android.media.AudioManager; import android.media.MediaRouter; +import android.media.projection.MediaProjectionManager; import android.media.session.MediaSessionManager; import android.media.tv.ITvInputManager; import android.media.tv.TvInputManager; @@ -752,6 +753,12 @@ class ContextImpl extends Context { return new PersistentDataBlockManager( IPersistentDataBlockService.Stub.asInterface(b)); }}); + + registerService(MEDIA_PROJECTION_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + return new MediaProjectionManager(ctx); + } + }); } static ContextImpl getImpl(Context context) { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 1dd018f3d1df2..6c7197ed35de8 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2858,6 +2858,15 @@ public abstract class Context { */ public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; + /** + * Use with {@link #getSystemService} to retrieve a {@link + * android.media.projection.MediaProjectionManager} instance for managing + * media projection sessions. + * @see #getSystemService + * @see android.media.projection.ProjectionManager + */ + public static final String MEDIA_PROJECTION_SERVICE = "media_projection"; + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 79673b32d1993..ce7a2a4221765 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -16,7 +16,10 @@ package android.hardware.display; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; +import android.media.projection.MediaProjection; import android.os.Handler; import android.util.SparseArray; import android.view.Display; @@ -188,6 +191,22 @@ public final class DisplayManager { */ public static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = 1 << 3; + + /** + * Virtual display flag: Indicates that the display is being created for + * the purpose of screen sharing. This implies + * VIRTUAL_DISPLAY_FLAG_PRIVATE. Other flags are not allowed (especially + * not VIRTUAL_DISPLAY_FLAG_PUBLIC or PRESENTATION). + * + * Requires screen share permission for use. + * + * While a display of this type exists, the system will show some sort of + * notification to the user indicating that the screen is being shared. + * + * @see #createVirtualDisplay + */ + public static final int VIRTUAL_DISPLAY_FLAG_SCREEN_SHARE = 1 << 4; + /** @hide */ public DisplayManager(Context context) { mContext = context; @@ -303,7 +322,7 @@ public final class DisplayManager { } /** - * Unregisters an input device listener. + * Unregisters a display listener. * * @param listener The listener to unregister. * @@ -423,6 +442,16 @@ public final class DisplayManager { return mGlobal.getWifiDisplayStatus(); } + /** + * Creates a virtual display. + * + * @see #createVirtualDisplay(String, int, int, int, Surface, int, VirtualDisplay.Callbacks) + */ + public VirtualDisplay createVirtualDisplay(@NonNull String name, + int width, int height, int densityDpi, @Nullable Surface surface, int flags) { + return createVirtualDisplay(name, width, height, densityDpi, surface, flags, null, null); + } + /** * Creates a virtual display. *

@@ -455,17 +484,28 @@ public final class DisplayManager { * be rendered, or null if there is none initially. * @param flags A combination of virtual display flags: * {@link #VIRTUAL_DISPLAY_FLAG_PUBLIC}, {@link #VIRTUAL_DISPLAY_FLAG_PRESENTATION}, - * {@link #VIRTUAL_DISPLAY_FLAG_SECURE}, or {@link #VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY}. + * {@link #VIRTUAL_DISPLAY_FLAG_SECURE}, {@link #VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY}, + * or {@link #VIRTUAL_DISPLAY_FLAG_SCREEN_SHARE}. + * @param callbacks Callbacks to call when the state of the {@link VirtualDisplay} changes * @return The newly created virtual display, or null if the application could * not create the virtual display. * * @throws SecurityException if the caller does not have permission to create * a virtual display with the specified flags. */ - public VirtualDisplay createVirtualDisplay(String name, - int width, int height, int densityDpi, Surface surface, int flags) { - return mGlobal.createVirtualDisplay(mContext, - name, width, height, densityDpi, surface, flags); + public VirtualDisplay createVirtualDisplay(@NonNull String name, + int width, int height, int densityDpi, @Nullable Surface surface, int flags, + @Nullable VirtualDisplay.Callbacks callbacks, @Nullable Handler handler) { + return createVirtualDisplay(null, + name, width, height, densityDpi, surface, flags, callbacks, handler); + } + + /** @hide */ + public VirtualDisplay createVirtualDisplay(@Nullable MediaProjection projection, + @NonNull String name, int width, int height, int densityDpi, @Nullable Surface surface, + int flags, @Nullable VirtualDisplay.Callbacks callbacks, @Nullable Handler handler) { + return mGlobal.createVirtualDisplay(mContext, projection, + name, width, height, densityDpi, surface, flags, callbacks, handler); } /** diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index a8d55e83a24a3..f2426e51fb833 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -18,6 +18,8 @@ package android.hardware.display; import android.content.Context; import android.hardware.display.DisplayManager.DisplayListener; +import android.media.projection.MediaProjection; +import android.media.projection.IMediaProjection; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -368,8 +370,9 @@ public final class DisplayManagerGlobal { } } - public VirtualDisplay createVirtualDisplay(Context context, String name, - int width, int height, int densityDpi, Surface surface, int flags) { + public VirtualDisplay createVirtualDisplay(Context context, MediaProjection projection, + String name, int width, int height, int densityDpi, Surface surface, int flags, + VirtualDisplay.Callbacks callbacks, Handler handler) { if (TextUtils.isEmpty(name)) { throw new IllegalArgumentException("name must be non-null and non-empty"); } @@ -378,11 +381,12 @@ public final class DisplayManagerGlobal { + "greater than 0"); } - Binder token = new Binder(); + VirtualDisplayCallbacks callbackWrapper = new VirtualDisplayCallbacks(callbacks, handler); + IMediaProjection projectionToken = projection != null ? projection.getProjection() : null; int displayId; try { - displayId = mDm.createVirtualDisplay(token, context.getPackageName(), - name, width, height, densityDpi, surface, flags); + displayId = mDm.createVirtualDisplay(callbackWrapper, projectionToken, + context.getPackageName(), name, width, height, densityDpi, surface, flags); } catch (RemoteException ex) { Log.e(TAG, "Could not create virtual display: " + name, ex); return null; @@ -396,15 +400,15 @@ public final class DisplayManagerGlobal { Log.wtf(TAG, "Could not obtain display info for newly created " + "virtual display: " + name); try { - mDm.releaseVirtualDisplay(token); + mDm.releaseVirtualDisplay(callbackWrapper); } catch (RemoteException ex) { } return null; } - return new VirtualDisplay(this, display, token, surface); + return new VirtualDisplay(this, display, callbackWrapper, surface); } - public void setVirtualDisplaySurface(IBinder token, Surface surface) { + public void setVirtualDisplaySurface(IVirtualDisplayCallbacks token, Surface surface) { try { mDm.setVirtualDisplaySurface(token, surface); } catch (RemoteException ex) { @@ -412,7 +416,7 @@ public final class DisplayManagerGlobal { } } - public void releaseVirtualDisplay(IBinder token) { + public void releaseVirtualDisplay(IVirtualDisplayCallbacks token) { try { mDm.releaseVirtualDisplay(token); } catch (RemoteException ex) { @@ -462,4 +466,59 @@ public final class DisplayManagerGlobal { } } } + + private final static class VirtualDisplayCallbacks extends IVirtualDisplayCallbacks.Stub { + private VirtualDisplayCallbacksDelegate mDelegate; + + public VirtualDisplayCallbacks(VirtualDisplay.Callbacks callbacks, Handler handler) { + mDelegate = new VirtualDisplayCallbacksDelegate(callbacks, handler); + } + + @Override // Binder call + public void onDisplayPaused() { + mDelegate.sendEmptyMessage(VirtualDisplayCallbacksDelegate.MSG_DISPLAY_PAUSED); + } + + @Override // Binder call + public void onDisplayResumed() { + mDelegate.sendEmptyMessage(VirtualDisplayCallbacksDelegate.MSG_DISPLAY_RESUMED); + } + + @Override // Binder call + public void onDisplayStopped() { + mDelegate.sendEmptyMessage(VirtualDisplayCallbacksDelegate.MSG_DISPLAY_STOPPED); + } + } + + private final static class VirtualDisplayCallbacksDelegate extends Handler { + public static final int MSG_DISPLAY_PAUSED = 0; + public static final int MSG_DISPLAY_RESUMED = 1; + public static final int MSG_DISPLAY_STOPPED = 2; + + private final VirtualDisplay.Callbacks mCallbacks; + + public VirtualDisplayCallbacksDelegate(VirtualDisplay.Callbacks callbacks, + Handler handler) { + super(handler != null ? handler.getLooper() : Looper.myLooper(), null, true /*async*/); + mCallbacks = callbacks; + } + + @Override + public void handleMessage(Message msg) { + if (mCallbacks == null) { + return; + } + switch (msg.what) { + case MSG_DISPLAY_PAUSED: + mCallbacks.onDisplayPaused(); + break; + case MSG_DISPLAY_RESUMED: + mCallbacks.onDisplayResumed(); + break; + case MSG_DISPLAY_STOPPED: + mCallbacks.onDisplayStopped(); + break; + } + } + } } diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl index 23c58c84dadba..44ffbc4467d81 100644 --- a/core/java/android/hardware/display/IDisplayManager.aidl +++ b/core/java/android/hardware/display/IDisplayManager.aidl @@ -17,8 +17,10 @@ package android.hardware.display; import android.hardware.display.IDisplayManagerCallback; +import android.hardware.display.IVirtualDisplayCallbacks; import android.hardware.display.WifiDisplay; import android.hardware.display.WifiDisplayStatus; +import android.media.projection.IMediaProjection; import android.view.DisplayInfo; import android.view.Surface; @@ -57,14 +59,15 @@ interface IDisplayManager { // No permissions required. WifiDisplayStatus getWifiDisplayStatus(); - // Requires CAPTURE_VIDEO_OUTPUT or CAPTURE_SECURE_VIDEO_OUTPUT for certain - // combinations of flags. - int createVirtualDisplay(IBinder token, String packageName, - String name, int width, int height, int densityDpi, in Surface surface, int flags); + // Requires CAPTURE_VIDEO_OUTPUT, CAPTURE_SECURE_VIDEO_OUTPUT, or an appropriate + // MediaProjection token for certain combinations of flags. + int createVirtualDisplay(in IVirtualDisplayCallbacks callbacks, + in IMediaProjection projectionToken, String packageName, String name, + int width, int height, int densityDpi, in Surface surface, int flags); // No permissions required but must be same Uid as the creator. - void setVirtualDisplaySurface(in IBinder token, in Surface surface); + void setVirtualDisplaySurface(in IVirtualDisplayCallbacks token, in Surface surface); // No permissions required but must be same Uid as the creator. - void releaseVirtualDisplay(in IBinder token); + void releaseVirtualDisplay(in IVirtualDisplayCallbacks token); } diff --git a/core/java/android/hardware/display/IVirtualDisplayCallbacks.aidl b/core/java/android/hardware/display/IVirtualDisplayCallbacks.aidl new file mode 100644 index 0000000000000..a1cdc01123ffe --- /dev/null +++ b/core/java/android/hardware/display/IVirtualDisplayCallbacks.aidl @@ -0,0 +1,41 @@ +/* + * 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.hardware.display; + +/** @hide */ +oneway interface IVirtualDisplayCallbacks { + /** + * Called when the virtual display video projection has been + * paused by the system or when the surface has been detached + * by the application by calling setSurface(null). + * The surface will not receive any more buffers while paused. + */ + void onDisplayPaused(); + + /** + * Called when the virtual display video projection has been + * resumed after having been paused. + */ + void onDisplayResumed(); + + /** + * Called when the virtual display video projection has been + * stopped by the system. It will no longer receive frames + * and it will never be resumed. It is still the responsibility + * of the application to release() the virtual display. + */ + void onDisplayStopped(); +} diff --git a/core/java/android/hardware/display/VirtualDisplay.java b/core/java/android/hardware/display/VirtualDisplay.java index 691d6a076ae03..df6116b6c5780 100644 --- a/core/java/android/hardware/display/VirtualDisplay.java +++ b/core/java/android/hardware/display/VirtualDisplay.java @@ -35,11 +35,11 @@ import android.view.Surface; public final class VirtualDisplay { private final DisplayManagerGlobal mGlobal; private final Display mDisplay; - private IBinder mToken; + private IVirtualDisplayCallbacks mToken; private Surface mSurface; - VirtualDisplay(DisplayManagerGlobal global, Display display, IBinder token, - Surface surface) { + VirtualDisplay(DisplayManagerGlobal global, Display display, + IVirtualDisplayCallbacks token, Surface surface) { mGlobal = global; mDisplay = display; mToken = token; @@ -98,4 +98,31 @@ public final class VirtualDisplay { return "VirtualDisplay{display=" + mDisplay + ", token=" + mToken + ", surface=" + mSurface + "}"; } + + /** + * Interface for receiving information about a {@link VirtualDisplay}'s state changes. + */ + public static abstract class Callbacks { + /** + * Called when the virtual display video projection has been + * paused by the system or when the surface has been detached + * by the application by calling setSurface(null). + * The surface will not receive any more buffers while paused. + */ + public void onDisplayPaused() { } + + /** + * Called when the virtual display video projection has been + * resumed after having been paused. + */ + public void onDisplayResumed() { } + + /** + * Called when the virtual display video projection has been + * stopped by the system. It will no longer receive frames + * and it will never be resumed. It is still the responsibility + * of the application to release() the virtual display. + */ + public void onDisplayStopped() { } + } } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c9fd6c4e56fe7..2b313b3af1966 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2771,6 +2771,13 @@ android:description="@string/permdesc_accessDrmCertificates" android:protectionLevel="signature|system" /> + + + diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 24926893df225..f133707833e2e 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3861,6 +3861,11 @@ Allows an application to interact with the recovery system and system updates. + + Create media projection sessions + + Allows an application to create media projection sessions. These sessions can provide applications the ability to capture display and audio contents. Should never be needed by normal apps. + Touch twice for zoom control diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl new file mode 100644 index 0000000000000..dd84ad324ed05 --- /dev/null +++ b/media/java/android/media/projection/IMediaProjection.aidl @@ -0,0 +1,31 @@ +/* + * 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.media.projection.IMediaProjectionCallback; + +/** {@hide} */ +interface IMediaProjection { + void start(IMediaProjectionCallback callback); + void stop(); + boolean canProjectAudio(); + boolean canProjectVideo(); + boolean canProjectSecureVideo(); + int getVirtualDisplayFlags(); + void addCallback(IMediaProjectionCallback callback); + void removeCallback(IMediaProjectionCallback callback); +} diff --git a/media/java/android/media/projection/IMediaProjectionCallback.aidl b/media/java/android/media/projection/IMediaProjectionCallback.aidl new file mode 100644 index 0000000000000..f3743d1307e93 --- /dev/null +++ b/media/java/android/media/projection/IMediaProjectionCallback.aidl @@ -0,0 +1,22 @@ +/* + * 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; + +/** {@hide} */ +oneway interface IMediaProjectionCallback { + void onStop(); +} diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl new file mode 100644 index 0000000000000..6ed803afd58b4 --- /dev/null +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -0,0 +1,28 @@ +/* + * 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.media.projection.IMediaProjection; +import android.os.IBinder; + +/** {@hide} */ +interface IMediaProjectionManager { + boolean hasProjectionPermission(int uid, String packageName); + IMediaProjection createProjection(int uid, String packageName, int type, + boolean permanentGrant); + boolean isValidMediaProjection(IMediaProjection projection); +} diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java new file mode 100644 index 0000000000000..348a577dc5aed --- /dev/null +++ b/media/java/android/media/projection/MediaProjection.java @@ -0,0 +1,198 @@ +/* + * 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.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.AudioRecord; +import android.media.projection.IMediaProjection; +import android.media.projection.IMediaProjectionCallback; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; +import android.view.Surface; + +import java.util.Map; + +/** + * A token granting applications the ability to capture screen contents and/or + * record system audio. The exact capabilities granted depend on the type of + * MediaProjection. + * + *

+ * A screen capture session can be started through {@link + * MediaProjectionManager#getScreenCaptureIntent}. This grants the ability to + * capture screen contents, but not system audio. + *

+ */ +public final class MediaProjection { + private static final String TAG = "MediaProjection"; + + private final IMediaProjection mImpl; + private final Context mContext; + private final Map mCallbacks; + + /** @hide */ + public MediaProjection(Context context, IMediaProjection impl) { + mCallbacks = new ArrayMap(); + mContext = context; + mImpl = impl; + try { + mImpl.start(new MediaProjectionCallback()); + } catch (RemoteException e) { + throw new RuntimeException("Failed to start media projection", e); + } + } + + /** Register a listener to receive notifications about when the {@link + * MediaProjection} changes state. + * + * @param callback The callback to call. + * @param handler The handler on which the callback should be invoked, or + * null if the callback should be invoked on the calling thread's looper. + * + * @see #removeCallback + */ + public void addCallback(Callback callback, Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback should not be null"); + } + mCallbacks.put(callback, new CallbackRecord(callback, handler)); + } + + /** Unregister a MediaProjection listener. + * + * @param callback The callback to unregister. + * + * @see #addCallback + */ + public void removeCallback(Callback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback should not be null"); + } + mCallbacks.remove(callback); + } + + /** + * Creates a {@link android.hardware.display.VirtualDisplay} to capture the + * contents of the screen. + * + * @param name The name of the virtual display, must be non-empty. + * @param width The width of the virtual display in pixels. Must be + * greater than 0. + * @param height The height of the virtual display in pixels. Must be + * greater than 0. + * @param dpi The density of the virtual display in dpi. Must be greater + * than 0. + * @param surface The surface to which the content of the virtual display + * should be rendered, or null if there is none initially. + * @param isSecure Whether the display should be considered a secure + * display. This typically requires special permissions not available to + * third party applications. + * @param callbacks Callbacks to call when the virtual display's state + * changes, or null if none. + * @param handler The {@link android.os.Handler} on which the callback should be + * invoked, or null if the callback should be invoked on the calling + * thread's main {@link android.os.Looper}. + * + * @see android.hardware.display.DisplayManager#createVirtualDisplay( + * String, int, int, int, int, Surface, VirtualDisplay.Callbacks, Handler) + */ + public VirtualDisplay createVirtualDisplay(@NonNull String name, + int width, int height, int dpi, boolean isSecure, @Nullable Surface surface, + @Nullable VirtualDisplay.Callbacks callbacks, @Nullable Handler handler) { + DisplayManager dm = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE); + int flags = isSecure ? DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE : 0; + return dm.createVirtualDisplay( + this, name, width, height, dpi, surface, flags, callbacks, handler); + } + + /** + * Creates an AudioRecord to capture audio played back by the system. + */ + public AudioRecord createAudioRecord( + int sampleRateInHz, int channelConfig, + int audioFormat, int bufferSizeInBytes) { + return null; + } + + /** + * Stops projection. + */ + public void stop() { + try { + mImpl.stop(); + } catch (RemoteException e) { + Log.e(TAG, "Unable to stop projection", e); + } + } + + /** + * Get the underlying IMediaProjection. + * @hide + */ + public IMediaProjection getProjection() { + return mImpl; + } + + /** + * Callbacks for the projection session. + */ + public static abstract class Callback { + /** + * Called when the MediaProjection session is no longer valid. + * + * Once a MediaProjection has been stopped, it's up to the application to release any + * resources it may be holding (e.g. {@link android.hardware.display.VirtualDisplay}s). + */ + public void onStop() { } + } + + private final class MediaProjectionCallback extends IMediaProjectionCallback.Stub { + @Override + public void onStop() { + final int N = mCallbacks.size(); + for (int i = 0; i < N; i++) { + mCallbacks.get(i).onStop(); + } + } + } + + private final static class CallbackRecord { + private Callback mCallback; + private Handler mHandler; + + public CallbackRecord(Callback callback, Handler handler) { + mCallback = callback; + mHandler = handler; + } + + public void onStop() { + mHandler.post(new Runnable() { + @Override + public void run() { + mCallback.onStop(); + } + }); + } + } +} diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java new file mode 100644 index 0000000000000..aac8cf9639206 --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -0,0 +1,91 @@ +/* + * 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.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.media.projection.IMediaProjection; +import android.os.Binder; +import android.os.IBinder; + +/** + * Manages the retrieval of certain types of {@link MediaProjection} tokens. + * + *

+ * Get an instance of this class by calling {@link + * android.content.Context#getSystemService(java.lang.String) + * Context.getSystemService()} with the argument {@link + * android.content.Context#MEDIA_PROJECTION_SERVICE}. + *

+ */ +public final class MediaProjectionManager { + /** @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; + + /** @hide */ + public MediaProjectionManager(Context context) { + mContext = context; + } + + /** + * Returns an Intent that must passed to startActivityForResult() + * in order to start screen capture. The activity will prompt + * the user whether to allow screen capture. The result of this + * activity should be passed to getMediaProjection. + */ + public Intent getScreenCaptureIntent() { + Intent i = new Intent(); + i.setClassName("com.android.systemui", + "com.android.systemui.media.MediaProjectionPermissionActivity"); + return i; + } + + /** + * Retrieve the MediaProjection obtained from a succesful screen + * capture request. Will be null if the result from the + * startActivityForResult() is anything other than RESULT_OK. + * + * @param resultCode The result code from {@link android.app.Activity#onActivityResult(int, + * int, android.content.Intent)} + * @param resultData The resulting data from {@link android.app.Activity#onActivityResult(int, + * int, android.content.Intent)} + */ + 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; + } + return new MediaProjection(mContext, IMediaProjection.Stub.asInterface(projection)); + } +} diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 5752646ef581a..a0388990b018e 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -109,6 +109,9 @@ + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index f8c9d4cad6a9b..46057af634696 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -703,5 +703,15 @@ Contents hidden + Exit guest + + + %s will start capturing everything that\'s displayed on your screen. + + + Don\'t show again + + + Start now diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java new file mode 100644 index 0000000000000..b441eaada7db0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java @@ -0,0 +1,138 @@ +/* + * 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 com.android.systemui.media; + +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.media.projection.MediaProjectionManager; +import android.media.projection.IMediaProjectionManager; +import android.media.projection.IMediaProjection; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; +import android.view.LayoutInflater; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.TextView; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; +import com.android.systemui.R; + +public class MediaProjectionPermissionActivity extends AlertActivity + implements DialogInterface.OnClickListener, CheckBox.OnCheckedChangeListener { + private static final String TAG = "MediaProjectionPermissionActivity"; + + private boolean mPermanentGrant; + private String mPackageName; + private int mUid; + private IMediaProjectionManager mService; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + Intent intent = getIntent(); + mPackageName = getCallingPackage(); + IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); + mService = IMediaProjectionManager.Stub.asInterface(b); + + if (mPackageName == null) { + finish(); + return; + } + + PackageManager packageManager = getPackageManager(); + ApplicationInfo aInfo; + try { + aInfo = packageManager.getApplicationInfo(mPackageName, 0); + mUid = aInfo.uid; + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "unable to look up package name", e); + finish(); + return; + } + + try { + if (mService.hasProjectionPermission(mUid, mPackageName)) { + setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName, + false /*permanentGrant*/)); + finish(); + return; + } + } catch (RemoteException e) { + Log.e(TAG, "Error checking projection permissions", e); + finish(); + return; + } + + String appName = aInfo.loadLabel(packageManager).toString(); + + final AlertController.AlertParams ap = mAlertParams; + ap.mIcon = aInfo.loadIcon(packageManager); + ap.mMessage = getString(R.string.media_projection_dialog_text, appName); + ap.mPositiveButtonText = getString(R.string.media_projection_action_text); + ap.mNegativeButtonText = getString(android.R.string.cancel); + ap.mPositiveButtonListener = this; + ap.mNegativeButtonListener = this; + + // add "always use" checkbox + LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); + ap.mView = inflater.inflate(R.layout.remember_permission_checkbox, null); + CheckBox rememberPermissionCheckbox = + (CheckBox)ap.mView.findViewById(R.id.remember); + rememberPermissionCheckbox.setOnCheckedChangeListener(this); + + setupAlert(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + try { + if (which == AlertDialog.BUTTON_POSITIVE) { + setResult(RESULT_OK, getMediaProjectionIntent( + mUid, mPackageName, mPermanentGrant)); + } + } catch (RemoteException e) { + Log.e(TAG, "Error granting projection permission", e); + setResult(RESULT_CANCELED); + } finally { + finish(); + } + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + mPermanentGrant = isChecked; + } + + private Intent getMediaProjectionIntent(int uid, String packageName, boolean permanentGrant) + throws RemoteException { + IMediaProjection projection = mService.createProjection(uid, packageName, + MediaProjectionManager.TYPE_SCREEN_CAPTURE, permanentGrant); + Intent intent = new Intent(); + intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder()); + return intent; + } +} diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java index 4cfd042a1fa1c..9d1f8c68e9b5e 100644 --- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java @@ -3757,9 +3757,10 @@ public final class ActivityStackSupervisor implements DisplayListener { VirtualActivityDisplay(int width, int height, int density) { DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance(); - mVirtualDisplay = dm.createVirtualDisplay(mService.mContext, VIRTUAL_DISPLAY_BASE_NAME, - width, height, density, null, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | - DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY); + mVirtualDisplay = dm.createVirtualDisplay(mService.mContext, null, + VIRTUAL_DISPLAY_BASE_NAME, width, height, density, null, + DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | + DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY, null, null); init(mVirtualDisplay.getDisplay()); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 6697b60891e68..2737646625eed 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -29,8 +29,11 @@ import android.hardware.display.DisplayViewport; import android.hardware.display.DisplayManagerInternal.DisplayTransactionListener; import android.hardware.display.IDisplayManager; import android.hardware.display.IDisplayManagerCallback; +import android.hardware.display.IVirtualDisplayCallbacks; import android.hardware.display.WifiDisplayStatus; import android.hardware.input.InputManagerInternal; +import android.media.projection.IMediaProjection; +import android.media.projection.IMediaProjectionManager; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -39,6 +42,7 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; import android.text.TextUtils; @@ -127,6 +131,7 @@ public final class DisplayManagerService extends SystemService { private final DisplayAdapterListener mDisplayAdapterListener; private WindowManagerInternal mWindowManagerInternal; private InputManagerInternal mInputManagerInternal; + private IMediaProjectionManager mProjectionService; // The synchronization root for the display manager. // This lock guards most of the display manager's state. @@ -486,7 +491,8 @@ public final class DisplayManagerService extends SystemService { } } - private int createVirtualDisplayInternal(IBinder appToken, int callingUid, String packageName, + private int createVirtualDisplayInternal(IVirtualDisplayCallbacks callbacks, + IMediaProjection projection, int callingUid, String packageName, String name, int width, int height, int densityDpi, Surface surface, int flags) { synchronized (mSyncRoot) { if (mVirtualDisplayAdapter == null) { @@ -496,8 +502,8 @@ public final class DisplayManagerService extends SystemService { } DisplayDevice device = mVirtualDisplayAdapter.createVirtualDisplayLocked( - appToken, callingUid, packageName, name, width, height, densityDpi, - surface, flags); + callbacks, projection, callingUid, packageName, + name, width, height, densityDpi, surface, flags); if (device == null) { return -1; } @@ -511,7 +517,7 @@ public final class DisplayManagerService extends SystemService { // Something weird happened and the logical display was not created. Slog.w(TAG, "Rejecting request to create virtual display " + "because the logical display was not created."); - mVirtualDisplayAdapter.releaseVirtualDisplayLocked(appToken); + mVirtualDisplayAdapter.releaseVirtualDisplayLocked(callbacks.asBinder()); handleDisplayDeviceRemovedLocked(device); } return -1; @@ -878,6 +884,14 @@ public final class DisplayManagerService extends SystemService { mTempCallbacks.clear(); } + private IMediaProjectionManager getProjectionService() { + if (mProjectionService == null) { + IBinder b = ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE); + mProjectionService = IMediaProjectionManager.Stub.asInterface(b); + } + return mProjectionService; + } + private void dumpInternal(PrintWriter pw) { pw.println("DISPLAY MANAGER (dumpsys display)"); @@ -1215,13 +1229,14 @@ public final class DisplayManagerService extends SystemService { } @Override // Binder call - public int createVirtualDisplay(IBinder appToken, String packageName, - String name, int width, int height, int densityDpi, Surface surface, int flags) { + public int createVirtualDisplay(IVirtualDisplayCallbacks callbacks, + IMediaProjection projection, String packageName, String name, + int width, int height, int densityDpi, Surface surface, int flags) { final int callingUid = Binder.getCallingUid(); if (!validatePackageName(callingUid, packageName)) { throw new SecurityException("packageName must match the calling uid"); } - if (appToken == null) { + if (callbacks == null) { throw new IllegalArgumentException("appToken must not be null"); } if (TextUtils.isEmpty(name)) { @@ -1231,51 +1246,78 @@ public final class DisplayManagerService extends SystemService { throw new IllegalArgumentException("width, height, and densityDpi must be " + "greater than 0"); } + + if (projection != null) { + try { + if (!getProjectionService().isValidMediaProjection(projection)) { + throw new SecurityException("Invalid media projection"); + } + } catch (RemoteException e) { + throw new SecurityException("unable to validate media projection"); + } + flags &= DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE; + try { + flags |= projection.getVirtualDisplayFlags(); + } catch (RemoteException e) { + throw new RuntimeException("unable to retrieve media projection flags"); + } + } + + if ((flags & DisplayManager.VIRTUAL_DISPLAY_FLAG_SCREEN_SHARE) != 0) { + if ((flags & DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC) != 0 || + (flags & DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION) != 0) { + throw new IllegalArgumentException("screen sharing virtual displays must not " + + "be public or presentation displays"); + } + if (!canProjectVideo(projection)) { + throw new SecurityException("Requires CAPTURE_VIDEO_OUTPUT or " + + "CAPTURE_SECURE_VIDEO_OUTPUT permission, or an appropriate " + + "MediaProjection token in order to create a screen sharing virtual " + + "display."); + } + } + + if (callingUid != Process.SYSTEM_UID && (flags & DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC) != 0) { - if (mContext.checkCallingPermission(android.Manifest.permission.CAPTURE_VIDEO_OUTPUT) - != PackageManager.PERMISSION_GRANTED - && mContext.checkCallingPermission( - android.Manifest.permission.CAPTURE_SECURE_VIDEO_OUTPUT) - != PackageManager.PERMISSION_GRANTED) { + if (!canProjectVideo(projection)) { throw new SecurityException("Requires CAPTURE_VIDEO_OUTPUT or " - + "CAPTURE_SECURE_VIDEO_OUTPUT permission to create a " - + "public virtual display."); + + "CAPTURE_SECURE_VIDEO_OUTPUT permission, or an appropriate " + + "MediaProjection token to create a public virtual display."); } } if ((flags & DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE) != 0) { - if (mContext.checkCallingPermission( - android.Manifest.permission.CAPTURE_SECURE_VIDEO_OUTPUT) - != PackageManager.PERMISSION_GRANTED) { + if (!canProjectSecureVideo(projection)) { throw new SecurityException("Requires CAPTURE_SECURE_VIDEO_OUTPUT " - + "to create a secure virtual display."); + + "or an appropriate MediaProjection token to create a " + + "secure virtual display."); } } final long token = Binder.clearCallingIdentity(); try { - return createVirtualDisplayInternal(appToken, callingUid, packageName, - name, width, height, densityDpi, surface, flags); + return createVirtualDisplayInternal(callbacks, projection, callingUid, + packageName, name, width, height, densityDpi, surface, flags); } finally { Binder.restoreCallingIdentity(token); } } @Override // Binder call - public void setVirtualDisplaySurface(IBinder appToken, Surface surface) { + public void setVirtualDisplaySurface(IVirtualDisplayCallbacks callbacks, Surface surface) { final long token = Binder.clearCallingIdentity(); try { - setVirtualDisplaySurfaceInternal(appToken, surface); + setVirtualDisplaySurfaceInternal(callbacks.asBinder(), surface); } finally { Binder.restoreCallingIdentity(token); } } @Override // Binder call - public void releaseVirtualDisplay(IBinder appToken) { + public void releaseVirtualDisplay(IVirtualDisplayCallbacks callbacks) { final long token = Binder.clearCallingIdentity(); try { - releaseVirtualDisplayInternal(appToken); + releaseVirtualDisplayInternal(callbacks.asBinder()); } finally { Binder.restoreCallingIdentity(token); } @@ -1312,6 +1354,39 @@ public final class DisplayManagerService extends SystemService { } return false; } + + private boolean canProjectVideo(IMediaProjection projection) { + if (projection != null) { + try { + if (projection.canProjectVideo()) { + return true; + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to query projection service for permissions", e); + } + } + if (mContext.checkCallingPermission( + android.Manifest.permission.CAPTURE_VIDEO_OUTPUT) + != PackageManager.PERMISSION_GRANTED) { + return true; + } + return canProjectSecureVideo(projection); + } + + private boolean canProjectSecureVideo(IMediaProjection projection) { + if (projection != null) { + try { + if (projection.canProjectSecureVideo()){ + return true; + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to query projection service for permissions", e); + } + } + return mContext.checkCallingPermission( + android.Manifest.permission.CAPTURE_SECURE_VIDEO_OUTPUT) + != PackageManager.PERMISSION_GRANTED; + } } private final class LocalService extends DisplayManagerInternal { diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index ed619d9c60cb3..284780d344730 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -337,4 +337,4 @@ final class LogicalDisplay { pw.println("mBaseDisplayInfo=" + mBaseDisplayInfo); pw.println("mOverrideDisplayInfo=" + mOverrideDisplayInfo); } -} \ No newline at end of file +} diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index ec14cf1e1d10d..10320816df7ff 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -18,9 +18,13 @@ package com.android.server.display; import android.content.Context; import android.hardware.display.DisplayManager; +import android.hardware.display.IVirtualDisplayCallbacks; +import android.media.projection.IMediaProjection; +import android.media.projection.IMediaProjectionCallback; import android.os.Handler; import android.os.IBinder; import android.os.IBinder.DeathRecipient; +import android.os.Message; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Slog; @@ -28,6 +32,8 @@ import android.view.Display; import android.view.Surface; import android.view.SurfaceControl; +import java.io.PrintWriter; + /** * A display adapter that provides virtual displays on behalf of applications. *

@@ -40,30 +46,38 @@ final class VirtualDisplayAdapter extends DisplayAdapter { private final ArrayMap mVirtualDisplayDevices = new ArrayMap(); + private Handler mHandler; // Called with SyncRoot lock held. public VirtualDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context, Handler handler, Listener listener) { super(syncRoot, context, handler, listener, TAG); + mHandler = handler; } - public DisplayDevice createVirtualDisplayLocked(IBinder appToken, - int ownerUid, String ownerPackageName, + public DisplayDevice createVirtualDisplayLocked(IVirtualDisplayCallbacks callbacks, + IMediaProjection projection, int ownerUid, String ownerPackageName, String name, int width, int height, int densityDpi, Surface surface, int flags) { boolean secure = (flags & DisplayManager.VIRTUAL_DISPLAY_FLAG_SECURE) != 0; + IBinder appToken = callbacks.asBinder(); IBinder displayToken = SurfaceControl.createDisplay(name, secure); VirtualDisplayDevice device = new VirtualDisplayDevice(displayToken, appToken, - ownerUid, ownerPackageName, name, width, height, densityDpi, surface, flags); + ownerUid, ownerPackageName, name, width, height, densityDpi, surface, flags, + new Callbacks(callbacks, mHandler)); + + mVirtualDisplayDevices.put(appToken, device); try { + if (projection != null) { + projection.addCallback(new MediaProjectionCallback(appToken)); + } appToken.linkToDeath(device, 0); } catch (RemoteException ex) { + mVirtualDisplayDevices.remove(appToken); device.destroyLocked(); return null; } - mVirtualDisplayDevices.put(appToken, device); - // Return the display device without actually sending the event indicating // that it was added. The caller will handle it. return device; @@ -98,23 +112,35 @@ final class VirtualDisplayAdapter extends DisplayAdapter { } } - private final class VirtualDisplayDevice extends DisplayDevice - implements DeathRecipient { + private void handleMediaProjectionStoppedLocked(IBinder appToken) { + VirtualDisplayDevice device = mVirtualDisplayDevices.remove(appToken); + if (device != null) { + Slog.i(TAG, "Virtual display device released because media projection stopped: " + + device.mName); + device.stopLocked(); + } + } + + private final class VirtualDisplayDevice extends DisplayDevice implements DeathRecipient { private final IBinder mAppToken; private final int mOwnerUid; final String mOwnerPackageName; - private final String mName; + final String mName; private final int mWidth; private final int mHeight; private final int mDensityDpi; private final int mFlags; + private final Callbacks mCallbacks; private Surface mSurface; private DisplayDeviceInfo mInfo; + private int mState; + private boolean mStopped; - public VirtualDisplayDevice(IBinder displayToken, - IBinder appToken, int ownerUid, String ownerPackageName, - String name, int width, int height, int densityDpi, Surface surface, int flags) { + public VirtualDisplayDevice(IBinder displayToken, IBinder appToken, + int ownerUid, String ownerPackageName, + String name, int width, int height, int densityDpi, Surface surface, int flags, + Callbacks callbacks) { super(VirtualDisplayAdapter.this, displayToken); mAppToken = appToken; mOwnerUid = ownerUid; @@ -125,6 +151,8 @@ final class VirtualDisplayAdapter extends DisplayAdapter { mDensityDpi = densityDpi; mSurface = surface; mFlags = flags; + mCallbacks = callbacks; + mState = Display.STATE_UNKNOWN; } @Override @@ -142,6 +170,19 @@ final class VirtualDisplayAdapter extends DisplayAdapter { mSurface = null; } SurfaceControl.destroyDisplay(getDisplayTokenLocked()); + mCallbacks.dispatchDisplayStopped(); + } + + @Override + public void requestDisplayStateLocked(int state) { + if (state != mState) { + mState = state; + if (state == Display.STATE_OFF) { + mCallbacks.dispatchDisplayPaused(); + } else { + mCallbacks.dispatchDisplayResumed(); + } + } } @Override @@ -150,7 +191,7 @@ final class VirtualDisplayAdapter extends DisplayAdapter { } public void setSurfaceLocked(Surface surface) { - if (mSurface != surface) { + if (!mStopped && mSurface != surface) { if ((mSurface != null) != (surface != null)) { sendDisplayDeviceEventLocked(this, DISPLAY_DEVICE_EVENT_CHANGED); } @@ -160,6 +201,20 @@ final class VirtualDisplayAdapter extends DisplayAdapter { } } + public void stopLocked() { + setSurfaceLocked(null); + mStopped = true; + } + + @Override + public void dumpLocked(PrintWriter pw) { + super.dumpLocked(pw); + pw.println("mFlags=" + mFlags); + pw.println("mState=" + Display.stateToString(mState)); + pw.println("mStopped=" + mStopped); + } + + @Override public DisplayDeviceInfo getDisplayDeviceInfoLocked() { if (mInfo == null) { @@ -174,9 +229,11 @@ final class VirtualDisplayAdapter extends DisplayAdapter { mInfo.presentationDeadlineNanos = 1000000000L / (int) mInfo.refreshRate; // 1 frame mInfo.flags = 0; if ((mFlags & DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC) == 0) { - mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE - | DisplayDeviceInfo.FLAG_NEVER_BLANK - | DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + mInfo.flags |= DisplayDeviceInfo.FLAG_PRIVATE; + if ((mFlags & DisplayManager.VIRTUAL_DISPLAY_FLAG_SCREEN_SHARE) == 0) { + mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY + | DisplayDeviceInfo.FLAG_NEVER_BLANK; + } } else if ((mFlags & DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY) != 0) { mInfo.flags |= DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; } @@ -195,4 +252,62 @@ final class VirtualDisplayAdapter extends DisplayAdapter { return mInfo; } } + + private static class Callbacks extends Handler { + private static final int MSG_ON_DISPLAY_PAUSED = 0; + private static final int MSG_ON_DISPLAY_RESUMED = 1; + private static final int MSG_ON_DISPLAY_STOPPED = 2; + + private final IVirtualDisplayCallbacks mCallbacks; + + public Callbacks(IVirtualDisplayCallbacks callbacks, Handler handler) { + super(handler.getLooper()); + mCallbacks = callbacks; + } + + @Override + public void handleMessage(Message msg) { + try { + switch (msg.what) { + case MSG_ON_DISPLAY_PAUSED: + mCallbacks.onDisplayPaused(); + break; + case MSG_ON_DISPLAY_RESUMED: + mCallbacks.onDisplayResumed(); + break; + case MSG_ON_DISPLAY_STOPPED: + mCallbacks.onDisplayStopped(); + break; + } + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify listener of virtual display event.", e); + } + } + + public void dispatchDisplayPaused() { + sendEmptyMessage(MSG_ON_DISPLAY_PAUSED); + } + + public void dispatchDisplayResumed() { + sendEmptyMessage(MSG_ON_DISPLAY_RESUMED); + } + + public void dispatchDisplayStopped() { + sendEmptyMessage(MSG_ON_DISPLAY_STOPPED); + } + } + + private final class MediaProjectionCallback extends IMediaProjectionCallback.Stub { + private IBinder mAppToken; + public MediaProjectionCallback(IBinder appToken) { + mAppToken = appToken; + } + + @Override + public void onStop() { + synchronized (getSyncRoot()) { + handleMediaProjectionStoppedLocked(mAppToken); + } + } + } } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java new file mode 100644 index 0000000000000..7b28699aa5645 --- /dev/null +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -0,0 +1,329 @@ +/* + * 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 com.android.server.media.projection; + +import com.android.server.Watchdog; + +import android.Manifest; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.hardware.display.DisplayManager; +import android.media.projection.IMediaProjectionManager; +import android.media.projection.IMediaProjection; +import android.media.projection.IMediaProjectionCallback; +import android.media.projection.MediaProjectionManager; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.IBinder.DeathRecipient; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Slog; + +import com.android.server.SystemService; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Manages MediaProjection sessions. + * + * The {@link MediaProjectionManagerService} manages the creation and lifetime of MediaProjections, + * as well as the capabilities they grant. Any service using MediaProjection tokens as permission + * grants must validate the token before use by calling {@link + * IMediaProjectionService#isValidMediaProjection}. + */ +public final class MediaProjectionManagerService extends SystemService + implements Watchdog.Monitor { + private static final String TAG = "MediaProjectionManagerService"; + + private final Object mLock = new Object(); // Protects the list of media projections + private final Map mProjectionGrants; + + private final Context mContext; + private final AppOpsManager mAppOps; + + public MediaProjectionManagerService(Context context) { + super(context); + mContext = context; + mProjectionGrants = new ArrayMap(); + mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); + Watchdog.getInstance().addMonitor(this); + } + + @Override + public void onStart() { + publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(), + false /*allowIsolated*/); + } + + @Override + public void monitor() { + synchronized (mLock) { /* check for deadlock */ } + } + + private void dump(final PrintWriter pw) { + pw.println("MEDIA PROJECTION MANAGER (dumpsys media_projection)"); + synchronized (mLock) { + Collection projections = mProjectionGrants.values(); + pw.println("Media Projections: size=" + projections.size()); + for (MediaProjection mp : projections) { + mp.dump(pw, " "); + } + } + } + + private final class BinderService extends IMediaProjectionManager.Stub { + + @Override // Binder call + public boolean hasProjectionPermission(int uid, String packageName) { + long token = Binder.clearCallingIdentity(); + boolean hasPermission = false; + try { + hasPermission |= checkPermission(packageName, + android.Manifest.permission.CAPTURE_VIDEO_OUTPUT) + || mAppOps.checkOpNoThrow( + AppOpsManager.OP_PROJECT_MEDIA, uid, packageName) + == AppOpsManager.MODE_ALLOWED; + } finally { + Binder.restoreCallingIdentity(token); + } + return hasPermission; + } + + @Override // Binder call + public IMediaProjection createProjection(int uid, String packageName, int type, + boolean isPermanentGrant) { + if (mContext.checkCallingPermission(Manifest.permission.CREATE_MEDIA_PROJECTION) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires CREATE_MEDIA_PROJECTION in order to grant " + + "projection permission"); + } + long callingToken = Binder.clearCallingIdentity(); + MediaProjection projection; + try { + projection = new MediaProjection(type, uid, packageName); + if (isPermanentGrant) { + mAppOps.setMode(AppOpsManager.OP_PROJECT_MEDIA, + projection.uid, projection.packageName, AppOpsManager.MODE_ALLOWED); + } + } finally { + Binder.restoreCallingIdentity(callingToken); + } + return projection; + } + + @Override // Binder call + public boolean isValidMediaProjection(IMediaProjection projection) { + return mProjectionGrants.containsKey(projection.asBinder()); + } + + @Override // Binder call + public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { + if (mContext == null + || mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump MediaProjectionManager from from pid=" + + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()); + return; + } + + final long token = Binder.clearCallingIdentity(); + try { + dump(pw); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private boolean checkPermission(String packageName, String permission) { + return mContext.getPackageManager().checkPermission(permission, packageName) + == PackageManager.PERMISSION_GRANTED; + } + } + + private final class MediaProjection extends IMediaProjection.Stub implements DeathRecipient { + public int uid; + public String packageName; + + private IBinder mToken; + private int mType; + private CallbackDelegate mCallbackDelegate; + + public MediaProjection(int type, int uid, String packageName) { + mType = type; + this.uid = uid; + this.packageName = packageName; + mCallbackDelegate = new CallbackDelegate(); + } + + @Override // Binder call + public boolean canProjectVideo() { + return mType == MediaProjectionManager.TYPE_MIRRORING || + mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE; + } + + @Override // Binder call + public boolean canProjectSecureVideo() { + return false; + } + + @Override // Binder call + public boolean canProjectAudio() { + return mType == MediaProjectionManager.TYPE_MIRRORING || + mType == MediaProjectionManager.TYPE_PRESENTATION; + } + + @Override // Binder call + public int getVirtualDisplayFlags() { + switch (mType) { + case MediaProjectionManager.TYPE_SCREEN_CAPTURE: + return DisplayManager.VIRTUAL_DISPLAY_FLAG_SCREEN_SHARE; + case MediaProjectionManager.TYPE_MIRRORING: + return DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC | + DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; + case MediaProjectionManager.TYPE_PRESENTATION: + return DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION | + DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; + } + throw new RuntimeException("Unknown MediaProjection type"); + } + + @Override // Binder call + public void start(IMediaProjectionCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + synchronized (mLock) { + if (mProjectionGrants.containsKey(asBinder())) { + throw new IllegalStateException( + "Cannot start already started MediaProjection"); + } + addCallback(callback); + try { + mToken = callback.asBinder(); + mToken.linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.w(TAG, + "MediaProjectionCallbacks must be valid, aborting MediaProjection", e); + return; + } + mProjectionGrants.put(asBinder(), this); + } + } + + @Override // Binder call + public void stop() { + synchronized (mLock) { + if (!mProjectionGrants.containsKey(asBinder())) { + Slog.w(TAG, "Attempted to stop inactive MediaProjection " + + "(uid=" + Binder.getCallingUid() + ", " + + "pid=" + Binder.getCallingPid() + ")"); + return; + } + mToken.unlinkToDeath(this, 0); + mCallbackDelegate.dispatchStop(); + mProjectionGrants.remove(asBinder()); + } + } + + @Override + public void binderDied() { + stop(); + } + + @Override + public void addCallback(IMediaProjectionCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + mCallbackDelegate.add(callback); + } + + @Override + public void removeCallback(IMediaProjectionCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + mCallbackDelegate.remove(callback); + } + + public void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "(" + packageName + ", uid=" + uid + "): " + typeToString(mType)); + } + } + + private static class CallbackDelegate { + private static final int MSG_ON_STOP = 0; + private List mCallbacks; + private Handler mHandler; + private Object mLock = new Object(); + + public CallbackDelegate() { + mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/); + mCallbacks = new ArrayList(); + } + + public void add(IMediaProjectionCallback callback) { + synchronized (mLock) { + mCallbacks.add(callback); + } + } + + public void remove(IMediaProjectionCallback callback) { + synchronized (mLock) { + mCallbacks.remove(callback); + } + } + + public void dispatchStop() { + synchronized (mLock) { + for (final IMediaProjectionCallback callback : mCallbacks) { + mHandler.post(new Runnable() { + @Override + public void run() { + try { + callback.onStop(); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify media projection has stopped", e); + } + } + }); + } + } + } + } + + private static String typeToString(int type) { + switch (type) { + case MediaProjectionManager.TYPE_SCREEN_CAPTURE: + return "TYPE_SCREEN_CAPTURE"; + case MediaProjectionManager.TYPE_MIRRORING: + return "TYPE_MIRRORING"; + case MediaProjectionManager.TYPE_PRESENTATION: + return "TYPE_PRESENTATION"; + } + return Integer.toString(type); + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index a6030cfc7dcab..65794b36cf8b5 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -72,6 +72,7 @@ import com.android.server.lights.LightsManager; import com.android.server.lights.LightsService; import com.android.server.media.MediaRouterService; import com.android.server.media.MediaSessionService; +import com.android.server.media.projection.MediaProjectionManagerService; import com.android.server.net.NetworkPolicyManagerService; import com.android.server.net.NetworkStatsService; import com.android.server.notification.NotificationManagerService; @@ -949,6 +950,10 @@ public final class SystemServer { mSystemServiceManager.startService(LauncherAppsService.class); } + if (!disableNonCoreServices) { + mSystemServiceManager.startService(MediaProjectionManagerService.class); + } + // Before things start rolling, be sure we have decided whether // we are in safe mode. final boolean safeMode = wm.detectSafeMode();