Create MEDIA_ROUTING_CONTROL app op permission for proxy routing

The new permission allows holders of COMPANION_DEVICE_WATCH to use
MediaRouter2 to control the routing of other apps from the watch.
Users will grant the permission from a Special App Access setting.

Bug: 305919655
Bug: 192657812
Test: atest CtsMediaBetterTogetherTestCases
Change-Id: I204ddbf545c3e8952bd6bec1ef86bffadbe58cbd
diff --git a/core/api/current.txt b/core/api/current.txt
index 7d51574..d8fd7a3 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -211,6 +211,7 @@
     field public static final String MANAGE_WIFI_NETWORK_SELECTION = "android.permission.MANAGE_WIFI_NETWORK_SELECTION";
     field public static final String MASTER_CLEAR = "android.permission.MASTER_CLEAR";
     field public static final String MEDIA_CONTENT_CONTROL = "android.permission.MEDIA_CONTENT_CONTROL";
+    field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String MEDIA_ROUTING_CONTROL = "android.permission.MEDIA_ROUTING_CONTROL";
     field public static final String MODIFY_AUDIO_SETTINGS = "android.permission.MODIFY_AUDIO_SETTINGS";
     field public static final String MODIFY_PHONE_STATE = "android.permission.MODIFY_PHONE_STATE";
     field public static final String MOUNT_FORMAT_FILESYSTEMS = "android.permission.MOUNT_FORMAT_FILESYSTEMS";
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 7691c1e..3309aee 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -639,6 +639,7 @@
     field public static final String OPSTR_MANAGE_EXTERNAL_STORAGE = "android:manage_external_storage";
     field public static final String OPSTR_MANAGE_IPSEC_TUNNELS = "android:manage_ipsec_tunnels";
     field public static final String OPSTR_MANAGE_ONGOING_CALLS = "android:manage_ongoing_calls";
+    field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String OPSTR_MEDIA_ROUTING_CONTROL = "android:media_routing_control";
     field public static final String OPSTR_MUTE_MICROPHONE = "android:mute_microphone";
     field public static final String OPSTR_NEIGHBORING_CELLS = "android:neighboring_cells";
     field public static final String OPSTR_PHONE_CALL_CAMERA = "android:phone_call_camera";
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index b03bd59..a99dfa6 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -48,6 +48,7 @@
 import android.database.DatabaseUtils;
 import android.health.connect.HealthConnectManager;
 import android.media.AudioAttributes.AttributeUsage;
+import android.media.MediaRouter2;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Handler;
@@ -89,6 +90,7 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.Parcelling;
 import com.android.internal.util.Preconditions;
+import com.android.media.flags.Flags;
 
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
@@ -1506,9 +1508,15 @@
     public static final int OP_CREATE_ACCESSIBILITY_OVERLAY =
             AppProtoEnums.APP_OP_CREATE_ACCESSIBILITY_OVERLAY;
 
+    /**
+     * See {@link #OPSTR_MEDIA_ROUTING_CONTROL}.
+     * @hide
+     */
+    public static final int OP_MEDIA_ROUTING_CONTROL = AppProtoEnums.APP_OP_MEDIA_ROUTING_CONTROL;
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public static final int _NUM_OP = 139;
+    public static final int _NUM_OP = 140;
 
     /**
      * All app ops represented as strings.
@@ -1654,6 +1662,7 @@
             OPSTR_RECEIVE_SANDBOX_TRIGGER_AUDIO,
             OPSTR_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
             OPSTR_CREATE_ACCESSIBILITY_OVERLAY,
+            OPSTR_MEDIA_ROUTING_CONTROL,
     })
     public @interface AppOpString {}
 
@@ -1981,6 +1990,19 @@
     public static final String OPSTR_MANAGE_ONGOING_CALLS = "android:manage_ongoing_calls";
 
     /**
+     * Allows apps holding this permission to control the routing of other apps via {@link
+     * MediaRouter2}.
+     *
+     * <p>For example, holding this permission allows watches (via companion apps) to control the
+     * routing of applications running on the phone.
+     *
+     * @hide
+     */
+    @SystemApi
+    @FlaggedApi(Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
+    public static final String OPSTR_MEDIA_ROUTING_CONTROL = "android:media_routing_control";
+
+    /**
      * AppOp granted to apps that we are started via {@code am instrument -e --no-isolated-storage}
      *
      * <p>MediaProvider is the only component (outside of system server) that should care about this
@@ -2404,7 +2426,8 @@
             OP_CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD,
             OP_USE_FULL_SCREEN_INTENT,
             OP_RECEIVE_SANDBOX_TRIGGER_AUDIO,
-            OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA
+            OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
+            OP_MEDIA_ROUTING_CONTROL,
     };
 
     static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{
@@ -2846,6 +2869,9 @@
                 OPSTR_CREATE_ACCESSIBILITY_OVERLAY,
                 "CREATE_ACCESSIBILITY_OVERLAY")
                 .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
+        new AppOpInfo.Builder(OP_MEDIA_ROUTING_CONTROL, OPSTR_MEDIA_ROUTING_CONTROL,
+                "MEDIA_ROUTING_CONTROL")
+                .setPermission(Manifest.permission.MEDIA_ROUTING_CONTROL).build(),
     };
 
     // The number of longs needed to form a full bitmask of app ops
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index c75f996..72e0fe9 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5981,6 +5981,13 @@
     <permission android:name="android.permission.MEDIA_CONTENT_CONTROL"
         android:protectionLevel="signature|privileged" />
 
+    <!-- Allows an application to control the routing of media apps.
+         <p>Only for use by role COMPANION_DEVICE_WATCH</p>
+         @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control")
+         -->
+    <permission android:name="android.permission.MEDIA_ROUTING_CONTROL"
+                android:protectionLevel="signature|appop" />
+
     <!-- @SystemApi @hide Allows an application to set the volume key long-press listener.
          <p>When it's set, the application will receive the volume key long-press event
          instead of changing volume.</p>
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index dd1df47..283d61b 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -48,3 +48,10 @@
     description: "Enables the following type constants in MediaRoute2Info: CAR, COMPUTER, GAME_CONSOLE, SMARTPHONE, SMARTWATCH, TABLET, TABLET_DOCKED. Note that this doesn't gate any behavior. It only guards some API int symbols."
     bug: "301713440"
 }
+
+flag {
+    name: "enable_privileged_routing_for_media_routing_control"
+    namespace: "media_solutions"
+    description: "Allow access to privileged routing capabilities to MEDIA_ROUTING_CONTROL holders."
+    bug: "305919655"
+}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 10d04d3..c7e5bf9 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -668,6 +668,7 @@
 
     <!-- Permission required for CTS test - SystemMediaRouter2Test -->
     <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
+    <uses-permission android:name="android.permission.MEDIA_ROUTING_CONTROL" />
     <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
 
     <!-- Permission required for CTS test - SoundDoseHelperTest -->
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index d456a74..6a43697 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -36,6 +36,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.PermissionChecker;
 import android.content.pm.PackageManager;
 import android.media.IMediaRouter2;
 import android.media.IMediaRouter2Manager;
@@ -200,7 +201,8 @@
         final long token = Binder.clearCallingIdentity();
 
         try {
-            enforcePrivilegedRoutingPermissions(uid, pid);
+            // TODO (b/305919655) - Handle revoking of MEDIA_ROUTING_CONTROL at runtime.
+            enforcePrivilegedRoutingPermissions(uid, pid, /* callerPackageName */ null);
             PackageManager pm = mContext.getPackageManager();
             pm.getPackageInfo(clientPackageName, PackageManager.PackageInfoFlags.of(0));
             return true;
@@ -727,13 +729,36 @@
         return hasBluetoothRoutingPermission;
     }
 
-    @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
-    private void enforcePrivilegedRoutingPermissions(int callerUid, int callerPid) {
-        mContext.enforcePermission(
-                Manifest.permission.MEDIA_CONTENT_CONTROL,
-                callerPid,
-                callerUid,
-                "Must hold MEDIA_CONTENT_CONTROL permission.");
+    @RequiresPermission(
+            anyOf = {
+                Manifest.permission.MEDIA_ROUTING_CONTROL,
+                Manifest.permission.MEDIA_CONTENT_CONTROL
+            })
+    private void enforcePrivilegedRoutingPermissions(
+            int callerUid, int callerPid, @Nullable String callerPackageName) {
+        if (mContext.checkPermission(
+                        Manifest.permission.MEDIA_CONTENT_CONTROL, callerPid, callerUid)
+                == PackageManager.PERMISSION_GRANTED) {
+            return;
+        }
+
+        if (!Flags.enablePrivilegedRoutingForMediaRoutingControl()) {
+            throw new SecurityException("Must hold MEDIA_CONTENT_CONTROL");
+        }
+
+        if (PermissionChecker.checkPermissionForDataDelivery(
+                        mContext,
+                        Manifest.permission.MEDIA_ROUTING_CONTROL,
+                        callerPid,
+                        callerUid,
+                        callerPackageName,
+                        /* attributionTag */ null,
+                        /* message */ "Checking permissions for registering manager in"
+                                          + " MediaRouter2ServiceImpl.")
+                != PermissionChecker.PERMISSION_GRANTED) {
+            throw new SecurityException(
+                    "Must hold MEDIA_CONTENT_CONTROL or MEDIA_ROUTING_CONTROL permissions.");
+        }
     }
 
     // End of methods that implements operations for both MediaRouter2 and MediaRouter2Manager.
@@ -1195,7 +1220,8 @@
                             + " callerUserId: %d",
                         callerUid, callerPid, callerPackageName, callerUserId));
 
-        enforcePrivilegedRoutingPermissions(callerUid, callerPid);
+        // TODO (b/305919655) - Handle revoking of MEDIA_ROUTING_CONTROL at runtime.
+        enforcePrivilegedRoutingPermissions(callerUid, callerPid, callerPackageName);
 
         UserRecord userRecord = getOrCreateUserRecordLocked(callerUserId);
         managerRecord = new ManagerRecord(