Merge "[Audiosharing] Implement source add and remove." into main
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b7043d4..2a45624 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4185,6 +4185,25 @@
                        android:value="@string/menu_key_apps"/>
         </activity>
 
+        <!-- @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") -->
+        <activity-alias
+            android:name="MediaRoutingControlActivity"
+            android:knownActivityEmbeddingCerts="@array/config_known_host_certs"
+            android:exported="true"
+            android:targetActivity=".spa.SpaBridgeActivity"
+            android:label="@string/media_routing_control_title">
+            <intent-filter android:priority="1">
+                <action android:name="android.settings.REQUEST_MEDIA_ROUTING_CONTROL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <meta-data android:name="com.android.settings.spa.DESTINATION"
+                       android:value="TogglePermissionAppList/MediaRoutingControl"/>
+            <meta-data android:name="com.android.settings.HIGHLIGHT_MENU_KEY"
+                       android:value="@string/menu_key_apps"/>
+            <meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
+                       android:value="true" />
+        </activity-alias>
+
         <!-- Keep compatibility with old WebView-picker implementation -->
         <activity-alias android:name=".WebViewImplementation"
                   android:targetActivity="Settings$WebViewAppPickerActivity"
diff --git a/res/drawable/audio_sharing_rounded_bg.xml b/res/drawable/audio_sharing_rounded_bg.xml
new file mode 100644
index 0000000..db1e1bb
--- /dev/null
+++ b/res/drawable/audio_sharing_rounded_bg.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="?android:colorButtonNormal" />
+    <corners android:radius="12dp" />
+</shape>
\ No newline at end of file
diff --git a/res/drawable/audio_sharing_rounded_bg_ripple.xml b/res/drawable/audio_sharing_rounded_bg_ripple.xml
new file mode 100644
index 0000000..18696c6
--- /dev/null
+++ b/res/drawable/audio_sharing_rounded_bg_ripple.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item android:drawable="@drawable/audio_sharing_rounded_bg"/>
+</ripple>
\ No newline at end of file
diff --git a/res/layout/dialog_custom_title_audio_sharing.xml b/res/layout/dialog_custom_title_audio_sharing.xml
new file mode 100644
index 0000000..0513c4b
--- /dev/null
+++ b/res/layout/dialog_custom_title_audio_sharing.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:padding="?android:dialogPreferredPadding">
+
+    <ImageView
+        android:id="@+id/title_icon"
+        android:layout_width="32dp"
+        android:layout_height="32dp"
+        android:layout_gravity="center"
+        android:tint="?android:attr/colorControlNormal" />
+
+    <TextView
+        android:id="@+id/title_text"
+        style="?android:attr/windowTitleStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:maxLines="2"
+        android:paddingTop="14dp"
+        android:textAlignment="center"
+        android:textSize="24sp" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/privatespace_account_login_error.xml b/res/layout/privatespace_account_login_error.xml
index a38dd50..beae22f 100644
--- a/res/layout/privatespace_account_login_error.xml
+++ b/res/layout/privatespace_account_login_error.xml
@@ -22,7 +22,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:filterTouchesWhenObscured="true"
-    app:sucHeaderText="@string/privatespace_retry_signin_title"
-    app:sudDescriptionText="@string/privatespace_retry_summary"
+    app:sucHeaderText="@string/private_space_retry_signin_title"
+    app:sudDescriptionText="@string/private_space_retry_summary"
     android:icon="@drawable/ic_error_red">
 </com.google.android.setupdesign.GlifLayout>
diff --git a/res/layout/privatespace_advancing_screen.xml b/res/layout/privatespace_advancing_screen.xml
index 5b69593..3a85b16 100644
--- a/res/layout/privatespace_advancing_screen.xml
+++ b/res/layout/privatespace_advancing_screen.xml
@@ -17,7 +17,7 @@
 <com.google.android.setupdesign.GlifLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/privatesapce_autoadvance_screen"
+    android:id="@+id/private_space_autoadvance_screen"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:icon="@drawable/ic_privatespace_icon">
@@ -39,7 +39,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:textSize="14sp"
-            android:text="@string/privatespace_setting_up_text"
+            android:text="@string/private_space_setting_up_text"
             android:layout_marginBottom="24dp"/>
 
     </LinearLayout>
diff --git a/res/layout/privatespace_creation_error.xml b/res/layout/privatespace_creation_error.xml
index af11f3a..0ec0cb6 100644
--- a/res/layout/privatespace_creation_error.xml
+++ b/res/layout/privatespace_creation_error.xml
@@ -17,11 +17,10 @@
 <com.google.android.setupdesign.GlifLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/privatespace_setup_error"
+    android:id="@+id/private_space_setup_error"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:filterTouchesWhenObscured="true"
-    app:sucHeaderText="@string/privatespace_error_screen_title"
-    app:sudDescriptionText="@string/privatespace_error_screen_summary"
+    app:sucHeaderText="@string/private_space_error_screen_title"
     android:icon="@drawable/ic_warning_circle_red">
 </com.google.android.setupdesign.GlifLayout>
diff --git a/res/layout/privatespace_education_screen.xml b/res/layout/privatespace_education_screen.xml
index adb65c9..350e780 100644
--- a/res/layout/privatespace_education_screen.xml
+++ b/res/layout/privatespace_education_screen.xml
@@ -21,8 +21,8 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:filterTouchesWhenObscured="true"
-    app:sucHeaderText="@string/privatespace_setup_title"
-    app:sudDescriptionText="@string/privatespace_hide_apps_summary"
+    app:sucHeaderText="@string/private_space_setup_title"
+    app:sudDescriptionText="@string/private_space_hide_apps_summary"
     android:icon="@drawable/ic_privatespace_icon">
     <ScrollView
         android:id="@+id/main_clear_scrollview"
@@ -43,7 +43,29 @@
             android:src="@drawable/privatespace_placeholder_image"/>
         <TextView
             style="@style/PrivateSpaceSetupSubHeaderStyle"
-            android:text="@string/privatespace_how_title"/>
+            android:text="@string/private_space_how_title"/>
+        <RelativeLayout
+            style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
+            <ImageView
+                android:id="@+id/lockIcon"
+                style="@style/PrivateSpaceBulletPointIconStyle"
+                android:src="@drawable/ic_lock_closed" />
+            <TextView
+                style="@style/PrivateSpaceBulletPointTextFontStyle"
+                android:layout_toRightOf="@+id/lockIcon"
+                android:text="@string/private_space_protected_lock_text"/>
+        </RelativeLayout>
+        <RelativeLayout
+            style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
+            <ImageView
+                android:id="@+id/bellIcon"
+                style="@style/PrivateSpaceBulletPointIconStyle"
+                android:src="@drawable/ic_notifications" />
+            <TextView
+                style="@style/PrivateSpaceBulletPointTextFontStyle"
+                android:layout_toRightOf="@+id/bellIcon"
+                android:text="@string/private_space_hidden_notifications_text"/>
+        </RelativeLayout>
         <RelativeLayout
             style="@style/PrivateSpaceSetupBulletPointLayoutStyle"
             android:layout_width="fill_parent"
@@ -55,29 +77,7 @@
             <TextView
                 style="@style/PrivateSpaceBulletPointTextFontStyle"
                 android:layout_toRightOf="@+id/appsIcon"
-                android:text="@string/privatespace_access_bottom_text"/>
-        </RelativeLayout>
-        <RelativeLayout
-            style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
-            <ImageView
-                android:id="@+id/lockIcon"
-                style="@style/PrivateSpaceBulletPointIconStyle"
-                android:src="@drawable/ic_lock_closed" />
-            <TextView
-                style="@style/PrivateSpaceBulletPointTextFontStyle"
-                android:layout_toRightOf="@+id/lockIcon"
-                android:text="@string/privatespace_protected_lock_text"/>
-        </RelativeLayout>
-        <RelativeLayout
-            style="@style/PrivateSpaceSetupBulletPointLayoutStyle">
-            <ImageView
-                android:id="@+id/bellIcon"
-                style="@style/PrivateSpaceBulletPointIconStyle"
-                android:src="@drawable/ic_notifications" />
-            <TextView
-                style="@style/PrivateSpaceBulletPointTextFontStyle"
-                android:layout_toRightOf="@+id/bellIcon"
-                android:text="@string/privatespace_hidden_notifications_text"/>
+                android:text="@string/private_space_access_bottom_text"/>
         </RelativeLayout>
         <Space
             android:layout_width="wrap_content"
@@ -94,7 +94,7 @@
                 style="@style/PrivateSpaceBulletPointTextFontStyle"
                 android:textSize = "14sp"
                 android:layout_toRightOf="@+id/infoIcon"
-                android:text="@string/privatespace_apps_permission_text"/>
+                android:text="@string/private_space_apps_permission_text"/>
         </RelativeLayout>
     </LinearLayout>
     </ScrollView>
diff --git a/res/layout/privatespace_setlock_screen.xml b/res/layout/privatespace_setlock_screen.xml
index 5caf4ae..7211948 100644
--- a/res/layout/privatespace_setlock_screen.xml
+++ b/res/layout/privatespace_setlock_screen.xml
@@ -21,8 +21,8 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:filterTouchesWhenObscured="true"
-    app:sucHeaderText="@string/privatespace_lockscreen_title"
-    app:sudDescriptionText="@string/privatespace_lockscreen_summary"
+    app:sucHeaderText="@string/private_space_lockscreen_title"
+    app:sudDescriptionText="@string/private_space_lockscreen_summary"
     android:icon="@drawable/ic_lock">
     <com.google.android.setupdesign.view.FillContentLayout
         style="@style/SudContentFrame"
diff --git a/res/layout/privatespace_setup_success.xml b/res/layout/privatespace_setup_success.xml
index 00b6fec..e3e6b68 100644
--- a/res/layout/privatespace_setup_success.xml
+++ b/res/layout/privatespace_setup_success.xml
@@ -17,11 +17,11 @@
 <com.google.android.setupdesign.GlifLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/privatespace_setup_success"
+    android:id="@+id/private_space_setup_success"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:filterTouchesWhenObscured="true"
-    app:sucHeaderText="@string/privatespace_success_title"
-    app:sudDescriptionText="@string/privatespace_access_text"
+    app:sucHeaderText="@string/private_space_success_title"
+    app:sudDescriptionText="@string/private_space_access_text"
     android:icon="@drawable/ic_privatespace_done">
 </com.google.android.setupdesign.GlifLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6fbaddc..650dcfe 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1253,63 +1253,61 @@
     <!-- Title of the Alert Dialog when no screen lock is set [CHAR LIMIT=30] -->
     <string name="no_device_lock_title">Set a screen lock</string>
     <!-- Summary of the alert when no screen lock is set [CHAR LIMIT=90] -->
-    <string name="no_device_lock_summary">To use Private Space, set a screen lock on this device.</string>
+    <string name="no_device_lock_summary">To use your private space, set a screen lock on this device</string>
     <!-- Action label for dialog when no screen lock is set [CHAR LIMIT=30] -->
     <string name="no_device_lock_action_label">Set screen lock</string>
     <!-- Action label to cancel Alert dialog when no screen lock is set [CHAR LIMIT=30] -->
     <string name="no_device_lock_cancel">Cancel</string>
-    <!-- Action label to cancel Private Space Setup flow [CHAR LIMIT=30] -->
-    <string name="privatespace_cancel_label">Cancel</string>
-    <!-- Label for Private Space setup button to create Private Space [CHAR LIMIT=30] -->
-    <string name="privatespace_setup_button_label">Set up</string>
+    <!-- Action label to cancel private space Setup flow [CHAR LIMIT=30] -->
+    <string name="private_space_cancel_label">Cancel</string>
+    <!-- Label for private space setup button to create private space [CHAR LIMIT=30] -->
+    <string name="private_space_setup_button_label">Set up</string>
     <!-- Title for Private Space setup education screen. [CHAR LIMIT=50] -->
-    <string name="privatespace_setup_title">Set up Private Space</string>
-    <!-- Summary for the Private Space setup education screen. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_hide_apps_summary">Hide private apps in a secure space that only you can access</string>
-    <!-- Text shown in Private Space setup screen which explains how the Private Space works [CHAR LIMIT=50] -->
-    <string name="privatespace_how_title">How it works</string>
-    <!-- Text shown in Private Space setup screen which explains Private Space can be accessed from bottom of all apps list. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_access_bottom_text">You can access Private Space from the bottom of your apps list</string>
-    <!-- Text shown in Private Space setup screen which explains Private Space apps are protected by a lock. [CHAR LIMIT=60] -->
-    <string name="privatespace_protected_lock_text">Apps in Private Space are protected by a lock</string>
-    <!-- Text shown in Private Space setup screen which explains notifications from Private Space apps will not be shown when Private Space is locked. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_hidden_notifications_text">Notifications from apps in Private Space are hidden when it\u2019s locked</string>
-    <!-- Text shown in Private Space setup screen which explains that the permissions granted to Private Space apps will not be shown in settings when Private Space is locked. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_apps_permission_text">Private Space apps won\u2019t appear in permission manager, privacy dashboard, and other settings when Private Space is locked</string>
-    <!-- Text shown at the bottom in Private Space auto advancing  screens. [CHAR LIMIT=60] -->
-    <string name="privatespace_setting_up_text">Setting up Private Space\u2026</string>
-    <!-- Title for Private Space setup in auto advancing screen informing private space is hidden when locked. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_apps_hidden_title">Usage info for Private Space apps is hidden when it\u2019s locked</string>
-    <!-- Title for Private Space setup in auto advancing screen informing private space can be accessed from apps list. [CHAR LIMIT=60] -->
-    <string name="privatespace_access_from_apps_title">Access Private Space from your apps list</string>
-    <!-- Title for Private Space setup in auto advancing screen informing some system apps are already installed in Private Space. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_system_apps_installed_title">Some system apps are already installed in Private Space</string>
-    <!-- Title for Private Space creation error screen. [CHAR LIMIT=60] -->
-    <string name="privatespace_error_screen_title">Couldn\u2019t set up Private Space</string>
-    <!-- Summary for the Private Space creation error screen. [CHAR LIMIT=60] -->
-    <string name="privatespace_error_screen_summary">Try again now, or come back later</string>
+    <string name="private_space_setup_title">Set up a private space</string>
+    <!-- Summary for the private space setup education screen. [CHAR LIMIT=NONE] -->
+    <string name="private_space_hide_apps_summary">Keep private apps in a separate space that you can hide or lock</string>
+    <!-- Text shown in private space setup screen which explains how the private space works [CHAR LIMIT=50] -->
+    <string name="private_space_how_title">How it works</string>
+    <!-- Text shown in private space setup screen which explains private space can be accessed from bottom of all apps list. [CHAR LIMIT=NONE] -->
+    <string name="private_space_access_bottom_text">You can access your private space from the bottom of your apps list</string>
+    <!-- Text shown in private space setup screen which explains private space apps are protected by a lock. [CHAR LIMIT=60] -->
+    <string name="private_space_protected_lock_text">Apps in your private space are protected by a lock</string>
+    <!-- Text shown in private space setup screen which explains notifications from private space apps will not be shown when private space is locked. [CHAR LIMIT=NONE] -->
+    <string name="private_space_hidden_notifications_text">Notifications from apps in your private space are hidden when it\u2019s locked</string>
+    <!-- This is info text to help explain in private space setup screen that the permissions granted to private space apps will not be shown in settings when private space is locked. [CHAR LIMIT=NONE] -->
+    <string name="private_space_apps_permission_text">Apps in your private space won\'t appear in permission manager, privacy dashboard, and other settings when your private space is locked.\n\nYour private space can\'t be moved to a new device. You\'ll need to set up another private space if you want to use it on another device.\n\nAnyone that connects your device to a computer or installs harmful apps on your device may be able to access your private space.</string>
+    <!-- Text shown at the bottom in private space auto advancing  screens. [CHAR LIMIT=60] -->
+    <string name="private_space_setting_up_text">Setting up private space\u2026</string>
+    <!-- Title for private space setup in auto advancing screen informing private space notifications are hidden when locked. [CHAR LIMIT=NONE] -->
+    <string name="private_space_notifications_hidden_title">Notifications from apps in private space are hidden when it\u2019s locked</string>
+    <!-- Title for private space setup in auto advancing screen informing photos/files from private space can be shared when unlocked. [CHAR LIMIT=NONE] -->
+    <string name="private_space_share_photos_title">Unlock your space to share photos or files from private space apps</string>
+    <!-- Title for private space setup in auto advancing screen informing some system apps are already installed in private space. [CHAR LIMIT=NONE] -->
+    <string name="private_space_apps_installed_title">Some apps are already installed in your private space</string>
+    <!-- Title for private space creation error screen. [CHAR LIMIT=60] -->
+    <string name="private_space_error_screen_title">Couldn\u2019t set up private space</string>
     <!-- Label for button to retry creating private space again on creation error. [CHAR LIMIT=30] -->
-    <string name="privatespace_tryagain_label">Try Again</string>
-    <!-- Title for Private Space lock setup screen. [CHAR LIMIT=50] -->
-    <string name="privatespace_lockscreen_title">Use screen lock to unlock?</string>
-    <!-- Summary for the Private Space lock setup screen. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_lockscreen_summary">You can unlock Private Space the same way you unlock your device, or choose a different lock</string>
-    <!-- Action label to use existing device lock for Private Space. [CHAR LIMIT=50] -->
-    <string name="privatespace_use_screenlock_label">Use screen lock</string>
-    <!-- Label for Private Space lock setup button to choose a new lock. [CHAR LIMIT=50] -->
-    <string name="privatespace_set_lock_label">Choose new lock</string>
-    <!-- Title for Private Space setup success screen. [CHAR LIMIT=30] -->
-    <string name="privatespace_success_title">All set!</string>
-    <!-- Summary for the Private Space setup success screen. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_access_text">You can access Private Space from your apps list</string>
-    <!-- Label for Private Space done button to show a toast, finish setup and launch All apps [CHAR LIMIT=30] -->
-    <string name="privatespace_done_label">Done</string>
+    <string name="private_space_tryagain_label">Try Again</string>
+    <!-- Title for private space lock setup screen. [CHAR LIMIT=50] -->
+    <string name="private_space_lockscreen_title">Use screen lock to unlock private space?</string>
+    <!-- Summary for the private space lock setup screen. [CHAR LIMIT=NONE] -->
+    <string name="private_space_lockscreen_summary">You can unlock your private space the same way you unlock your device, or choose a different lock</string>
+    <!-- Action label to use existing device lock for private space. [CHAR LIMIT=50] -->
+    <string name="private_space_use_screenlock_label">Use screen lock</string>
+    <!-- Label for private space lock setup button to choose a new lock. [CHAR LIMIT=50] -->
+    <string name="private_space_set_lock_label">Choose new lock</string>
+    <!-- Title for private space setup success screen. [CHAR LIMIT=30] -->
+    <string name="private_space_success_title">All set!</string>
+    <!-- Summary for the private space setup success screen. [CHAR LIMIT=NONE] -->
+    <string name="private_space_access_text">To access your private space, go to your apps list then scroll down</string>
+    <!-- Label for private space done button to show a toast, finish setup and launch All apps [CHAR LIMIT=30] -->
+    <string name="private_space_done_label">Done</string>
     <!-- Toast to show on private space setup completion informing user to scroll down All apps to access private space. [CHAR LIMIT=60] -->
-    <string name="scrolldown_to_access">Scroll down to access Private Space</string>
-    <!-- Title for Private Space account login error screen. [CHAR LIMIT=60] -->
-    <string name="privatespace_retry_signin_title">Sign in to set up Private Space</string>
-    <!-- Summary for the Private Space account login error screen. [CHAR LIMIT=NONE] -->
-    <string name="privatespace_retry_summary">You need to sign in to an account to set up Private Space</string>
+    <string name="private_space_scrolldown_to_access">Scroll down to find private space</string>
+    <!-- Title for private space account login error screen. [CHAR LIMIT=60] -->
+    <string name="private_space_retry_signin_title">Sign in to set up a private space</string>
+    <!-- Summary for the private space account login error screen. [CHAR LIMIT=NONE] -->
+    <string name="private_space_retry_summary">You need to sign in to an account to set up a private space</string>
     <!-- private space lock setup screen title. This title is asking the user to choose a type of screen lock (such as a pattern, PIN, or password) that they need to enter to unlock private space. [CHAR LIMIT=60] -->
     <string name="private_space_lock_setup_title">Choose a lock for your private space</string>
     <!-- private space lock setup screen description [CHAR LIMIT=NONE] -->
@@ -9479,6 +9477,14 @@
     <!-- Description of allowing overlay setting [CHAR LIMIT=NONE] -->
     <string name="allow_overlay_description">Allow this app to display on top of other apps you\u2019re using. This app will be able to see where you tap or change what\u2019s displayed on the screen.</string>
 
+    <!-- Change Media Output settings -->
+    <!-- Title for Change Media Output screen [CHAR LIMIT=30] -->
+    <string name="media_routing_control_title">Change media output</string>
+    <!-- Label for setting which controls whether app can change media outputs for other apps [CHAR LIMIT=45] -->
+    <string name="allow_media_routing_control">Allow app to switch media output</string>
+    <!-- Description for allowing change media output setting [CHAR LIMIT=NONE] -->
+    <string name="allow_media_routing_description">Allow this app to choose which connected device plays audio or video from other apps. If allowed, this app can access a list of available devices such as headphones and speakers and choose which output device is used to stream or cast audio or video.</string>
+
     <!-- Manager External Storage settings title [CHAR LIMIT=40] -->
     <string name="manage_external_storage_title">All files access</string>
     <!-- Label for a setting which controls whether an app can manage external storage [CHAR LIMIT=45] -->
diff --git a/res/xml/privatespace_setup_education_entries.xml b/res/xml/privatespace_setup_education_entries.xml
deleted file mode 100644
index 18fb19d..0000000
--- a/res/xml/privatespace_setup_education_entries.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2023 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.
-  -->
-
-<ItemGroup
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <Item
-        android:enabled="false"
-        android:icon="@drawable/ic_apps"
-        android:title="@string/privatespace_access_bottom_text"
-        app:sudIconTint="?android:attr/textColorSecondary"/>
-
-    <Item
-        android:enabled="false"
-        android:icon="@drawable/ic_lock_24dp"
-        android:title="@string/privatespace_protected_lock_text"
-        app:sudIconTint="?android:attr/textColorSecondary"/>
-
-    <Item
-        android:enabled="false"
-        android:icon="@drawable/ic_notifications"
-        android:title="@string/privatespace_hidden_notifications_text"
-        app:sudIconTint="?android:attr/textColorSecondary"/>
-</ItemGroup>
diff --git a/res/xml/special_access.xml b/res/xml/special_access.xml
index 3f3d75d..743a122 100644
--- a/res/xml/special_access.xml
+++ b/res/xml/special_access.xml
@@ -95,6 +95,13 @@
         settings:controller="com.android.settings.applications.specialaccess.notificationaccess.NotificationAccessController" />
 
     <Preference
+        android:key="media_routing_control"
+        android:title="@string/media_routing_control_title"
+        android:order="-1100"
+        settings:controller="com.android.settings.applications.specialaccess.MediaRoutingControlPreferenceController" >
+    </Preference>
+
+    <Preference
         android:key="use_full_screen_intent"
         android:title="@string/full_screen_intent_title"
         settings:controller="com.android.settings.spa.app.specialaccess.UseFullScreenIntentPreferenceController" />
diff --git a/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceController.java b/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceController.java
new file mode 100644
index 0000000..72011ba
--- /dev/null
+++ b/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceController.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 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.settings.applications.specialaccess;
+
+import android.Manifest;
+import android.content.Context;
+import android.text.TextUtils;
+
+import androidx.preference.Preference;
+
+import com.android.media.flags.Flags;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.spa.SpaActivity;
+import com.android.settings.spa.app.specialaccess.MediaRoutingControlAppListProvider;
+
+/**
+ * This controller manages features availability for special app access for
+ * {@link Manifest.permission#MEDIA_ROUTING_CONTROL} permission.
+ */
+public class MediaRoutingControlPreferenceController extends BasePreferenceController {
+    public MediaRoutingControlPreferenceController(Context context, String preferenceKey) {
+        super(context, preferenceKey);
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return Flags.enablePrivilegedRoutingForMediaRoutingControl()
+                ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+    }
+
+    @Override
+    public boolean handlePreferenceTreeClick(Preference preference) {
+        if (TextUtils.equals(preference.getKey(), mPreferenceKey)) {
+            SpaActivity.startSpaActivity(
+                    mContext, MediaRoutingControlAppListProvider.INSTANCE.getAppListRoute());
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
index a70d7d8..3321d50 100644
--- a/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
+++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectFragment.java
@@ -57,6 +57,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Base fragment class for profile settings.
@@ -295,8 +296,7 @@
                 personalFragmentConstructor,
                 workFragmentConstructor,
                 privateFragmentConstructor,
-                new PrivateSpaceInfoProvider() {},
-                new ManagedProfileInfoProvider() {});
+                new PrivateSpaceInfoProvider() {});
     }
 
     /**
@@ -309,36 +309,35 @@
             FragmentConstructor personalFragmentConstructor,
             FragmentConstructor workFragmentConstructor,
             FragmentConstructor privateFragmentConstructor,
-            PrivateSpaceInfoProvider privateSpaceInfoProvider,
-            ManagedProfileInfoProvider managedProfileInfoProvider) {
+            PrivateSpaceInfoProvider privateSpaceInfoProvider) {
         Fragment[] result = new Fragment[0];
         ArrayList<Fragment> fragments = new ArrayList<>();
 
         try {
-            final Bundle personalOnly = bundle != null ? bundle : new Bundle();
-            personalOnly.putInt(EXTRA_PROFILE, ProfileType.PERSONAL);
-            final Fragment personalFragment =
-                    personalFragmentConstructor.constructAndGetFragment();
-            personalFragment.setArguments(personalOnly);
-            fragments.add(personalFragment);
+            UserManager userManager = context.getSystemService(UserManager.class);
+            List<UserInfo> userInfos = userManager.getProfiles(UserHandle.myUserId());
 
-            if (managedProfileInfoProvider.isManagedProfilePresent(context)) {
-                final Bundle workOnly = bundle != null ? bundle.deepCopy() : new Bundle();
-                workOnly.putInt(EXTRA_PROFILE, ProfileType.WORK);
-                final Fragment workFragment =
-                        workFragmentConstructor.constructAndGetFragment();
-                workFragment.setArguments(workOnly);
-                fragments.add(workFragment);
-            }
-
-            if (Flags.allowPrivateProfile()
-                    && !privateSpaceInfoProvider.isPrivateSpaceLocked(context)) {
-                final Bundle privateOnly = bundle != null ? bundle.deepCopy() : new Bundle();
-                privateOnly.putInt(EXTRA_PROFILE, ProfileType.PRIVATE);
-                final Fragment privateFragment =
-                        privateFragmentConstructor.constructAndGetFragment();
-                privateFragment.setArguments(privateOnly);
-                fragments.add(privateFragment);
+            for (UserInfo userInfo : userInfos) {
+                if (userInfo.getUserHandle().isSystem()) {
+                    fragments.add(createAndGetFragment(
+                            ProfileType.PERSONAL,
+                            bundle != null ? bundle : new Bundle(),
+                            personalFragmentConstructor));
+                } else if (userInfo.isManagedProfile()) {
+                    fragments.add(createAndGetFragment(
+                            ProfileType.WORK,
+                            bundle != null ? bundle.deepCopy() : new Bundle(),
+                            workFragmentConstructor));
+                } else if (Flags.allowPrivateProfile() && userInfo.isPrivateProfile()) {
+                    if (!privateSpaceInfoProvider.isPrivateSpaceLocked(context)) {
+                        fragments.add(createAndGetFragment(
+                                ProfileType.PRIVATE,
+                                bundle != null ? bundle.deepCopy() : new Bundle(),
+                                privateFragmentConstructor));
+                    }
+                } else {
+                    Log.d(TAG, "Not showing tab for unsupported user");
+                }
             }
 
             result = new Fragment[fragments.size()];
@@ -350,6 +349,14 @@
         return result;
     }
 
+    private static Fragment createAndGetFragment(
+            @ProfileType int profileType, Bundle bundle, FragmentConstructor fragmentConstructor) {
+        bundle.putInt(EXTRA_PROFILE, profileType);
+        final Fragment fragment = fragmentConstructor.constructAndGetFragment();
+        fragment.setArguments(bundle);
+        return fragment;
+    }
+
     interface FragmentConstructor {
         Fragment constructAndGetFragment();
     }
@@ -360,13 +367,6 @@
         }
     }
 
-    interface ManagedProfileInfoProvider {
-        default boolean isManagedProfilePresent(Context context) {
-            return Utils.doesProfileOfTypeExists(
-                    context.getSystemService(UserManager.class), ProfileType.WORK);
-        }
-    }
-
     static class ViewPagerAdapter extends FragmentStateAdapter {
 
         private final Fragment[] mChildFragments;
diff --git a/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java b/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
index 77d57f7..9e905f2 100644
--- a/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
+++ b/src/com/android/settings/privatespace/AutoAdvanceSetupFragment.java
@@ -52,16 +52,17 @@
     private static final String TITLE_INDEX = "title_index";
     private static final int DELAY_BETWEEN_SCREENS = 5000; // 5 seconds in millis
     private static final int ANIMATION_DURATION_MILLIS = 500;
+    private static final int HEADER_TEXT_MAX_LINES = 4;
     private GlifLayout mRootView;
     private Handler mHandler;
     private int mScreenTitleIndex;
     private static final List<Pair<Integer, Integer>> HEADER_IMAGE_PAIRS =
             ImmutableList.of(
-                    new Pair(R.string.privatespace_apps_hidden_title,
+                    new Pair(R.string.private_space_notifications_hidden_title,
                             R.drawable.privatespace_setup_flow_placeholder),
-                    new Pair(R.string.privatespace_access_from_apps_title,
+                    new Pair(R.string.private_space_share_photos_title,
                             R.drawable.privatespace_setup_flow_placeholder),
-                    new Pair(R.string.privatespace_system_apps_installed_title,
+                    new Pair(R.string.private_space_apps_installed_title,
                             R.drawable.privatespace_setup_flow_placeholder));
 
     private Runnable mUpdateScreenResources =
@@ -116,6 +117,7 @@
         mRootView =
                 (GlifLayout)
                         inflater.inflate(R.layout.privatespace_advancing_screen, container, false);
+        mRootView.getHeaderTextView().setMaxLines(HEADER_TEXT_MAX_LINES);
         updateHeaderAndImage();
         mHandler = new Handler(Looper.getMainLooper());
         mHandler.postDelayed(mUpdateScreenResources, DELAY_BETWEEN_SCREENS);
diff --git a/src/com/android/settings/privatespace/PrivateProfileCreationError.java b/src/com/android/settings/privatespace/PrivateProfileCreationError.java
index 91c35a3..74beef4 100644
--- a/src/com/android/settings/privatespace/PrivateProfileCreationError.java
+++ b/src/com/android/settings/privatespace/PrivateProfileCreationError.java
@@ -47,14 +47,14 @@
         final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
         mixin.setPrimaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_tryagain_label)
+                        .setText(R.string.private_space_tryagain_label)
                         .setListener(onTryAgain())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
                         .build());
         mixin.setSecondaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_cancel_label)
+                        .setText(R.string.private_space_cancel_label)
                         .setListener(onCancel())
                         .setButtonType(FooterButton.ButtonType.CANCEL)
                         .setTheme(
diff --git a/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java b/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java
index ac3518e..e445a7f 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceAccountLoginError.java
@@ -51,7 +51,7 @@
         final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
         mixin.setPrimaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_tryagain_label)
+                        .setText(R.string.private_space_tryagain_label)
                         .setListener(nextScreen())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
diff --git a/src/com/android/settings/privatespace/PrivateSpaceEducation.java b/src/com/android/settings/privatespace/PrivateSpaceEducation.java
index 887854b..eb562af 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceEducation.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceEducation.java
@@ -50,14 +50,14 @@
         final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
         mixin.setPrimaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_setup_button_label)
+                        .setText(R.string.private_space_setup_button_label)
                         .setListener(onSetup())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
                         .build());
         mixin.setSecondaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_cancel_label)
+                        .setText(R.string.private_space_cancel_label)
                         .setListener(onCancel())
                         .setButtonType(FooterButton.ButtonType.CANCEL)
                         .setTheme(
diff --git a/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java b/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
index dd4bc7f..a256ea6 100644
--- a/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
+++ b/src/com/android/settings/privatespace/PrivateSpaceSetLockFragment.java
@@ -60,14 +60,14 @@
         final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
         mixin.setPrimaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_use_screenlock_label)
+                        .setText(R.string.private_space_use_screenlock_label)
                         .setListener(onClickUse())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
                         .build());
         mixin.setSecondaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_set_lock_label)
+                        .setText(R.string.private_space_set_lock_label)
                         .setListener(onClickNewLock())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(
diff --git a/src/com/android/settings/privatespace/SetupSuccessFragment.java b/src/com/android/settings/privatespace/SetupSuccessFragment.java
index fc2ce46..0b1b9d9 100644
--- a/src/com/android/settings/privatespace/SetupSuccessFragment.java
+++ b/src/com/android/settings/privatespace/SetupSuccessFragment.java
@@ -55,7 +55,7 @@
         final FooterBarMixin mixin = rootView.getMixin(FooterBarMixin.class);
         mixin.setPrimaryButton(
                 new FooterButton.Builder(getContext())
-                        .setText(R.string.privatespace_done_label)
+                        .setText(R.string.private_space_done_label)
                         .setListener(onClickNext())
                         .setButtonType(FooterButton.ButtonType.NEXT)
                         .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
@@ -104,6 +104,7 @@
     }
 
     private void accessPrivateSpaceToast() {
-        Toast.makeText(getContext(), R.string.scrolldown_to_access, Toast.LENGTH_SHORT).show();
+        Toast.makeText(getContext(), R.string.private_space_scrolldown_to_access,
+                Toast.LENGTH_SHORT).show();
     }
 }
diff --git a/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java b/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java
index 04101b2..54b0374 100644
--- a/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java
+++ b/src/com/android/settings/privatespace/onelock/UseOneLockControllerSwitch.java
@@ -192,7 +192,7 @@
         new AlertDialog.Builder(mContext)
                   .setMessage(R.string.private_space_new_lock_title)
                   .setPositiveButton(
-                            R.string.privatespace_set_lock_label,
+                            R.string.private_space_set_lock_label,
                             (dialog, which) -> {
                                 Intent intent = new Intent(mContext,
                                           PrivateProfileContextHelperActivity.class);
@@ -201,7 +201,7 @@
                                           UNUNIFY_PRIVATE_LOCK_FROM_DEVICE_REQUEST,
                                           /*Options*/ null, mUserHandle);
                             })
-                  .setNegativeButton(R.string.privatespace_cancel_label,
+                  .setNegativeButton(R.string.private_space_cancel_label,
                             (DialogInterface dialog, int which) -> {
                                 mUnifyProfile.setChecked(true);
                                 dialog.dismiss();
diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
index 7ab836b..a6cf5cc 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -32,6 +32,7 @@
 import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
 import com.android.settings.spa.app.specialaccess.LongBackgroundTasksAppListProvider
 import com.android.settings.spa.app.specialaccess.MediaManagementAppsAppListProvider
+import com.android.settings.spa.app.specialaccess.MediaRoutingControlAppListProvider
 import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider
 import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider
 import com.android.settings.spa.app.specialaccess.PictureInPictureListProvider
@@ -64,6 +65,7 @@
             AllFilesAccessAppListProvider,
             DisplayOverOtherAppsAppListProvider,
             MediaManagementAppsAppListProvider,
+            MediaRoutingControlAppListProvider,
             ModifySystemSettingsAppListProvider,
             UseFullScreenIntentAppListProvider,
             PictureInPictureListProvider,
diff --git a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
index c990927..3e48aa5 100644
--- a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
+++ b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt
@@ -18,6 +18,7 @@
 
 import android.Manifest
 import android.app.AlarmManager
+import android.app.AppOpsManager
 import android.app.compat.CompatChanges
 import android.app.settings.SettingsEnums
 import android.content.Context
@@ -56,6 +57,7 @@
     override val pageTitleResId = R.string.alarms_and_reminders_title
     override val switchTitleResId = R.string.alarms_and_reminders_switch_title
     override val footerResId = R.string.alarms_and_reminders_footer_title
+    override val enhancedConfirmationKey: String = AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM
 
     override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
         userIdFlow.map { userId ->
diff --git a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
index 7f63e38..dc98330 100644
--- a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
+++ b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt
@@ -18,6 +18,7 @@
 
 import android.Manifest
 import android.app.AppGlobals
+import android.app.AppOpsManager
 import android.app.AppOpsManager.MODE_DEFAULT
 import android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES
 import android.content.Context
@@ -55,6 +56,7 @@
             UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
             UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY,
         )
+    override val enhancedConfirmationKey: String = AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES
 
     override fun transformItem(app: ApplicationInfo) =
         InstallUnknownAppsRecord(
diff --git a/src/com/android/settings/spa/app/specialaccess/MediaRoutingControl.kt b/src/com/android/settings/spa/app/specialaccess/MediaRoutingControl.kt
new file mode 100644
index 0000000..91c4928
--- /dev/null
+++ b/src/com/android/settings/spa/app/specialaccess/MediaRoutingControl.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 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.settings.spa.app.specialaccess
+
+import android.Manifest
+import android.app.AppOpsManager
+import android.app.role.RoleManager
+import android.app.settings.SettingsEnums
+import android.companion.AssociationRequest
+import android.content.Context
+import com.android.settings.R
+import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
+import com.android.settingslib.spaprivileged.template.app.AppOpPermissionListModel
+import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
+import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider
+
+object MediaRoutingControlAppListProvider : TogglePermissionAppListProvider {
+    override val permissionType = "MediaRoutingControl"
+    override fun createModel(context: Context) = MediaRoutingControlAppsListModel(context)
+}
+
+class MediaRoutingControlAppsListModel(context: Context) : AppOpPermissionListModel(context) {
+    override val pageTitleResId = R.string.media_routing_control_title
+    override val switchTitleResId = R.string.allow_media_routing_control
+    override val footerResId = R.string.allow_media_routing_description
+    override val appOp = AppOpsManager.OP_MEDIA_ROUTING_CONTROL
+    override val permission = Manifest.permission.MEDIA_ROUTING_CONTROL
+    override val setModeByUid = true
+    private val roleManager = context.getSystemService(RoleManager::class.java)
+
+    override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) {
+        super.setAllowed(record, newAllowed)
+        logPermissionToggleAction(newAllowed)
+    }
+
+    override fun isChangeable(record: AppOpPermissionRecord): Boolean {
+        return super.isChangeable(record) && (this.roleManager
+                ?.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH)
+                ?.contains(record.app.packageName) == true)
+    }
+
+    private fun logPermissionToggleAction(newAllowed: Boolean) {
+        featureFactory.metricsFeatureProvider.action(
+                context,
+                SettingsEnums.MEDIA_ROUTING_CONTROL,
+                if (newAllowed)
+                    VALUE_LOGGING_ALLOWED
+                else
+                    VALUE_LOGGING_DENIED
+        )
+    }
+
+    companion object {
+        const val VALUE_LOGGING_ALLOWED = 1
+        const val VALUE_LOGGING_DENIED = 0
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
index cd615919..fe8f103 100644
--- a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
+++ b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt
@@ -16,6 +16,7 @@
 
 package com.android.settings.spa.app.specialaccess
 
+import android.app.AppOpsManager
 import android.app.AppOpsManager.OP_PICTURE_IN_PICTURE
 import android.content.Context
 import android.content.pm.ActivityInfo
@@ -53,6 +54,7 @@
     override val pageTitleResId = R.string.picture_in_picture_title
     override val switchTitleResId = R.string.picture_in_picture_app_detail_switch
     override val footerResId = R.string.picture_in_picture_app_detail_summary
+    override val enhancedConfirmationKey: String = AppOpsManager.OPSTR_PICTURE_IN_PICTURE
 
     private val packageManager = context.packageManager
 
diff --git a/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt b/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
index fb05a38..0285b74 100644
--- a/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
+++ b/src/com/android/settings/spa/app/specialaccess/SpecialAppAccess.kt
@@ -61,6 +61,7 @@
                 AllFilesAccessAppListProvider,
                 DisplayOverOtherAppsAppListProvider,
                 MediaManagementAppsAppListProvider,
+                MediaRoutingControlAppListProvider,
                 ModifySystemSettingsAppListProvider,
                 UseFullScreenIntentAppListProvider,
                 PictureInPictureListProvider,
diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceControllerTest.java
new file mode 100644
index 0000000..20a1d04
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/applications/specialaccess/MediaRoutingControlPreferenceControllerTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2023 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.settings.applications.specialaccess;
+
+import static com.android.settingslib.spa.framework.util.SpaIntentKt.KEY_DESTINATION;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.preference.Preference;
+
+import com.android.media.flags.Flags;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.spa.SpaActivity;
+import com.android.settings.spa.app.specialaccess.MediaRoutingControlAppListProvider;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class MediaRoutingControlPreferenceControllerTest {
+
+    private static final String PREFERENCE_KEY = "test_preference_key";
+    private static final String DIFFERENT_PREFERENCE_KEY = "other_preference_key";
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Mock
+    private Context mContext;
+    @Mock
+    private PackageManager mPackageManager;
+
+    private MediaRoutingControlPreferenceController mTestController;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getPackageManager()).thenReturn(mPackageManager);
+        mTestController = new MediaRoutingControlPreferenceController(
+                mContext, PREFERENCE_KEY);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
+    public void getAvailabilityStatus_withFlagEnabled_shouldReturnTrue() {
+        assertThat(mTestController.getAvailabilityStatus())
+                .isEqualTo(BasePreferenceController.AVAILABLE);
+    }
+
+    @Test
+    @RequiresFlagsDisabled(Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
+    public void getAvailabilityStatus_withFlagDisabled_shouldReturnFalse() {
+        assertThat(mTestController.getAvailabilityStatus())
+                .isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE);
+    }
+
+    @Test
+    public void handlePreferenceTreeClick_withDifferentPreference_shouldReturnFalse() {
+        Preference preference = mock(Preference.class);
+        when(preference.getKey()).thenReturn(DIFFERENT_PREFERENCE_KEY);
+
+        assertThat(mTestController.handlePreferenceTreeClick(preference)).isFalse();
+    }
+
+    @Test
+    public void handlePreferenceTreeClick_withMediaRoutingPreference_shouldReturnTrue() {
+        Preference preference = mock(Preference.class);
+        when(preference.getKey()).thenReturn(PREFERENCE_KEY);
+
+        assertThat(mTestController.handlePreferenceTreeClick(preference)).isTrue();
+    }
+
+    @Test
+    public void handlePreferenceTreeClick_withDifferentPreference_shouldNotStartSpaActivity() {
+        Preference preference = mock(Preference.class);
+        when(preference.getKey()).thenReturn(DIFFERENT_PREFERENCE_KEY);
+
+        mTestController.handlePreferenceTreeClick(preference);
+
+        verify(mContext, never()).startActivity(any(Intent.class));
+    }
+
+    @Test
+    public void handlePreferenceTreeClick_withMediaRoutingPreference_shouldStartSpaActivity() {
+        Preference preference = mock(Preference.class);
+        when(preference.getKey()).thenReturn(PREFERENCE_KEY);
+
+        mTestController.handlePreferenceTreeClick(preference);
+
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mContext).startActivity(intentCaptor.capture());
+        final Intent intent = intentCaptor.getValue();
+        assertThat(intent.getComponent().getClassName()).isEqualTo(SpaActivity.class.getName());
+        assertThat(intent.getStringExtra(KEY_DESTINATION)).isEqualTo(
+                MediaRoutingControlAppListProvider.INSTANCE.getAppListRoute());
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
index 302c8f3..3df6449 100644
--- a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectFragmentTest.java
@@ -17,6 +17,9 @@
 package com.android.settings.dashboard.profileselector;
 
 import static android.content.Intent.EXTRA_USER_ID;
+import static android.os.UserManager.USER_TYPE_FULL_SYSTEM;
+import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
+import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
 
 import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.EXTRA_PROFILE;
 import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.PERSONAL_TAB;
@@ -30,9 +33,11 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.UserInfo;
 import android.os.Bundle;
 import android.os.Flags;
 import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.ArraySet;
 
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
@@ -51,7 +56,9 @@
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 @RunWith(RobolectricTestRunner.class)
@@ -60,6 +67,9 @@
         com.android.settings.testutils.shadow.ShadowFragment.class,
 })
 public class ProfileSelectFragmentTest {
+    private static final String PRIMARY_USER_NAME = "primary";
+    private static final String MANAGED_USER_NAME = "managed";
+    private static final String PRIVATE_USER_NAME = "private";
 
     private Context mContext;
     private TestProfileSelectFragment mFragment;
@@ -151,6 +161,8 @@
     @Test
     public void testGetFragments_whenOnlyPersonal_returnsOneFragment() {
         mSetFlagsRule.disableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
                 null /* bundle */,
@@ -162,6 +174,10 @@
 
     @Test
     public void testGetFragments_whenPrivateDisabled_returnsOneFragment() {
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+        mUserManager.addProfile(
+                new UserInfo(11, PRIVATE_USER_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE));
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
                 null /* bundle */,
@@ -173,12 +189,6 @@
                     public boolean isPrivateSpaceLocked(Context context) {
                         return true;
                     }
-                },
-                new ProfileSelectFragment.ManagedProfileInfoProvider() {
-                    @Override
-                    public boolean isManagedProfilePresent(Context context) {
-                        return false;
-                    }
                 });
         assertThat(fragments).hasLength(1);
     }
@@ -186,6 +196,10 @@
     @Test
     public void testGetFragments_whenPrivateEnabled_returnsTwoFragments() {
         mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+        mUserManager.addProfile(
+                new UserInfo(11, PRIVATE_USER_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE));
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
                 null /* bundle */,
@@ -197,12 +211,6 @@
                     public boolean isPrivateSpaceLocked(Context context) {
                         return false;
                     }
-                },
-                new ProfileSelectFragment.ManagedProfileInfoProvider() {
-                    @Override
-                    public boolean isManagedProfilePresent(Context context) {
-                        return false;
-                    }
                 });
         assertThat(fragments).hasLength(2);
     }
@@ -210,6 +218,10 @@
     @Test
     public void testGetFragments_whenManagedProfile_returnsTwoFragments() {
         mSetFlagsRule.disableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+        mUserManager.addProfile(
+                new UserInfo(10, MANAGED_USER_NAME, null, 0, USER_TYPE_PROFILE_MANAGED));
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
                 null /* bundle */,
@@ -221,12 +233,6 @@
                     public boolean isPrivateSpaceLocked(Context context) {
                         return false;
                     }
-                },
-                new ProfileSelectFragment.ManagedProfileInfoProvider() {
-                    @Override
-                    public boolean isManagedProfilePresent(Context context) {
-                        return true;
-                    }
                 });
         assertThat(fragments).hasLength(2);
     }
@@ -234,6 +240,12 @@
     @Test
     public void testGetFragments_whenAllProfiles_returnsThreeFragments() {
         mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+        mUserManager.addProfile(
+                new UserInfo(10, MANAGED_USER_NAME, null, 0, USER_TYPE_PROFILE_MANAGED));
+        mUserManager.addProfile(
+                new UserInfo(11, PRIVATE_USER_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE));
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
                 null /* bundle */,
@@ -245,12 +257,6 @@
                     public boolean isPrivateSpaceLocked(Context context) {
                         return false;
                     }
-                },
-                new ProfileSelectFragment.ManagedProfileInfoProvider() {
-                    @Override
-                    public boolean isManagedProfilePresent(Context context) {
-                        return true;
-                    }
                 });
         assertThat(fragments).hasLength(3);
     }
@@ -258,6 +264,12 @@
     @Test
     public void testGetFragments_whenAvailableBundle_returnsFragmentsWithCorrectBundles() {
         mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_PRIVATE_PROFILE);
+        mUserManager.addProfile(
+                new UserInfo(0, PRIMARY_USER_NAME, null, 0, USER_TYPE_FULL_SYSTEM));
+        mUserManager.addProfile(
+                new UserInfo(10, MANAGED_USER_NAME, null, 0, USER_TYPE_PROFILE_MANAGED));
+        mUserManager.addProfile(
+                new UserInfo(11, PRIVATE_USER_NAME, null, 0, USER_TYPE_PROFILE_PRIVATE));
         Bundle bundle = new Bundle();
         Fragment[] fragments = ProfileSelectFragment.getFragments(
                 mContext,
@@ -270,20 +282,21 @@
                     public boolean isPrivateSpaceLocked(Context context) {
                         return false;
                     }
-                },
-                new ProfileSelectFragment.ManagedProfileInfoProvider() {
-                    @Override
-                    public boolean isManagedProfilePresent(Context context) {
-                        return true;
-                    }
                 });
         assertThat(fragments).hasLength(3);
-        assertThat(fragments[0].getArguments().getInt(EXTRA_PROFILE))
-                .isEqualTo(ProfileSelectFragment.ProfileType.PERSONAL);
-        assertThat(fragments[1].getArguments().getInt(EXTRA_PROFILE))
-                .isEqualTo(ProfileSelectFragment.ProfileType.WORK);
-        assertThat(fragments[2].getArguments().getInt(EXTRA_PROFILE))
-                .isEqualTo(ProfileSelectFragment.ProfileType.PRIVATE);
+
+        List<Integer> foundProfileTypesList = new ArrayList<>();
+        for (Fragment fragment : fragments) {
+            foundProfileTypesList.add(fragment.getArguments().getInt(EXTRA_PROFILE));
+        }
+
+        assertThat(foundProfileTypesList).hasSize(3);
+
+        Set<Integer> foundProfileTypes = new ArraySet<>(foundProfileTypesList);
+        assertThat(foundProfileTypes).containsExactly(
+                ProfileSelectFragment.ProfileType.PERSONAL,
+                ProfileSelectFragment.ProfileType.WORK,
+                ProfileSelectFragment.ProfileType.PRIVATE);
     }
 
     public static class TestProfileSelectFragment extends ProfileSelectFragment {
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java
index ff8ea62..7c1650d2 100644
--- a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceControllerTest.java
@@ -34,6 +34,7 @@
 import com.android.settings.testutils.FakeFeatureFactory;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
@@ -75,6 +76,7 @@
         assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
     }
 
+    @Ignore("b/315267179")
     @Test
     public void getSummary_available_returnExpectedDate() {
         when(mFactory.batterySettingsFeatureProvider.isFirstUseDateAvailable(eq(mContext),
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java
index 608ce00..e50aa1c 100644
--- a/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceControllerTest.java
@@ -34,6 +34,7 @@
 import com.android.settings.testutils.FakeFeatureFactory;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
@@ -76,6 +77,7 @@
         assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
     }
 
+    @Ignore("b/315267179")
     @Test
     public void getSummary_available_returnExpectedDate() {
         when(mFactory.batterySettingsFeatureProvider.isManufactureDateAvailable(eq(mContext),
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java
index 91d8c7d..bdf81e4 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryTipsCardPreferenceTest.java
@@ -40,6 +40,7 @@
 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -155,6 +156,7 @@
                         "ScreenTimeoutAnomaly");
     }
 
+    @Ignore("b/313582999")
     @Test
     public void onClick_mainBtnOfAppsAnomaly_selectHighlightSlot() {
         final PowerAnomalyEvent appsAnomaly = BatteryTestUtils.createAppAnomalyEvent();
@@ -176,6 +178,7 @@
                 .action(mContext, SettingsEnums.ACTION_BATTERY_TIPS_CARD_ACCEPT, "AppAnomaly");
     }
 
+    @Ignore("b/313582999")
     @Test
     public void onClick_dismissBtnOfAppsAnomaly_keepHighlightSlotIndex() {
         final PowerAnomalyEvent appsAnomaly = BatteryTestUtils.createAppAnomalyEvent();
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt
new file mode 100644
index 0000000..5f0f2c6
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/MediaRoutingControlTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2023 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.settings.spa.app.specialaccess
+
+import android.Manifest
+import android.app.AppOpsManager
+import android.app.role.RoleManager
+import android.app.settings.SettingsEnums
+import android.companion.AssociationRequest
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import androidx.lifecycle.MutableLiveData
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settings.testutils.FakeFeatureFactory
+import com.android.settingslib.spaprivileged.model.app.IAppOpsController
+import com.android.settingslib.spaprivileged.template.app.AppOpPermissionRecord
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class MediaRoutingControlTest {
+    @get:Rule
+    val mockito: MockitoRule = MockitoJUnit.rule()
+
+    @Spy
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    private lateinit var listModel: MediaRoutingControlAppsListModel
+
+    @Mock
+    private lateinit var mockRoleManager: RoleManager
+
+    private val fakeFeatureFactory = FakeFeatureFactory()
+    private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider
+
+    @Before
+    fun setUp() {
+        whenever(context.getSystemService(RoleManager::class.java))
+                .thenReturn(mockRoleManager)
+        listModel = MediaRoutingControlAppsListModel(context)
+    }
+
+    @Test
+    fun modelResourceIdAndProperties() {
+        assertThat(listModel.pageTitleResId).isEqualTo(R.string.media_routing_control_title)
+        assertThat(listModel.switchTitleResId).isEqualTo(R.string.allow_media_routing_control)
+        assertThat(listModel.footerResId).isEqualTo(R.string.allow_media_routing_description)
+        assertThat(listModel.appOp).isEqualTo(AppOpsManager.OP_MEDIA_ROUTING_CONTROL)
+        assertThat(listModel.permission).isEqualTo(Manifest.permission.MEDIA_ROUTING_CONTROL)
+        assertThat(listModel.setModeByUid).isTrue()
+    }
+
+    @Test
+    fun setAllowed_callWithNewStatusAsTrue_shouldChangeAppControllerModeToAllowed() {
+        val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController = fakeAppOpController,
+                )
+
+        listModel.setAllowed(permissionRequestedRecord, true)
+
+        assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ALLOWED)
+    }
+
+    @Test
+    fun setAllowed_callWithNewStatusAsTrue_shouldLogPermissionToggleActionAsAllowed() {
+        val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController = fakeAppOpController,
+                )
+
+        listModel.setAllowed(permissionRequestedRecord, true)
+
+        verify(metricsFeatureProvider)
+                .action(context, SettingsEnums.MEDIA_ROUTING_CONTROL, VALUE_LOGGING_ALLOWED)
+    }
+
+    @Test
+    fun setAllowed_callWithNewStatusAsFalse_shouldChangeAppControllerModeToErrored() {
+        val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController = fakeAppOpController,
+                )
+
+        listModel.setAllowed(permissionRequestedRecord, false)
+
+        assertThat(fakeAppOpController.getMode()).isEqualTo(AppOpsManager.MODE_ERRORED)
+    }
+
+    @Test
+    fun setAllowed_callWithNewStatusAsFalse_shouldLogPermissionToggleActionAsDenied() {
+        val fakeAppOpController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT)
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController = fakeAppOpController,
+                )
+
+        listModel.setAllowed(permissionRequestedRecord, false)
+
+        verify(metricsFeatureProvider)
+                .action(context, SettingsEnums.MEDIA_ROUTING_CONTROL, VALUE_LOGGING_DENIED)
+    }
+
+    @Test
+    fun isChangeable_permissionRequestedByAppAndWatchCompanionRoleAssigned_shouldReturnTrue() {
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController =
+                            FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+                )
+        whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH))
+                .thenReturn(listOf(PACKAGE_NAME))
+
+        val isSpecialAccessChangeable = listModel.isChangeable(permissionRequestedRecord)
+
+        assertThat(isSpecialAccessChangeable).isTrue()
+    }
+
+    @Test
+    fun isChangeable_permissionNotRequestedByAppButWatchCompanionRoleAssigned_shouldReturnFalse() {
+        val permissionNotRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = false,
+                        hasRequestBroaderPermission = false,
+                        appOpsController =
+                            FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+                )
+        whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH))
+                .thenReturn(listOf(PACKAGE_NAME))
+
+        val isSpecialAccessChangeable = listModel.isChangeable(permissionNotRequestedRecord)
+
+        assertThat(isSpecialAccessChangeable).isFalse()
+    }
+
+    @Test
+    fun isChangeable_permissionRequestedByAppButWatchCompanionRoleNotAssigned_shouldReturnFalse() {
+        val permissionRequestedRecord =
+                AppOpPermissionRecord(
+                        app = ApplicationInfo().apply { packageName = PACKAGE_NAME },
+                        hasRequestPermission = true,
+                        hasRequestBroaderPermission = false,
+                        appOpsController =
+                            FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT),
+                )
+        whenever(mockRoleManager.getRoleHolders(AssociationRequest.DEVICE_PROFILE_WATCH))
+                .thenReturn(listOf("other.package.name"))
+
+        val isSpecialAccessChangeable = listModel.isChangeable(permissionRequestedRecord)
+
+        assertThat(isSpecialAccessChangeable).isFalse()
+    }
+
+    private class FakeAppOpsController(fakeMode: Int) : IAppOpsController {
+
+        override val mode = MutableLiveData(fakeMode)
+
+        override fun setAllowed(allowed: Boolean) {
+            if (allowed)
+                mode.postValue(AppOpsManager.MODE_ALLOWED)
+            else
+                mode.postValue(AppOpsManager.MODE_ERRORED)
+        }
+
+        override fun getMode(): Int = mode.value!!
+    }
+
+    companion object {
+        const val PACKAGE_NAME = "test.package.name"
+        const val VALUE_LOGGING_ALLOWED = 1
+        const val VALUE_LOGGING_DENIED = 0
+    }
+}
\ No newline at end of file