Add callbacks for MediaProjection start / stop.

Also, enforce that there is only one valid MediaProjection at a time.

Bug: 16488053
Change-Id: Id05445d798c98cb208bc4dab186296392e15d30b
diff --git a/Android.mk b/Android.mk
index b419231..1cdc709 100644
--- a/Android.mk
+++ b/Android.mk
@@ -329,6 +329,7 @@
 	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/projection/IMediaProjectionWatcherCallback.aidl \
 	media/java/android/media/routing/IMediaRouteService.aidl \
 	media/java/android/media/routing/IMediaRouteClientCallback.aidl \
 	media/java/android/media/routing/IMediaRouter.aidl \
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index aee3090..df77789 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2811,11 +2811,11 @@
         android:description="@string/permdesc_accessDrmCertificates"
         android:protectionLevel="signature|system" />
 
-    <!-- Api Allows an application to create media projection sessions.
+    <!-- Api Allows an application to manage media projection sessions.
          @hide This is not a third-party API (intended for system apps). -->
-    <permission android:name="android.permission.CREATE_MEDIA_PROJECTION"
-        android:label="@string/permlab_createMediaProjection"
-        android:description="@string/permdesc_createMediaProjection"
+    <permission android:name="android.permission.MANAGE_MEDIA_PROJECTION"
+        android:label="@string/permlab_manageMediaProjection"
+        android:description="@string/permdesc_manageMediaProjection"
         android:protectionLevel="signature" />
 
     <!-- @SystemApi Allows an application to read install sessions
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6262f13..7f6c4c7 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3898,10 +3898,10 @@
     <!-- Description of an application permission that lets it control keyguard. -->
     <string name="permdesc_recovery">Allows an application to interact with the recovery system and system updates.</string>
 
-    <!-- Title of an application permission that lets it create media projection sessions. -->
-    <string name="permlab_createMediaProjection">Create media projection sessions</string>
-    <!-- Description of an application permission that lets it create media projection sessions. -->
-    <string name="permdesc_createMediaProjection">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.</string>
+    <!-- Title of an application permission that lets it manage media projection sessions. -->
+    <string name="permlab_manageMediaProjection">Manage media projection sessions</string>
+    <!-- Description of an application permission that lets it manage media projection sessions. -->
+    <string name="permdesc_manageMediaProjection">Allows an application to manage media projection sessions. These sessions can provide applications the ability to capture display and audio contents. Should never be needed by normal apps.</string>
 
     <!-- Title of an application permission that lets it read install sessions. -->
     <string name="permlab_readInstallSessions">Read install sessions</string>
diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl
index 6ed803a..7e10c51 100644
--- a/media/java/android/media/projection/IMediaProjectionManager.aidl
+++ b/media/java/android/media/projection/IMediaProjectionManager.aidl
@@ -17,6 +17,9 @@
 package android.media.projection;
 
 import android.media.projection.IMediaProjection;
+import android.media.projection.IMediaProjectionCallback;
+import android.media.projection.IMediaProjectionWatcherCallback;
+import android.media.projection.MediaProjectionInfo;
 import android.os.IBinder;
 
 /** {@hide} */
@@ -25,4 +28,8 @@
     IMediaProjection createProjection(int uid, String packageName, int type,
             boolean permanentGrant);
     boolean isValidMediaProjection(IMediaProjection projection);
+    MediaProjectionInfo getActiveProjectionInfo();
+    void stopActiveProjection();
+    void addCallback(IMediaProjectionWatcherCallback callback);
+    void removeCallback(IMediaProjectionWatcherCallback callback);
 }
diff --git a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl
new file mode 100644
index 0000000..2231ce1
--- /dev/null
+++ b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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.MediaProjectionInfo;
+
+/** {@hide} */
+oneway interface IMediaProjectionWatcherCallback {
+    void onStart(in MediaProjectionInfo info);
+    void onStop(in MediaProjectionInfo info);
+}
diff --git a/media/java/android/media/projection/MediaProjectionInfo.aidl b/media/java/android/media/projection/MediaProjectionInfo.aidl
new file mode 100644
index 0000000..3c8f9b6
--- /dev/null
+++ b/media/java/android/media/projection/MediaProjectionInfo.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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;
+
+parcelable MediaProjectionInfo;
diff --git a/media/java/android/media/projection/MediaProjectionInfo.java b/media/java/android/media/projection/MediaProjectionInfo.java
new file mode 100644
index 0000000..7ebc31f
--- /dev/null
+++ b/media/java/android/media/projection/MediaProjectionInfo.java
@@ -0,0 +1,76 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+
+/** @hide */
+public final class MediaProjectionInfo implements Parcelable {
+    private final String mPackageName;
+    private final UserHandle mUserHandle;
+
+    public MediaProjectionInfo(String packageName, UserHandle handle) {
+        mPackageName = packageName;
+        mUserHandle = handle;
+    }
+
+    public MediaProjectionInfo(Parcel in) {
+        mPackageName = in.readString();
+        mUserHandle = UserHandle.readFromParcel(in);
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    public UserHandle getUserHandle() {
+        return mUserHandle;
+    }
+
+    @Override
+    public String toString() {
+        return "MediaProjectionInfo{mPackageName="
+            + mPackageName + ", mUserHandle="
+            + mUserHandle + "}";
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(mPackageName);
+        UserHandle.writeToParcel(mUserHandle, out);
+    }
+
+    public static final Parcelable.Creator<MediaProjectionInfo> CREATOR =
+            new Parcelable.Creator<MediaProjectionInfo>() {
+        @Override
+        public MediaProjectionInfo createFromParcel(Parcel in) {
+            return new MediaProjectionInfo (in);
+        }
+
+        @Override
+        public MediaProjectionInfo[] newArray(int size) {
+            return new MediaProjectionInfo[size];
+        }
+    };
+}
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index aac8cf9..50d66c6 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -17,12 +17,20 @@
 package android.media.projection;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.media.projection.IMediaProjection;
 import android.os.Binder;
+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 java.util.Map;
 
 /**
  * Manages the retrieval of certain types of {@link MediaProjection} tokens.
@@ -35,6 +43,7 @@
  * </p>
  */
 public final class MediaProjectionManager {
+    private static final String TAG = "MediaProjectionManager";
     /** @hide */
     public static final String EXTRA_APP_TOKEN = "android.media.projection.extra.EXTRA_APP_TOKEN";
     /** @hide */
@@ -49,10 +58,15 @@
     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<>();
     }
 
     /**
@@ -88,4 +102,105 @@
         }
         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 {
+            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) {
+            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) {
+            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);
+    }
+
+    /** @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);
+                }
+            });
+        }
+    }
 }
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index b8836a0..cbea664 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -111,7 +111,7 @@
     <uses-permission android:name="android.permission.CAMERA" />
 
     <!-- Screen Capturing -->
-    <uses-permission android:name="android.permission.CREATE_MEDIA_PROJECTION" />
+    <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" />
 
     <application
         android:name=".SystemUIApplication"
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 289b5aa..69d1dc9 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -26,6 +26,8 @@
 import android.media.projection.IMediaProjectionManager;
 import android.media.projection.IMediaProjection;
 import android.media.projection.IMediaProjectionCallback;
+import android.media.projection.IMediaProjectionWatcherCallback;
+import android.media.projection.MediaProjectionInfo;
 import android.media.projection.MediaProjectionManager;
 import android.os.Binder;
 import android.os.Handler;
@@ -34,6 +36,7 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
+import android.os.UserHandle;
 import android.util.ArrayMap;
 import android.util.Slog;
 
@@ -59,15 +62,20 @@
     private static final String TAG = "MediaProjectionManagerService";
 
     private final Object mLock = new Object(); // Protects the list of media projections
-    private final Map<IBinder, MediaProjection> mProjectionGrants;
+    private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters;
+    private final CallbackDelegate mCallbackDelegate;
 
     private final Context mContext;
     private final AppOpsManager mAppOps;
 
+    private IBinder mProjectionToken;
+    private MediaProjection mProjectionGrant;
+
     public MediaProjectionManagerService(Context context) {
         super(context);
         mContext = context;
-        mProjectionGrants = new ArrayMap<IBinder, MediaProjection>();
+        mDeathEaters = new ArrayMap<IBinder, IBinder.DeathRecipient>();
+        mCallbackDelegate = new CallbackDelegate();
         mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
         Watchdog.getInstance().addMonitor(this);
     }
@@ -83,13 +91,97 @@
         synchronized (mLock) { /* check for deadlock */ }
     }
 
+    private void startProjectionLocked(final MediaProjection projection) {
+        if (mProjectionGrant != null) {
+            mProjectionGrant.stop();
+        }
+        mProjectionToken = projection.asBinder();
+        mProjectionGrant = projection;
+        dispatchStart(projection);
+    }
+
+    private void stopProjectionLocked(final MediaProjection projection) {
+        mProjectionToken = null;
+        mProjectionGrant = null;
+        dispatchStop(projection);
+    }
+
+    private void addCallback(final IMediaProjectionWatcherCallback callback) {
+        IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
+            @Override
+            public void binderDied() {
+                synchronized (mLock) {
+                    unlinkDeathRecipientLocked(callback);
+                    removeCallback(callback);
+                }
+            }
+        };
+        synchronized (mLock) {
+            mCallbackDelegate.add(callback);
+            linkDeathRecipientLocked(callback, deathRecipient);
+        }
+    }
+
+    private void removeCallback(IMediaProjectionWatcherCallback callback) {
+        synchronized (mLock) {
+            unlinkDeathRecipientLocked(callback);
+            removeCallback(callback);
+        }
+    }
+
+    private void linkDeathRecipientLocked(IMediaProjectionWatcherCallback callback,
+            IBinder.DeathRecipient deathRecipient) {
+        try {
+            final IBinder token = callback.asBinder();
+            token.linkToDeath(deathRecipient, 0);
+            mDeathEaters.put(token, deathRecipient);
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Unable to link to death for media projection monitoring callback", e);
+        }
+    }
+
+    private void unlinkDeathRecipientLocked(IMediaProjectionWatcherCallback callback) {
+        final IBinder token = callback.asBinder();
+        IBinder.DeathRecipient deathRecipient = mDeathEaters.remove(token);
+        if (deathRecipient != null) {
+            token.unlinkToDeath(deathRecipient, 0);
+        }
+    }
+
+    private void dispatchStart(MediaProjection projection) {
+        mCallbackDelegate.dispatchStart(projection);
+    }
+
+    private void dispatchStop(MediaProjection projection) {
+        mCallbackDelegate.dispatchStop(projection);
+    }
+
+    private boolean isValidMediaProjection(IBinder token) {
+        synchronized (mLock) {
+            if (mProjectionToken != null) {
+                return mProjectionToken.equals(token);
+            }
+            return false;
+        }
+    }
+
+    private MediaProjectionInfo getActiveProjectionInfo() {
+        synchronized (mLock) {
+            if (mProjectionGrant == null) {
+                return null;
+            }
+            return mProjectionGrant.getProjectionInfo();
+        }
+    }
+
     private void dump(final PrintWriter pw) {
         pw.println("MEDIA PROJECTION MANAGER (dumpsys media_projection)");
         synchronized (mLock) {
-            Collection<MediaProjection> projections = mProjectionGrants.values();
-            pw.println("Media Projections: size=" + projections.size());
-            for (MediaProjection mp : projections) {
-                mp.dump(pw, "  ");
+            pw.println("Media Projection: ");
+            if (mProjectionGrant != null ) {
+                mProjectionGrant.dump(pw);
+            } else {
+                pw.println("null");
             }
         }
     }
@@ -115,11 +207,14 @@
         @Override // Binder call
         public IMediaProjection createProjection(int uid, String packageName, int type,
                 boolean isPermanentGrant) {
-            if (mContext.checkCallingPermission(Manifest.permission.CREATE_MEDIA_PROJECTION)
+            if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
                         != PackageManager.PERMISSION_GRANTED) {
-                throw new SecurityException("Requires CREATE_MEDIA_PROJECTION in order to grant "
+                throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to grant "
                         + "projection permission");
             }
+            if (packageName == null || packageName.isEmpty()) {
+                throw new IllegalArgumentException("package name must not be empty");
+            }
             long callingToken = Binder.clearCallingIdentity();
             MediaProjection projection;
             try {
@@ -136,7 +231,71 @@
 
         @Override // Binder call
         public boolean isValidMediaProjection(IMediaProjection projection) {
-            return mProjectionGrants.containsKey(projection.asBinder());
+            return MediaProjectionManagerService.this.isValidMediaProjection(
+                    projection.asBinder());
+        }
+
+        @Override // Binder call
+        public MediaProjectionInfo getActiveProjectionInfo() {
+            if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
+                        != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add "
+                        + "projection callbacks");
+            }
+            final long token = Binder.clearCallingIdentity();
+            try {
+                return MediaProjectionManagerService.this.getActiveProjectionInfo();
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override // Binder call
+        public void stopActiveProjection() {
+            if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
+                        != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add "
+                        + "projection callbacks");
+            }
+            final long token = Binder.clearCallingIdentity();
+            try {
+                if (mProjectionGrant != null) {
+                    mProjectionGrant.stop();
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+
+        }
+
+        @Override //Binder call
+        public void addCallback(final IMediaProjectionWatcherCallback callback) {
+            if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
+                        != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add "
+                        + "projection callbacks");
+            }
+            final long token = Binder.clearCallingIdentity();
+            try {
+                MediaProjectionManagerService.this.addCallback(callback);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override
+        public void removeCallback(IMediaProjectionWatcherCallback callback) {
+            if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
+                        != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to remove "
+                        + "projection callbacks");
+            }
+            final long token = Binder.clearCallingIdentity();
+            try {
+                MediaProjectionManagerService.this.removeCallback(callback);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
         }
 
         @Override // Binder call
@@ -157,25 +316,27 @@
             }
         }
 
+
         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 final class MediaProjection extends IMediaProjection.Stub {
+        public final int uid;
+        public final String packageName;
+        public final UserHandle userHandle;
 
         private IBinder mToken;
+        private IBinder.DeathRecipient mDeathEater;
         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();
+            userHandle = new UserHandle(UserHandle.getUserId(uid));
         }
 
         @Override // Binder call
@@ -220,49 +381,50 @@
         }
 
         @Override // Binder call
-        public void start(IMediaProjectionCallback callback) {
+        public void start(final IMediaProjectionCallback callback) {
             if (callback == null) {
                 throw new IllegalArgumentException("callback must not be null");
             }
             synchronized (mLock) {
-                if (mProjectionGrants.containsKey(asBinder())) {
+                if (isValidMediaProjection(asBinder())) {
                     throw new IllegalStateException(
                             "Cannot start already started MediaProjection");
                 }
                 addCallback(callback);
                 try {
                     mToken = callback.asBinder();
-                    mToken.linkToDeath(this, 0);
+                    mDeathEater = new IBinder.DeathRecipient() {
+                        @Override
+                        public void binderDied() {
+                            mCallbackDelegate.remove(callback);
+                            stop();
+                        }
+                    };
+                    mToken.linkToDeath(mDeathEater, 0);
                 } catch (RemoteException e) {
                     Slog.w(TAG,
                             "MediaProjectionCallbacks must be valid, aborting MediaProjection", e);
                     return;
                 }
-                mProjectionGrants.put(asBinder(), this);
+                startProjectionLocked(this);
             }
         }
 
         @Override // Binder call
         public void stop() {
             synchronized (mLock) {
-                if (!mProjectionGrants.containsKey(asBinder())) {
+                if (!isValidMediaProjection(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());
+                mToken.unlinkToDeath(mDeathEater, 0);
+                stopProjectionLocked(this);
             }
         }
 
         @Override
-        public void binderDied() {
-            stop();
-        }
-
-        @Override
         public void addCallback(IMediaProjectionCallback callback) {
             if (callback == null) {
                 throw new IllegalArgumentException("callback must not be null");
@@ -278,52 +440,143 @@
             mCallbackDelegate.remove(callback);
         }
 
-        public void dump(PrintWriter pw, String prefix) {
-            pw.println(prefix + "(" + packageName + ", uid=" + uid + "): " + typeToString(mType));
+        public MediaProjectionInfo getProjectionInfo() {
+            return new MediaProjectionInfo(packageName, userHandle);
+        }
+
+        public void dump(PrintWriter pw) {
+            pw.println("(" + packageName + ", uid=" + uid + "): " + typeToString(mType));
         }
     }
 
+
     private static class CallbackDelegate {
-        private static final int MSG_ON_STOP = 0;
-        private List<IMediaProjectionCallback> mCallbacks;
+        private Map<IBinder, IMediaProjectionCallback> mClientCallbacks;
+        private Map<IBinder, IMediaProjectionWatcherCallback> mWatcherCallbacks;
         private Handler mHandler;
         private Object mLock = new Object();
 
         public CallbackDelegate() {
             mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/);
-            mCallbacks = new ArrayList<IMediaProjectionCallback>();
+            mClientCallbacks = new ArrayMap<IBinder, IMediaProjectionCallback>();
+            mWatcherCallbacks = new ArrayMap<IBinder, IMediaProjectionWatcherCallback>();
         }
 
         public void add(IMediaProjectionCallback callback) {
             synchronized (mLock) {
-                mCallbacks.add(callback);
+                mClientCallbacks.put(callback.asBinder(), callback);
+            }
+        }
+
+        public void add(IMediaProjectionWatcherCallback callback) {
+            synchronized (mLock) {
+                mWatcherCallbacks.put(callback.asBinder(), callback);
             }
         }
 
         public void remove(IMediaProjectionCallback callback) {
             synchronized (mLock) {
-                mCallbacks.remove(callback);
+                mClientCallbacks.remove(callback.asBinder());
             }
         }
 
-        public void dispatchStop() {
+        public void remove(IMediaProjectionWatcherCallback callback) {
             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);
-                            }
-                        }
-                    });
+                mWatcherCallbacks.remove(callback.asBinder());
+            }
+        }
+
+        public void dispatchStart(MediaProjection projection) {
+            if (projection == null) {
+                Slog.e(TAG, "Tried to dispatch start notification for a null media projection."
+                        + " Ignoring!");
+                return;
+            }
+            synchronized (mLock) {
+                for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
+                    MediaProjectionInfo info = projection.getProjectionInfo();
+                    mHandler.post(new WatcherStartCallback(info, callback));
+                }
+            }
+        }
+
+        public void dispatchStop(MediaProjection projection) {
+            if (projection == null) {
+                Slog.e(TAG, "Tried to dispatch stop notification for a null media projection."
+                        + " Ignoring!");
+                return;
+            }
+            synchronized (mLock) {
+                for (IMediaProjectionCallback callback : mClientCallbacks.values()) {
+                    mHandler.post(new ClientStopCallback(callback));
+                }
+
+                for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) {
+                    MediaProjectionInfo info = projection.getProjectionInfo();
+                    mHandler.post(new WatcherStopCallback(info, callback));
                 }
             }
         }
     }
 
+    private static final class WatcherStartCallback implements Runnable {
+        private IMediaProjectionWatcherCallback mCallback;
+        private MediaProjectionInfo mInfo;
+
+        public WatcherStartCallback(MediaProjectionInfo info,
+                IMediaProjectionWatcherCallback callback) {
+            mInfo = info;
+            mCallback = callback;
+        }
+
+        @Override
+        public void run() {
+            try {
+                mCallback.onStart(mInfo);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Failed to notify media projection has stopped", e);
+            }
+        }
+    }
+
+    private static final class WatcherStopCallback implements Runnable {
+        private IMediaProjectionWatcherCallback mCallback;
+        private MediaProjectionInfo mInfo;
+
+        public WatcherStopCallback(MediaProjectionInfo info,
+                IMediaProjectionWatcherCallback callback) {
+            mInfo = info;
+            mCallback = callback;
+        }
+
+        @Override
+        public void run() {
+            try {
+                mCallback.onStop(mInfo);
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Failed to notify media projection has stopped", e);
+            }
+        }
+    }
+
+    private static final class ClientStopCallback implements Runnable {
+        private IMediaProjectionCallback mCallback;
+
+        public ClientStopCallback(IMediaProjectionCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void run() {
+            try {
+                mCallback.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: