Disallow applications from initiating cast screen.

Only allow the system ui and settings to connect to a remote display.
To do this, we essentially hide the remote displays from applications
by using the ROUTE_TYPE_REMOTE_DISPLAY then add permission checks
around the operations that connect to them.

As a bonus, this may actually save power on devices since applications
that use MediaRouter will not longer be performing discover on
remote display routes at all.

Bug: 11257292
Change-Id: I9ea8c568df4df5a0f0cf3d0f11b39c87e2110795
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index f12be5f..d5208d9 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -299,6 +299,10 @@
     /**
      * Initiates a fresh scan of availble Wifi displays.
      * The results are sent as a {@link #ACTION_WIFI_DISPLAY_STATUS_CHANGED} broadcast.
+     * <p>
+     * Requires {@link android.Manifest.permission#CONFIGURE_WIFI_DISPLAY}.
+     * </p>
+     *
      * @hide
      */
     public void scanWifiDisplays() {
@@ -312,8 +316,7 @@
      * Automatically remembers the display after a successful connection, if not
      * already remembered.
      * </p><p>
-     * Requires {@link android.Manifest.permission#CONFIGURE_WIFI_DISPLAY} to connect
-     * to unknown displays.  No permissions are required to connect to already known displays.
+     * Requires {@link android.Manifest.permission#CONFIGURE_WIFI_DISPLAY}.
      * </p>
      *
      * @param deviceAddress The MAC address of the device to which we should connect.
diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java
index 3d10158..525dc4f 100644
--- a/media/java/android/media/MediaRouter.java
+++ b/media/java/android/media/MediaRouter.java
@@ -18,11 +18,13 @@
 
 import com.android.internal.util.Objects;
 
+import android.Manifest;
 import android.app.ActivityThread;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
 import android.hardware.display.DisplayManager;
@@ -30,6 +32,7 @@
 import android.hardware.display.WifiDisplayStatus;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Process;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.UserHandle;
@@ -82,6 +85,7 @@
 
         RouteInfo mSelectedRoute;
 
+        final boolean mCanConfigureWifiDisplays;
         boolean mActivelyScanningWifiDisplays;
 
         int mDiscoveryRequestRouteTypes;
@@ -129,6 +133,13 @@
                     com.android.internal.R.string.default_audio_route_category_name,
                     ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false);
             mSystemCategory.mIsSystem = true;
+
+            // Only the system can configure wifi displays.  The display manager
+            // enforces this with a permission check.  Set a flag here so that we
+            // know whether this process is actually allowed to scan and connect.
+            mCanConfigureWifiDisplays = appContext.checkPermission(
+                    Manifest.permission.CONFIGURE_WIFI_DISPLAY,
+                    Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED;
         }
 
         // Called after sStatic is initialized
@@ -255,7 +266,7 @@
                 }
                 if ((cbi.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) {
                     activeScan = true;
-                    if ((cbi.type & (ROUTE_TYPE_LIVE_VIDEO | ROUTE_TYPE_REMOTE_DISPLAY)) != 0) {
+                    if ((cbi.type & ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
                         activeScanWifiDisplay = true;
                     }
                 }
@@ -268,7 +279,7 @@
             }
 
             // Update wifi display scanning.
-            if (activeScanWifiDisplay) {
+            if (activeScanWifiDisplay && mCanConfigureWifiDisplays) {
                 if (!mActivelyScanningWifiDisplays) {
                     mActivelyScanningWifiDisplays = true;
                     mHandler.post(mScanWifiDisplays);
@@ -493,7 +504,8 @@
                 route.mDescription = globalRoute.description;
                 changed = true;
             }
-            if (route.mSupportedTypes != globalRoute.supportedTypes) {
+            final int oldSupportedTypes = route.mSupportedTypes;
+            if (oldSupportedTypes != globalRoute.supportedTypes) {
                 route.mSupportedTypes = globalRoute.supportedTypes;
                 changed = true;
             }
@@ -536,7 +548,7 @@
             }
 
             if (changed) {
-                dispatchRouteChanged(route);
+                dispatchRouteChanged(route, oldSupportedTypes);
             }
             if (volumeChanged) {
                 dispatchRouteVolumeChanged(route);
@@ -908,7 +920,12 @@
         final boolean newRouteHasAddress = route != null && route.mDeviceAddress != null;
         if (activeDisplay != null || oldRouteHasAddress || newRouteHasAddress) {
             if (newRouteHasAddress && !matchesDeviceAddress(activeDisplay, route)) {
-                sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress);
+                if (sStatic.mCanConfigureWifiDisplays) {
+                    sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress);
+                } else {
+                    Log.e(TAG, "Cannot connect to wifi displays because this process "
+                            + "is not allowed to do so.");
+                }
             } else if (activeDisplay != null && !newRouteHasAddress) {
                 sStatic.mDisplayService.disconnectWifiDisplay();
             }
@@ -1175,10 +1192,34 @@
     }
 
     static void dispatchRouteChanged(RouteInfo info) {
+        dispatchRouteChanged(info, info.mSupportedTypes);
+    }
+
+    static void dispatchRouteChanged(RouteInfo info, int oldSupportedTypes) {
+        final int newSupportedTypes = info.mSupportedTypes;
         for (CallbackInfo cbi : sStatic.mCallbacks) {
-            if (cbi.filterRouteEvent(info)) {
+            // Reconstruct some of the history for callbacks that may not have observed
+            // all of the events needed to correctly interpret the current state.
+            // FIXME: This is a strong signal that we should deprecate route type filtering
+            // completely in the future because it can lead to inconsistencies in
+            // applications.
+            final boolean oldVisibility = cbi.filterRouteEvent(oldSupportedTypes);
+            final boolean newVisibility = cbi.filterRouteEvent(newSupportedTypes);
+            if (!oldVisibility && newVisibility) {
+                cbi.cb.onRouteAdded(cbi.router, info);
+                if (info.isSelected()) {
+                    cbi.cb.onRouteSelected(cbi.router, newSupportedTypes, info);
+                }
+            }
+            if (oldVisibility || newVisibility) {
                 cbi.cb.onRouteChanged(cbi.router, info);
             }
+            if (oldVisibility && !newVisibility) {
+                if (info.isSelected()) {
+                    cbi.cb.onRouteUnselected(cbi.router, oldSupportedTypes, info);
+                }
+                cbi.cb.onRouteRemoved(cbi.router, info);
+            }
         }
     }
 
@@ -1257,6 +1298,18 @@
         if (status.getFeatureState() == WifiDisplayStatus.FEATURE_STATE_ON) {
             displays = status.getDisplays();
             activeDisplay = status.getActiveDisplay();
+
+            // Only the system is able to connect to wifi display routes.
+            // The display manager will enforce this with a permission check but it
+            // still publishes information about all available displays.
+            // Filter the list down to just the active display.
+            if (!sStatic.mCanConfigureWifiDisplays) {
+                if (activeDisplay != null) {
+                    displays = new WifiDisplay[] { activeDisplay };
+                } else {
+                    displays = WifiDisplay.EMPTY_ARRAY;
+                }
+            }
         } else {
             displays = WifiDisplay.EMPTY_ARRAY;
             activeDisplay = null;
@@ -1293,7 +1346,7 @@
 
         // Don't scan if we're already connected to a wifi display,
         // the scanning process can cause a hiccup with some configurations.
-        if (wantScan && activeDisplay != null) {
+        if (wantScan && activeDisplay != null && sStatic.mCanConfigureWifiDisplays) {
             sStatic.mDisplayService.scanWifiDisplays();
         }
     }
@@ -2547,8 +2600,12 @@
         }
 
         public boolean filterRouteEvent(RouteInfo route) {
+            return filterRouteEvent(route.mSupportedTypes);
+        }
+
+        public boolean filterRouteEvent(int supportedTypes) {
             return (flags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
-                    || (type & route.mSupportedTypes) != 0;
+                    || (type & supportedTypes) != 0;
         }
     }
 
diff --git a/media/java/android/media/MediaRouterClientState.java b/media/java/android/media/MediaRouterClientState.java
index 0847503..54b8276 100644
--- a/media/java/android/media/MediaRouterClientState.java
+++ b/media/java/android/media/MediaRouterClientState.java
@@ -50,6 +50,17 @@
         globallySelectedRouteId = src.readString();
     }
 
+    public RouteInfo getRoute(String id) {
+        final int count = routes.size();
+        for (int i = 0; i < count; i++) {
+            final RouteInfo route = routes.get(i);
+            if (route.id.equals(id)) {
+                return route;
+            }
+        }
+        return null;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -61,6 +72,12 @@
         dest.writeString(globallySelectedRouteId);
     }
 
+    @Override
+    public String toString() {
+        return "MediaRouterClientState{ globallySelectedRouteId="
+                + globallySelectedRouteId + ", routes=" + routes.toString() + " }";
+    }
+
     public static final Parcelable.Creator<MediaRouterClientState> CREATOR =
             new Parcelable.Creator<MediaRouterClientState>() {
         @Override
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 09ac2da..8d6fe41 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -71,6 +71,9 @@
     <!-- Keyguard -->
     <uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
 
+    <!-- Wifi Display -->
+    <uses-permission android:name="android.permission.CONFIGURE_WIFI_DISPLAY" />
+
     <application
         android:persistent="true"
         android:allowClearUserData="false"
diff --git a/services/java/com/android/server/display/DisplayManagerService.java b/services/java/com/android/server/display/DisplayManagerService.java
index 249c8b0..02f26b3 100644
--- a/services/java/com/android/server/display/DisplayManagerService.java
+++ b/services/java/com/android/server/display/DisplayManagerService.java
@@ -466,6 +466,9 @@
 
     @Override // Binder call
     public void scanWifiDisplays() {
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.CONFIGURE_WIFI_DISPLAY,
+                "Permission required to scan wifi displays");
+
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mSyncRoot) {
@@ -483,13 +486,14 @@
         if (address == null) {
             throw new IllegalArgumentException("address must not be null");
         }
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.CONFIGURE_WIFI_DISPLAY,
+                "Permission required to connect to a wifi display");
 
-        final boolean trusted = canCallerConfigureWifiDisplay();
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mSyncRoot) {
                 if (mWifiDisplayAdapter != null) {
-                    mWifiDisplayAdapter.requestConnectLocked(address, trusted);
+                    mWifiDisplayAdapter.requestConnectLocked(address);
                 }
             }
         } finally {
@@ -499,12 +503,8 @@
 
     @Override
     public void pauseWifiDisplay() {
-        if (mContext.checkCallingPermission(
-                android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
-                        != PackageManager.PERMISSION_GRANTED) {
-            throw new SecurityException("Requires CONFIGURE_WIFI_DISPLAY"
-                    + "permission to pause a wifi display session.");
-        }
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.CONFIGURE_WIFI_DISPLAY,
+                "Permission required to pause a wifi display session");
 
         final long token = Binder.clearCallingIdentity();
         try {
@@ -520,12 +520,8 @@
 
     @Override
     public void resumeWifiDisplay() {
-        if (mContext.checkCallingPermission(
-                android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
-                        != PackageManager.PERMISSION_GRANTED) {
-            throw new SecurityException("Requires CONFIGURE_WIFI_DISPLAY"
-                    + "permission to resume a wifi display session.");
-        }
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.CONFIGURE_WIFI_DISPLAY,
+                "Permission required to resume a wifi display session");
 
         final long token = Binder.clearCallingIdentity();
         try {
@@ -541,6 +537,11 @@
 
     @Override // Binder call
     public void disconnectWifiDisplay() {
+        // This request does not require special permissions.
+        // Any app can request disconnection from the currently active wifi display.
+        // This exception should no longer be needed once wifi display control moves
+        // to the media router service.
+
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mSyncRoot) {
@@ -558,10 +559,8 @@
         if (address == null) {
             throw new IllegalArgumentException("address must not be null");
         }
-        if (!canCallerConfigureWifiDisplay()) {
-            throw new SecurityException("Requires CONFIGURE_WIFI_DISPLAY permission to "
-                    + "rename a wifi display.");
-        }
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.CONFIGURE_WIFI_DISPLAY,
+                "Permission required to rename to a wifi display");
 
         final long token = Binder.clearCallingIdentity();
         try {
@@ -580,10 +579,8 @@
         if (address == null) {
             throw new IllegalArgumentException("address must not be null");
         }
-        if (!canCallerConfigureWifiDisplay()) {
-            throw new SecurityException("Requires CONFIGURE_WIFI_DISPLAY permission to "
-                    + "forget a wifi display.");
-        }
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.CONFIGURE_WIFI_DISPLAY,
+                "Permission required to forget to a wifi display");
 
         final long token = Binder.clearCallingIdentity();
         try {
@@ -599,6 +596,9 @@
 
     @Override // Binder call
     public WifiDisplayStatus getWifiDisplayStatus() {
+        // This request does not require special permissions.
+        // Any app can get information about available wifi displays.
+
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mSyncRoot) {
@@ -612,11 +612,6 @@
         }
     }
 
-    private boolean canCallerConfigureWifiDisplay() {
-        return mContext.checkCallingPermission(android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
-                == PackageManager.PERMISSION_GRANTED;
-    }
-
     @Override // Binder call
     public int createVirtualDisplay(IBinder appToken, String packageName,
             String name, int width, int height, int densityDpi, Surface surface, int flags) {
diff --git a/services/java/com/android/server/display/WifiDisplayAdapter.java b/services/java/com/android/server/display/WifiDisplayAdapter.java
index 11558a3..fdef039 100644
--- a/services/java/com/android/server/display/WifiDisplayAdapter.java
+++ b/services/java/com/android/server/display/WifiDisplayAdapter.java
@@ -172,19 +172,9 @@
         });
     }
 
-    public void requestConnectLocked(final String address, final boolean trusted) {
+    public void requestConnectLocked(final String address) {
         if (DEBUG) {
-            Slog.d(TAG, "requestConnectLocked: address=" + address + ", trusted=" + trusted);
-        }
-
-        if (!trusted) {
-            synchronized (getSyncRoot()) {
-                if (!isRememberedDisplayLocked(address)) {
-                    Slog.w(TAG, "Ignoring request by an untrusted client to connect to "
-                            + "an unknown wifi display: " + address);
-                    return;
-                }
-            }
+            Slog.d(TAG, "requestConnectLocked: address=" + address);
         }
 
         getHandler().post(new Runnable() {
diff --git a/services/java/com/android/server/media/MediaRouterService.java b/services/java/com/android/server/media/MediaRouterService.java
index 1491eb6..a31695b 100644
--- a/services/java/com/android/server/media/MediaRouterService.java
+++ b/services/java/com/android/server/media/MediaRouterService.java
@@ -128,10 +128,13 @@
         final int pid = Binder.getCallingPid();
         final int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
                 false /*allowAll*/, true /*requireFull*/, "registerClientAsUser", packageName);
+        final boolean trusted = mContext.checkCallingOrSelfPermission(
+                android.Manifest.permission.CONFIGURE_WIFI_DISPLAY) ==
+                PackageManager.PERMISSION_GRANTED;
         final long token = Binder.clearCallingIdentity();
         try {
             synchronized (mLock) {
-                registerClientLocked(client, pid, packageName, resolvedUserId);
+                registerClientLocked(client, pid, packageName, resolvedUserId, trusted);
             }
         } finally {
             Binder.restoreCallingIdentity(token);
@@ -306,7 +309,7 @@
     }
 
     private void registerClientLocked(IMediaRouterClient client,
-            int pid, String packageName, int userId) {
+            int pid, String packageName, int userId, boolean trusted) {
         final IBinder binder = client.asBinder();
         ClientRecord clientRecord = mAllClientRecords.get(binder);
         if (clientRecord == null) {
@@ -316,7 +319,7 @@
                 userRecord = new UserRecord(userId);
                 newUser = true;
             }
-            clientRecord = new ClientRecord(userRecord, client, pid, packageName);
+            clientRecord = new ClientRecord(userRecord, client, pid, packageName, trusted);
             try {
                 binder.linkToDeath(clientRecord, 0);
             } catch (RemoteException ex) {
@@ -347,7 +350,7 @@
     private MediaRouterClientState getStateLocked(IMediaRouterClient client) {
         ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
         if (clientRecord != null) {
-            return clientRecord.mUserRecord.mState;
+            return clientRecord.getState();
         }
         return null;
     }
@@ -357,6 +360,11 @@
         final IBinder binder = client.asBinder();
         ClientRecord clientRecord = mAllClientRecords.get(binder);
         if (clientRecord != null) {
+            // Only let the system discover remote display routes for now.
+            if (!clientRecord.mTrusted) {
+                routeTypes &= ~MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
+            }
+
             if (clientRecord.mRouteTypes != routeTypes
                     || clientRecord.mActiveScan != activeScan) {
                 if (DEBUG) {
@@ -385,11 +393,14 @@
 
                 clientRecord.mSelectedRouteId = routeId;
                 if (explicit) {
+                    // Any app can disconnect from the globally selected route.
                     if (oldRouteId != null) {
                         clientRecord.mUserRecord.mHandler.obtainMessage(
                                 UserHandler.MSG_UNSELECT_ROUTE, oldRouteId).sendToTarget();
                     }
-                    if (routeId != null) {
+                    // Only let the system connect to new global routes for now.
+                    // A similar check exists in the display manager for wifi display.
+                    if (routeId != null && clientRecord.mTrusted) {
                         clientRecord.mUserRecord.mHandler.obtainMessage(
                                 UserHandler.MSG_SELECT_ROUTE, routeId).sendToTarget();
                     }
@@ -486,17 +497,19 @@
         public final IMediaRouterClient mClient;
         public final int mPid;
         public final String mPackageName;
+        public final boolean mTrusted;
 
         public int mRouteTypes;
         public boolean mActiveScan;
         public String mSelectedRouteId;
 
         public ClientRecord(UserRecord userRecord, IMediaRouterClient client,
-                int pid, String packageName) {
+                int pid, String packageName, boolean trusted) {
             mUserRecord = userRecord;
             mClient = client;
             mPid = pid;
             mPackageName = packageName;
+            mTrusted = trusted;
         }
 
         public void dispose() {
@@ -508,10 +521,15 @@
             clientDied(this);
         }
 
+        MediaRouterClientState getState() {
+            return mTrusted ? mUserRecord.mTrustedState : mUserRecord.mUntrustedState;
+        }
+
         public void dump(PrintWriter pw, String prefix) {
             pw.println(prefix + this);
 
             final String indent = prefix + "  ";
+            pw.println(indent + "mTrusted=" + mTrusted);
             pw.println(indent + "mRouteTypes=0x" + Integer.toHexString(mRouteTypes));
             pw.println(indent + "mActiveScan=" + mActiveScan);
             pw.println(indent + "mSelectedRouteId=" + mSelectedRouteId);
@@ -531,7 +549,8 @@
         public final int mUserId;
         public final ArrayList<ClientRecord> mClientRecords = new ArrayList<ClientRecord>();
         public final UserHandler mHandler;
-        public MediaRouterClientState mState;
+        public MediaRouterClientState mTrustedState;
+        public MediaRouterClientState mUntrustedState;
 
         public UserRecord(int userId) {
             mUserId = userId;
@@ -551,6 +570,10 @@
                 pw.println(indent + "<no clients>");
             }
 
+            pw.println(indent + "State");
+            pw.println(indent + "mTrustedState=" + mTrustedState);
+            pw.println(indent + "mUntrustedState=" + mUntrustedState);
+
             if (!mHandler.runWithScissors(new Runnable() {
                 @Override
                 public void run() {
@@ -729,8 +752,7 @@
             }
 
             final int newDiscoveryMode;
-            if ((routeTypes & (MediaRouter.ROUTE_TYPE_LIVE_VIDEO
-                    | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)) != 0) {
+            if ((routeTypes & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
                 if (activeScan) {
                     newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_ACTIVE;
                 } else {
@@ -968,19 +990,30 @@
         private void updateClientState() {
             mClientStateUpdateScheduled = false;
 
-            // Build a new client state.
-            MediaRouterClientState state = new MediaRouterClientState();
-            state.globallySelectedRouteId = mGloballySelectedRouteRecord != null ?
+            final String globallySelectedRouteId = mGloballySelectedRouteRecord != null ?
                     mGloballySelectedRouteRecord.getUniqueId() : null;
+
+            // Build a new client state for trusted clients.
+            MediaRouterClientState trustedState = new MediaRouterClientState();
+            trustedState.globallySelectedRouteId = globallySelectedRouteId;
             final int providerCount = mProviderRecords.size();
             for (int i = 0; i < providerCount; i++) {
-                mProviderRecords.get(i).appendClientState(state);
+                mProviderRecords.get(i).appendClientState(trustedState);
+            }
+
+            // Build a new client state for untrusted clients that can only see
+            // the currently selected route.
+            MediaRouterClientState untrustedState = new MediaRouterClientState();
+            untrustedState.globallySelectedRouteId = globallySelectedRouteId;
+            if (globallySelectedRouteId != null) {
+                untrustedState.routes.add(trustedState.getRoute(globallySelectedRouteId));
             }
 
             try {
                 synchronized (mService.mLock) {
                     // Update the UserRecord.
-                    mUserRecord.mState = state;
+                    mUserRecord.mTrustedState = trustedState;
+                    mUserRecord.mUntrustedState = untrustedState;
 
                     // Collect all clients.
                     final int count = mUserRecord.mClientRecords.size();