[Audiosharing] Migrate feature from overlay to Settings
Bug: 340379827 Test: atest Change-Id: I3a88ac1d2f575f3be1f26f617479bbfd25cf6a8e
This commit is contained in:
		@@ -5194,6 +5194,56 @@
 | 
			
		||||
            android:theme="@style/Theme.SpaLib.Dialog">
 | 
			
		||||
        </activity>
 | 
			
		||||
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name="com.android.settings.connecteddevice.audiosharing.AudioSharingActivity"
 | 
			
		||||
            android:label="@string/audio_sharing_title"
 | 
			
		||||
            android:exported="true">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS" />
 | 
			
		||||
                <category android:name="android.intent.category.DEFAULT" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
 | 
			
		||||
                android:value="com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment"/>
 | 
			
		||||
        </activity>
 | 
			
		||||
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name="com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity"
 | 
			
		||||
            android:permission="android.permission.BLUETOOTH_CONNECT"
 | 
			
		||||
            android:screenOrientation="portrait"
 | 
			
		||||
            android:exported="false">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="android.settings.BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER"/>
 | 
			
		||||
                <category android:name="android.intent.category.DEFAULT"/>
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </activity>
 | 
			
		||||
 | 
			
		||||
        <activity
 | 
			
		||||
            android:name="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamConfirmDialogActivity"
 | 
			
		||||
            android:exported="true"
 | 
			
		||||
            android:theme="@style/Transparent"
 | 
			
		||||
            android:configChanges="orientation|keyboardHidden|screenSize">
 | 
			
		||||
            <intent-filter android:priority="1">
 | 
			
		||||
                <action android:name="android.settings.AUDIO_STREAM_DIALOG" />
 | 
			
		||||
                <category android:name="android.intent.category.DEFAULT" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
            <meta-data android:name="com.android.settings.FRAGMENT_CLASS"
 | 
			
		||||
                android:value="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamConfirmDialog" />
 | 
			
		||||
        </activity>
 | 
			
		||||
 | 
			
		||||
        <service
 | 
			
		||||
            android:name="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService"
 | 
			
		||||
            android:foregroundServiceType="mediaPlayback"
 | 
			
		||||
            android:enabled="true"
 | 
			
		||||
            android:exported="false" />
 | 
			
		||||
 | 
			
		||||
        <receiver android:name="com.android.settings.connecteddevice.audiosharing.AudioSharingReceiver"
 | 
			
		||||
            android:exported="false">
 | 
			
		||||
            <intent-filter>
 | 
			
		||||
                <action android:name="com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE" />
 | 
			
		||||
                <action android:name="com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STOP" />
 | 
			
		||||
            </intent-filter>
 | 
			
		||||
        </receiver>
 | 
			
		||||
 | 
			
		||||
        <!-- This is the longest AndroidManifest.xml ever. -->
 | 
			
		||||
    </application>
 | 
			
		||||
</manifest>
 | 
			
		||||
 
 | 
			
		||||
@@ -745,4 +745,17 @@
 | 
			
		||||
    <string name="spatial_audio_speaker" product="tablet">Tablet speakers</string>
 | 
			
		||||
    <!-- Output device type for the phone speaker that is available for spatializer effect. [CHAR LIMIT=NONE]-->
 | 
			
		||||
    <string name="spatial_audio_speaker" product="device">Device speakers</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Content for audio sharing share dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_dialog_share_content" product="default">This phone\'s music and videos will play on both pairs of headphones</string>
 | 
			
		||||
    <string name="audio_sharing_dialog_share_content" product="tablet">This tablet\'s music and videos will play on both pairs of headphones</string>
 | 
			
		||||
    <string name="audio_sharing_dialog_share_content" product="device">This device\'s music and videos will play on both pairs of headphones</string>
 | 
			
		||||
    <!-- Content for audio sharing share dialog with more devices [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_dialog_share_more_content" product="default">This phone\'s music and videos will play on the headphones you connect</string>
 | 
			
		||||
    <string name="audio_sharing_dialog_share_more_content" product="tablet">This tablet\'s music and videos will play on the headphones you connect</string>
 | 
			
		||||
    <string name="audio_sharing_dialog_share_more_content" product="device">This device\'s music and videos will play on the headphones you connect</string>
 | 
			
		||||
    <!-- Le audio streams no le device dialog subtitle [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_no_le_device_subtitle" product="default">To listen to an audio stream, first connect headphones that support LE Audio to this phone.</string>
 | 
			
		||||
    <string name="audio_streams_dialog_no_le_device_subtitle" product="tablet">To listen to an audio stream, first connect headphones that support LE Audio to this tablet.</string>
 | 
			
		||||
    <string name="audio_streams_dialog_no_le_device_subtitle" product="device">To listen to an audio stream, first connect headphones that support LE Audio to this device.</string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								res/drawable/audio_sharing_guidance.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								res/drawable/audio_sharing_guidance.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.0 KiB  | 
							
								
								
									
										22
									
								
								res/drawable/audio_sharing_rounded_bg.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								res/drawable/audio_sharing_rounded_bg.xml
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
			
		||||
							
								
								
									
										21
									
								
								res/drawable/audio_sharing_rounded_bg_ripple.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								res/drawable/audio_sharing_rounded_bg_ripple.xml
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
			
		||||
							
								
								
									
										32
									
								
								res/drawable/ic_audio_calls_and_alarms.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								res/drawable/ic_audio_calls_and_alarms.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
<!--
 | 
			
		||||
  Copyright (C) 2018 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.
 | 
			
		||||
  -->
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24.0"
 | 
			
		||||
    android:viewportHeight="24.0"
 | 
			
		||||
    android:tint="?android:attr/colorControlNormal">
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M3,15V9H7L12,4V20L7,15H3ZM10,15.17V8.83L7.83,11H5V13H7.83L10,15.17Z"
 | 
			
		||||
        android:fillType="evenOdd"
 | 
			
		||||
        android:fillColor="?android:attr/colorPrimary"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M16.5,12C16.5,10.23 15.48,8.71 14,7.97V16.02C15.48,15.29 16.5,13.77 16.5,12Z"
 | 
			
		||||
        android:fillColor="?android:attr/colorPrimary"/>
 | 
			
		||||
    <path
 | 
			
		||||
        android:pathData="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.85 14,18.71V20.77C18.01,19.86 21,16.28 21,12C21,7.72 18.01,4.14 14,3.23Z"
 | 
			
		||||
        android:fillColor="?android:attr/colorPrimary"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										32
									
								
								res/drawable/ic_audio_play_sample.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								res/drawable/ic_audio_play_sample.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
<!--
 | 
			
		||||
  ~ 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.
 | 
			
		||||
  -->
 | 
			
		||||
 | 
			
		||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:width="24dp"
 | 
			
		||||
    android:height="24dp"
 | 
			
		||||
    android:viewportWidth="24"
 | 
			
		||||
    android:viewportHeight="24"
 | 
			
		||||
    android:tint="?android:attr/colorControlNormal">
 | 
			
		||||
  <path
 | 
			
		||||
      android:pathData="M14,8C9.6,8 6,11.6 6,16H8C8,12.7 10.7,10 14,10V8Z"
 | 
			
		||||
      android:fillColor="#4E4639"/>
 | 
			
		||||
  <path
 | 
			
		||||
      android:pathData="M14,6V4C7.4,4 2,9.4 2,16H4C4,10.5 8.5,6 14,6Z"
 | 
			
		||||
      android:fillColor="#4E4639"/>
 | 
			
		||||
  <path
 | 
			
		||||
      android:pathData="M16,4V12.6C15.4,12.3 14.7,12 14,12C11.8,12 10,13.8 10,16C10,18.2 11.8,20 14,20C16.2,20 18,18.2 18,16V7H22V4H16ZM14,18C12.9,18 12,17.1 12,16C12,14.9 12.9,14 14,14C15.1,14 16,14.9 16,16C16,17.1 15.1,18 14,18Z"
 | 
			
		||||
      android:fillColor="#4E4639"/>
 | 
			
		||||
</vector>
 | 
			
		||||
							
								
								
									
										33
									
								
								res/layout/audio_sharing_device_item.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								res/layout/audio_sharing_device_item.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
<?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.
 | 
			
		||||
  -->
 | 
			
		||||
 | 
			
		||||
<FrameLayout
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="wrap_content"
 | 
			
		||||
    android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
    <Button
 | 
			
		||||
        android:id="@+id/device_button"
 | 
			
		||||
        style="@style/SettingsLibActionButton"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginTop="4dp"
 | 
			
		||||
        android:background="@drawable/audio_sharing_rounded_bg_ripple"
 | 
			
		||||
        android:textAlignment="center" />
 | 
			
		||||
 | 
			
		||||
</FrameLayout>
 | 
			
		||||
							
								
								
									
										75
									
								
								res/layout/audio_sharing_password_dialog.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								res/layout/audio_sharing_password_dialog.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!--
 | 
			
		||||
    Copyright (C) 2022 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.
 | 
			
		||||
  -->
 | 
			
		||||
<ScrollView
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent"
 | 
			
		||||
    android:layout_marginTop="48dp"
 | 
			
		||||
    android:layout_marginBottom="48dp"
 | 
			
		||||
    android:overScrollMode="ifContentScrolls">
 | 
			
		||||
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
        <EditText
 | 
			
		||||
            android:id="@android:id/edit"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginStart="20dp"
 | 
			
		||||
            android:layout_marginEnd="20dp"
 | 
			
		||||
            android:minHeight="48dp" />
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
            <TextView
 | 
			
		||||
                android:id="@+id/audio_sharing_stream_password_checkbox_text"
 | 
			
		||||
                style="?android:attr/textAppearanceSmall"
 | 
			
		||||
                android:layout_width="0dp"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_marginStart="24dp"
 | 
			
		||||
                android:layout_marginTop="24dp"
 | 
			
		||||
                android:layout_weight="1"
 | 
			
		||||
                android:text="@string/audio_streams_no_password_summary"
 | 
			
		||||
                android:textColor="?android:attr/textColorSecondary" />
 | 
			
		||||
 | 
			
		||||
            <CheckBox
 | 
			
		||||
                android:id="@+id/audio_sharing_stream_password_checkbox"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_marginTop="24dp"
 | 
			
		||||
                android:layout_marginEnd="20dp" />
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
 | 
			
		||||
        <TextView
 | 
			
		||||
            android:id="@android:id/message"
 | 
			
		||||
            style="?android:attr/textAppearanceSmall"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginStart="24dp"
 | 
			
		||||
            android:layout_marginTop="24dp"
 | 
			
		||||
            android:layout_marginEnd="24dp"
 | 
			
		||||
            android:layout_marginBottom="24dp"
 | 
			
		||||
            android:text="@string/audio_streams_main_page_password_dialog_cannot_edit"
 | 
			
		||||
            android:textColor="?android:attr/textColorSecondary" />
 | 
			
		||||
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
</ScrollView>
 | 
			
		||||
							
								
								
									
										71
									
								
								res/layout/dialog_custom_body_audio_sharing.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								res/layout/dialog_custom_body_audio_sharing.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!--
 | 
			
		||||
  ~ Copyright (C) 2024 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:paddingHorizontal="?android:dialogPreferredPadding"
 | 
			
		||||
    android:paddingBottom="?android:dialogPreferredPadding">
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
        android:id="@+id/description_text"
 | 
			
		||||
        style="@style/DeviceAudioSharingText"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_gravity="center"
 | 
			
		||||
        android:paddingBottom="24dp"
 | 
			
		||||
        android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
    <ImageView
 | 
			
		||||
        android:id="@+id/description_image"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_gravity="center"
 | 
			
		||||
        android:contentDescription="@null"
 | 
			
		||||
        android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
    <androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
        android:id="@+id/device_btn_list"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_gravity="center"
 | 
			
		||||
        android:nestedScrollingEnabled="false"
 | 
			
		||||
        android:overScrollMode="never"
 | 
			
		||||
        android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
    <Button
 | 
			
		||||
        android:id="@+id/positive_btn"
 | 
			
		||||
        style="@style/SettingsLibActionButton"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_gravity="center"
 | 
			
		||||
        android:layout_marginTop="4dp"
 | 
			
		||||
        android:background="@drawable/audio_sharing_rounded_bg_ripple"
 | 
			
		||||
        android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
    <Button
 | 
			
		||||
        android:id="@+id/negative_btn"
 | 
			
		||||
        style="@style/SettingsLibActionButton"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_gravity="center"
 | 
			
		||||
        android:layout_marginTop="4dp"
 | 
			
		||||
        android:background="@drawable/audio_sharing_rounded_bg_ripple"
 | 
			
		||||
        android:visibility="gone" />
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
							
								
								
									
										42
									
								
								res/layout/dialog_custom_title_audio_sharing.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								res/layout/dialog_custom_title_audio_sharing.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
<?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="28dp"
 | 
			
		||||
        android:layout_height="28dp"
 | 
			
		||||
        android:layout_gravity="center"
 | 
			
		||||
        android:contentDescription="@null"
 | 
			
		||||
        android:tint="?android:attr/colorControlNormal" />
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
        android:id="@+id/title_text"
 | 
			
		||||
        style="@android:style/TextAppearance.DeviceDefault.Headline"
 | 
			
		||||
        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>
 | 
			
		||||
							
								
								
									
										29
									
								
								res/layout/preference_widget_lock.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								res/layout/preference_widget_lock.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
<?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.
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<ImageView
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:id="@+id/lock_icon"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent"
 | 
			
		||||
    android:layout_gravity="center"
 | 
			
		||||
    android:paddingStart="24dp"
 | 
			
		||||
    android:paddingEnd="24dp"
 | 
			
		||||
    android:background="?android:attr/selectableItemBackground"
 | 
			
		||||
    android:scaleType="center"
 | 
			
		||||
    android:src="@drawable/ic_lock_closed"
 | 
			
		||||
    android:importantForAccessibility="no" />
 | 
			
		||||
 | 
			
		||||
@@ -35,8 +35,8 @@
 | 
			
		||||
            android:gravity="center"
 | 
			
		||||
            android:orientation="vertical">
 | 
			
		||||
            <TextView
 | 
			
		||||
                android:id="@android:id/summary"
 | 
			
		||||
                style="@style/QrCodeScanner"
 | 
			
		||||
                android:text="Scan an audio stream QR code to listen with the active LE device"
 | 
			
		||||
                android:gravity="center"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
 
 | 
			
		||||
@@ -488,4 +488,7 @@
 | 
			
		||||
    <dimen name="contrast_button_text_size">14sp</dimen>
 | 
			
		||||
    <dimen name="contrast_button_text_spacing">4dp</dimen>
 | 
			
		||||
    <dimen name="contrast_button_horizontal_spacing">16dp</dimen>
 | 
			
		||||
 | 
			
		||||
    <dimen name="audio_streams_qrcode_size">264dp</dimen>
 | 
			
		||||
    <dimen name="audio_streams_qrcode_preview_radius">30dp</dimen>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
@@ -303,9 +303,6 @@
 | 
			
		||||
    <!-- Name shown in the title of individual stylus preference in the connected devices page [CHAR LIMIT=60] -->
 | 
			
		||||
    <string name="stylus_connected_devices_title">Stylus</string>
 | 
			
		||||
 | 
			
		||||
    <!--Text that appears when scanning for nearby audio streams is finished and no streams were found [CHAR LIMIT=40]-->
 | 
			
		||||
    <string name="audio_streams_empty">No nearby audio streams were found.</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Date & time settings screen title -->
 | 
			
		||||
    <string name="date_and_time">Date & time</string>
 | 
			
		||||
 | 
			
		||||
@@ -7315,8 +7312,6 @@
 | 
			
		||||
    <string name="help_url_insecure_vpn" translatable="false"></string>
 | 
			
		||||
    <!-- url for learning more about IT admin policy disabling -->
 | 
			
		||||
    <string name="help_url_action_disabled_by_it_admin" translatable="false"></string>
 | 
			
		||||
    <!-- url for learning more about bluetooth audio sharing -->
 | 
			
		||||
    <string name="help_url_audio_sharing" translatable="false"></string>
 | 
			
		||||
 | 
			
		||||
    <!-- User account title [CHAR LIMIT=30] -->
 | 
			
		||||
    <string name="user_account_title">Account for content</string>
 | 
			
		||||
@@ -13270,4 +13265,198 @@
 | 
			
		||||
 | 
			
		||||
    <!-- Title for System dashboard fragment -->
 | 
			
		||||
    <string name="device_diagnostics_title">Device diagnostics</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Title for audio sharing page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_title">Audio sharing</string>
 | 
			
		||||
    <!-- Title for audio sharing primary switch [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_switch_title">Share audio</string>
 | 
			
		||||
    <!-- Title for calls and alarms device on audio sharing page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="calls_and_alarms_device_title">Calls and alarms</string>
 | 
			
		||||
    <!-- Description for audio sharing page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_description">Let people listen to your media along with you. Listeners need their own LE Audio headphones.</string>
 | 
			
		||||
    <!-- Title for audio sharing device group [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_device_group_title">Active media devices</string>
 | 
			
		||||
    <!-- Title for call audio on audio sharing page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_call_audio_title">Call audio</string>
 | 
			
		||||
    <!-- Description for call audio on audio sharing page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_call_audio_description">Play only on <xliff:g example="My buds" id="device_name">%1$s</xliff:g></string>
 | 
			
		||||
    <!-- Title for play test sound on audio sharing page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_test_sound_title">Play a test sound</string>
 | 
			
		||||
    <!-- Description for play test sound on audio sharing page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_test_sound_description">Everyone listening should hear it</string>
 | 
			
		||||
    <!-- Title for stream settings group on audio sharing page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stream_settings_title">Audio stream settings</string>
 | 
			
		||||
    <!-- Title for stream name on audio sharing page, under stream settings group [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stream_name_title">Name</string>
 | 
			
		||||
    <!-- Title for stream password on audio sharing page, under stream settings group [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stream_password_title">Password</string>
 | 
			
		||||
    <!-- Title for stream compatibility on audio sharing page, under stream settings group [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stream_compatibility_title">Improve compatibility</string>
 | 
			
		||||
    <!-- Description for stream compatibility on audio sharing page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stream_compatibility_description">Helps some devices, like hearing aids, connect by reducing audio quality</string>
 | 
			
		||||
    <!-- Description for stream compatibility on audio sharing page when audio sharing is on [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stream_compatibility_disabled_description">Turns off the audio sharing to config the compatibility</string>
 | 
			
		||||
    <!-- Title for nearby audio group on audio sharing page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_nearby_audio_title">Listen to nearby audio</string>
 | 
			
		||||
    <!-- Description for audio sharing page footer [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_footer_description">Audio sharing supports Auracast™</string>
 | 
			
		||||
    <!-- Title for stream name dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stream_name_dialog_title">Audio stream name</string>
 | 
			
		||||
    <!-- Title for stream password dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stream_password_dialog_title">Audio stream password</string>
 | 
			
		||||
    <!-- Title for media device group during audio sharing [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_media_device_group_title">Other media devices</string>
 | 
			
		||||
    <!-- Summary for audio sharing on [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_summary_on">On</string>
 | 
			
		||||
    <!-- Summary for audio sharing off [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_summary_off">Off</string>
 | 
			
		||||
    <!-- Title for audio sharing share dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_share_dialog_title">Share your audio</string>
 | 
			
		||||
    <!-- Subtitle for audio sharing share dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_share_dialog_subtitle"><xliff:g example="My buds1" id="device_name1">%1$s</xliff:g> and <xliff:g example="My buds2" id="device_name2">%2$s</xliff:g></string>
 | 
			
		||||
    <!-- Text for audio sharing share button [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_share_button_label">Share audio</string>
 | 
			
		||||
    <!-- Text for audio sharing no thanks button [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_no_thanks_button_label">No thanks</string>
 | 
			
		||||
    <!-- Title for audio sharing share dialog with one device [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_share_with_dialog_title">Share audio with <xliff:g example="My buds" id="device_name">%1$s</xliff:g>?</string>
 | 
			
		||||
    <!-- Title for audio sharing share dialog with more devices [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_share_with_more_dialog_title">Share audio with another device</string>
 | 
			
		||||
    <!-- Text for audio sharing share with button [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_share_with_button_label">Share with <xliff:g example="My buds" id="device_name">%1$s</xliff:g></string>
 | 
			
		||||
    <!-- Text for audio sharing close button [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_close_button_label">Close</string>
 | 
			
		||||
    <!-- Content for audio sharing share dialog with no device, ask users to connect device  [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_dialog_connect_device_content">Connect another pair of compatible headphones, or share your stream\'s name and password with the other person</string>
 | 
			
		||||
    <!-- Content for audio sharing share dialog with no device, ask users to pair device  [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_dialog_pair_device_content">Pair another set of compatible headphones, or share your audio stream QR code with the other person</string>
 | 
			
		||||
    <!-- Text for sharing audio sharing state [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_sharing_label">Sharing audio</string>
 | 
			
		||||
    <!-- Text for audio sharing pair button [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_pair_button_label">Pair new device</string>
 | 
			
		||||
    <!-- Text for audio sharing qrcode button [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_qrcode_button_label">Show QR code</string>
 | 
			
		||||
    <!-- Title for audio sharing notification [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_notification_title">You\'re sharing audio</string>
 | 
			
		||||
    <!-- Content for audio sharing notification [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_notification_content">People listening can hear your media. They won\'t hear calls.</string>
 | 
			
		||||
    <!-- Text for audio sharing stop button [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stop_button_label">Stop sharing</string>
 | 
			
		||||
    <!-- Text for audio sharing settings button [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_settings_button_label">Settings</string>
 | 
			
		||||
    <!-- Title for audio sharing disconnect dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_disconnect_dialog_title">Choose a device to disconnect</string>
 | 
			
		||||
    <!-- Content for audio sharing disconnect dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_dialog_disconnect_content">Only 2 devices can share audio at a time</string>
 | 
			
		||||
    <!-- Text for audio sharing disconnect device button [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_disconnect_device_button_label">Disconnect <xliff:g example="My buds" id="device_name">%1$s</xliff:g></string>
 | 
			
		||||
    <!-- Title for audio sharing stop dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stop_dialog_title">Connect <xliff:g example="My buds" id="device_name">%1$s</xliff:g> ?</string>
 | 
			
		||||
    <!-- Content for audio sharing stop dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stop_dialog_content">You\'ll stop sharing audio with <xliff:g example="My buds" id="device_name">%1$s</xliff:g></string>
 | 
			
		||||
    <!-- Content for audio sharing stop dialog with two devices [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stop_dialog_with_two_content">You\'ll stop sharing audio with <xliff:g example="My buds1" id="device_name1">%1$s</xliff:g> and <xliff:g example="My buds2" id="device_name2">%2$s</xliff:g></string>
 | 
			
		||||
    <!-- Content for audio sharing stop dialog with more devices  [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_stop_dialog_with_more_content">You\'ll stop sharing audio with the connected headphones</string>
 | 
			
		||||
    <!-- Text for audio sharing connect button [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_connect_button_label">Connect</string>
 | 
			
		||||
    <!-- Text for sharing audio stop state [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_sharing_stopped_label">Audio sharing stopped</string>
 | 
			
		||||
    <!-- Title for audio sharing confirm dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_confirm_dialog_title">Connect a compatible device</string>
 | 
			
		||||
    <!-- Content for audio sharing confirm dialog [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_sharing_comfirm_dialog_content">To start sharing audio, first connect LE Audio headphones to your phone</string>
 | 
			
		||||
 | 
			
		||||
    <!-- Title for audio streams preference category [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_streams_category_title">Connect to a LE audio stream</string>
 | 
			
		||||
    <!-- Title for audio streams preference [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_streams_pref_title">Nearby audio streams</string>
 | 
			
		||||
    <!-- Title for audio streams page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_streams_title">Audio streams</string>
 | 
			
		||||
    <!-- Summary for QR code scanning in audio streams page [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_streams_qr_code_summary">Connect to an audio stream using QR code</string>
 | 
			
		||||
    <!--Text that appears when scanning for nearby audio streams is finished and no streams were found [CHAR LIMIT=40]-->
 | 
			
		||||
    <string name="audio_streams_empty">No nearby audio streams were found.</string>
 | 
			
		||||
    <!-- Disconnect from an audio stream [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_streams_disconnect">Disconnect</string>
 | 
			
		||||
    <!-- Connect an audio stream [CHAR LIMIT=none]-->
 | 
			
		||||
    <string name="audio_streams_connect">Connect</string>
 | 
			
		||||
    <!-- Hint for QR code process failure [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_qr_code_is_not_valid_format">QR code isn\u0027t a valid format</string>
 | 
			
		||||
    <!-- Le audio QR code scanner sub-title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_qr_code_scanner">To start listening, center the QR code below</string>
 | 
			
		||||
    <!-- The preference summary when add source response is bad code [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_add_source_bad_code_state_summary">Check password and try again</string>
 | 
			
		||||
    <!-- The preference summary when add source response results in general failure [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_add_source_failed_state_summary">Can\u0027t connect. Try again.</string>
 | 
			
		||||
    <!-- The preference summary when waiting for add source response [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_add_source_wait_for_response_summary">Connecting\u2026</string>
 | 
			
		||||
    <!-- The preference summary when waiting for sync [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_wait_for_sync_state_summary">Scanning\u2026</string>
 | 
			
		||||
    <!-- Le audio streams audio lost dialog title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_stream_is_not_available">Audio stream isn\u0027t available</string>
 | 
			
		||||
    <!-- Le audio streams audio lost dialog subtitle [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_is_not_playing">This audio stream isn\u0027t playing anything right now</string>
 | 
			
		||||
    <!-- Le audio streams dialog close [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_close">Close</string>
 | 
			
		||||
    <!-- Le audio streams dialog listen [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_listen">Listen</string>
 | 
			
		||||
    <!-- Le audio streams dialog retry button [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_retry">Try again</string>
 | 
			
		||||
    <!-- Le audio streams confirmation dialog title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_listen_to_audio_stream">Listen to audio stream</string>
 | 
			
		||||
    <!-- Le audio streams confirmation dialog subtitle [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_control_volume">The audio stream will play on <xliff:g example="LE headset" id="device_name">%1$s</xliff:g>. Use this device to control the volume.</string>
 | 
			
		||||
    <!-- Le audio streams failure dialog title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_cannot_listen">Can\u0027t listen to audio stream</string>
 | 
			
		||||
    <!-- Le audio streams confirm dialog default device [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_default_device">connected compatible headphones</string>
 | 
			
		||||
    <!-- Le audio streams activity title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_activity_title">Broadcasts</string>
 | 
			
		||||
    <!-- Le audio streams no password summary [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_no_password_summary">No password</string>
 | 
			
		||||
    <!-- Le audio streams failure dialog subtitle [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_learn_more">Learn more</string>
 | 
			
		||||
    <!-- Le audio streams failure dialog subtitle [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_cannot_play">Can\u0027t play this audio stream on <xliff:g example="LE headset" id="device_name">%1$s</xliff:g>.</string>
 | 
			
		||||
    <!-- The preference summary when add source succeed [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_listening_now">Listening now</string>
 | 
			
		||||
    <!-- Le audio streams service notification leave broadcast text [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_media_service_notification_leave_broadcast_text">Stop listening</string>
 | 
			
		||||
    <!-- Le audio streams no le device dialog title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_no_le_device_title">Connect compatible headphones</string>
 | 
			
		||||
    <!-- Le audio streams no le device dialog button [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_dialog_no_le_device_button">Connect a device</string>
 | 
			
		||||
    <!-- Le audio streams detail page title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_detail_page_title">Audio stream details</string>
 | 
			
		||||
    <!-- Le audio streams qr code page title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_qr_code_page_title">Audio stream QR code</string>
 | 
			
		||||
    <!-- Le audio streams qr code page password text [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_qr_code_page_password">Password: <xliff:g example="123" id="password">%1$s</xliff:g></string>
 | 
			
		||||
    <!-- Le audio streams qr code page description [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_qr_code_page_description">To listen to <xliff:g example="Local Music" id="stream_name">%1$s</xliff:g>, other people can connect compatible headphones to their Android device. They can then scan this QR code.</string>
 | 
			
		||||
    <!-- Le audio streams main page title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_main_page_title">Find an audio stream</string>
 | 
			
		||||
    <!-- Le audio streams main page subtitle [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_main_page_subtitle">Listen to a device that\u0027s sharing audio or to a nearby Auracast broadcast</string>
 | 
			
		||||
    <!-- Le audio streams main page device preference title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_main_page_device_title">Your audio device</string>
 | 
			
		||||
    <!-- Le audio streams main page device preference no device summary [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_main_page_no_device_summary">Connect compatible headphones</string>
 | 
			
		||||
    <!-- Le audio streams main page scanning section title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_main_page_scan_section_title">Audio streams nearby</string>
 | 
			
		||||
    <!-- Le audio streams main page scan qr code preference title [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_main_page_scan_qr_code_title">Scan QR code</string>
 | 
			
		||||
    <!-- Le audio streams main page scan qr code preference summary [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_main_page_scan_qr_code_summary">Start listening by scanning a stream\u0027s QR code</string>
 | 
			
		||||
    <!-- Le audio streams main page password dialog join button [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_main_page_password_dialog_join_button">Listen to stream</string>
 | 
			
		||||
    <!-- Le audio streams main page qr code scanner summary [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_main_page_qr_code_scanner_summary">Scan an audio stream QR code to listen with <xliff:g example="LE headset" id="device_name">%1$s</xliff:g></string>
 | 
			
		||||
    <!-- Le audio streams password dialog  [CHAR LIMIT=NONE] -->
 | 
			
		||||
    <string name="audio_streams_main_page_password_dialog_cannot_edit">Can\u0027t edit password while sharing. To change the password, first turn off audio sharing.</string>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    <!-- url for learning more about bluetooth audio sharing -->
 | 
			
		||||
    <string name="help_url_audio_sharing" translatable="false"></string>
 | 
			
		||||
</resources>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										101
									
								
								res/xml/bluetooth_audio_streams_dialog.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								res/xml/bluetooth_audio_streams_dialog.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,101 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!--
 | 
			
		||||
  ~ Copyright (C) 2024 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.
 | 
			
		||||
  -->
 | 
			
		||||
 | 
			
		||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="match_parent">
 | 
			
		||||
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
        android:id="@+id/dialog_bg"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:paddingStart="25dp"
 | 
			
		||||
        android:paddingEnd="25dp"
 | 
			
		||||
        android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginBottom="25dp"
 | 
			
		||||
            android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
            <ImageView
 | 
			
		||||
                android:id="@+id/dialog_icon"
 | 
			
		||||
                android:layout_width="30dp"
 | 
			
		||||
                android:layout_height="30dp"
 | 
			
		||||
                android:layout_marginTop="24dp"
 | 
			
		||||
                android:layout_gravity="center"
 | 
			
		||||
                android:src="@drawable/ic_bt_le_audio_sharing"/>
 | 
			
		||||
 | 
			
		||||
            <TextView
 | 
			
		||||
                android:id="@+id/dialog_title"
 | 
			
		||||
                android:textAppearance="@android:style/TextAppearance.DeviceDefault.Headline"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_marginTop="15dp"
 | 
			
		||||
                android:gravity="center"
 | 
			
		||||
                android:layout_gravity="center"/>
 | 
			
		||||
 | 
			
		||||
            <TextView
 | 
			
		||||
                android:id="@+id/dialog_subtitle"
 | 
			
		||||
                android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
 | 
			
		||||
                android:textStyle="bold"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_marginTop="15dp"
 | 
			
		||||
                android:gravity="center"
 | 
			
		||||
                android:layout_gravity="center"
 | 
			
		||||
                android:visibility="gone"/>
 | 
			
		||||
 | 
			
		||||
            <TextView
 | 
			
		||||
                android:id="@+id/dialog_subtitle_2"
 | 
			
		||||
                android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_marginTop="15dp"
 | 
			
		||||
                android:gravity="center"
 | 
			
		||||
                android:layout_gravity="center"
 | 
			
		||||
                android:visibility="gone"/>
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
 | 
			
		||||
        <androidx.constraintlayout.widget.ConstraintLayout
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginBottom="@dimen/broadcast_dialog_margin">
 | 
			
		||||
            <Button
 | 
			
		||||
                android:id="@+id/left_button"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:visibility="gone"
 | 
			
		||||
                app:layout_constraintBottom_toBottomOf="parent"
 | 
			
		||||
                app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
                app:layout_constraintTop_toTopOf="parent"
 | 
			
		||||
                style="@style/BroadcastActionButton"/>
 | 
			
		||||
            <Button
 | 
			
		||||
                android:id="@+id/right_button"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:visibility="gone"
 | 
			
		||||
                app:layout_constraintBottom_toBottomOf="parent"
 | 
			
		||||
                app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
                app:layout_constraintTop_toTopOf="parent"
 | 
			
		||||
                style="@style/BroadcastActionButton"/>
 | 
			
		||||
        </androidx.constraintlayout.widget.ConstraintLayout>
 | 
			
		||||
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
</FrameLayout>
 | 
			
		||||
							
								
								
									
										63
									
								
								res/xml/bluetooth_audio_streams_qr_code.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								res/xml/bluetooth_audio_streams_qr_code.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
<?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="match_parent">
 | 
			
		||||
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:clipToPadding="false"
 | 
			
		||||
        android:paddingLeft="25dp"
 | 
			
		||||
        android:paddingRight="25dp"
 | 
			
		||||
        android:gravity="center_horizontal"
 | 
			
		||||
        android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
        <TextView
 | 
			
		||||
            android:id="@android:id/summary"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:gravity="start"
 | 
			
		||||
            android:textSize="15sp"
 | 
			
		||||
            android:textColor="?android:attr/textColorPrimary"/>
 | 
			
		||||
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:gravity="center"
 | 
			
		||||
            android:orientation="vertical"
 | 
			
		||||
            android:paddingTop="70dp">
 | 
			
		||||
 | 
			
		||||
            <ImageView
 | 
			
		||||
                android:id="@+id/qrcode_view"
 | 
			
		||||
                android:layout_width="@dimen/qrcode_size"
 | 
			
		||||
                android:layout_height="@dimen/qrcode_size"
 | 
			
		||||
                android:src="@android:color/transparent"/>
 | 
			
		||||
 | 
			
		||||
            <TextView
 | 
			
		||||
                android:id="@+id/password"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:textSize="15sp"
 | 
			
		||||
                android:textColor="?android:attr/textColorPrimary"/>
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
							
								
								
									
										81
									
								
								res/xml/bluetooth_le_audio_sharing.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								res/xml/bluetooth_le_audio_sharing.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
<?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.
 | 
			
		||||
  -->
 | 
			
		||||
<PreferenceScreen
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:settings="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    android:title="@string/audio_sharing_title">
 | 
			
		||||
 | 
			
		||||
    <com.android.settingslib.widget.TopIntroPreference
 | 
			
		||||
        android:key="audio_sharing_top_intro"
 | 
			
		||||
        android:title="@string/audio_sharing_description"
 | 
			
		||||
        settings:searchable="false" />
 | 
			
		||||
 | 
			
		||||
    <PreferenceCategory
 | 
			
		||||
        android:key="audio_sharing_device_volume_group"
 | 
			
		||||
        android:title="@string/audio_sharing_device_group_title"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingDeviceVolumeGroupController" />
 | 
			
		||||
 | 
			
		||||
    <Preference
 | 
			
		||||
        android:icon="@drawable/ic_audio_calls_and_alarms"
 | 
			
		||||
        android:key="calls_and_alarms"
 | 
			
		||||
        android:summary=""
 | 
			
		||||
        android:title="@string/audio_sharing_call_audio_title"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.CallsAndAlarmsPreferenceController" />
 | 
			
		||||
 | 
			
		||||
    <Preference
 | 
			
		||||
        android:icon="@drawable/ic_audio_play_sample"
 | 
			
		||||
        android:key="audio_sharing_play_sound"
 | 
			
		||||
        android:summary="@string/audio_sharing_test_sound_description"
 | 
			
		||||
        android:title="@string/audio_sharing_test_sound_title"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPlaySoundPreferenceController" />
 | 
			
		||||
 | 
			
		||||
    <PreferenceCategory
 | 
			
		||||
        android:key="audio_sharing_stream_settings_category"
 | 
			
		||||
        android:title="@string/audio_sharing_stream_settings_title"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.StreamSettingsCategoryController">
 | 
			
		||||
 | 
			
		||||
        <com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreference
 | 
			
		||||
            android:key="audio_sharing_stream_name"
 | 
			
		||||
            android:title="@string/audio_sharing_stream_name_title"
 | 
			
		||||
            settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingNamePreferenceController" />
 | 
			
		||||
 | 
			
		||||
        <com.android.settings.connecteddevice.audiosharing.AudioSharingPasswordPreference
 | 
			
		||||
            android:dialogLayout="@layout/audio_sharing_password_dialog"
 | 
			
		||||
            android:key="audio_sharing_stream_password"
 | 
			
		||||
            android:summary="********"
 | 
			
		||||
            android:title="@string/audio_sharing_stream_password_title"
 | 
			
		||||
            settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPasswordPreferenceController" />
 | 
			
		||||
 | 
			
		||||
        <SwitchPreferenceCompat
 | 
			
		||||
            android:key="audio_sharing_stream_compatibility"
 | 
			
		||||
            android:title="@string/audio_sharing_stream_compatibility_title"
 | 
			
		||||
            settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingCompatibilityPreferenceController" />
 | 
			
		||||
    </PreferenceCategory>
 | 
			
		||||
 | 
			
		||||
    <PreferenceCategory
 | 
			
		||||
        android:key="audio_streams_settings_category"
 | 
			
		||||
        android:title="@string/audio_streams_category_title"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController">
 | 
			
		||||
 | 
			
		||||
        <Preference
 | 
			
		||||
            android:fragment="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsDashboardFragment"
 | 
			
		||||
            android:icon="@drawable/ic_chevron_right_24dp"
 | 
			
		||||
            android:key="audio_streams_settings"
 | 
			
		||||
            android:title="@string/audio_streams_pref_title" />
 | 
			
		||||
 | 
			
		||||
    </PreferenceCategory>
 | 
			
		||||
</PreferenceScreen>
 | 
			
		||||
							
								
								
									
										36
									
								
								res/xml/bluetooth_le_audio_stream_details_fragment.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								res/xml/bluetooth_le_audio_stream_details_fragment.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
<?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.
 | 
			
		||||
  -->
 | 
			
		||||
 | 
			
		||||
<PreferenceScreen
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:settings="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    android:title="@string/audio_streams_detail_page_title">
 | 
			
		||||
 | 
			
		||||
    <com.android.settingslib.widget.LayoutPreference
 | 
			
		||||
        android:key="audio_stream_header"
 | 
			
		||||
        android:layout="@layout/settings_entity_header"
 | 
			
		||||
        android:selectable="false"
 | 
			
		||||
        settings:allowDividerBelow="true"
 | 
			
		||||
        settings:searchable="false"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamHeaderController" />
 | 
			
		||||
 | 
			
		||||
    <com.android.settingslib.widget.ActionButtonsPreference
 | 
			
		||||
        android:key="audio_stream_button"
 | 
			
		||||
        settings:allowDividerBelow="true"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamButtonController" />
 | 
			
		||||
 | 
			
		||||
</PreferenceScreen>
 | 
			
		||||
							
								
								
									
										46
									
								
								res/xml/bluetooth_le_audio_streams.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								res/xml/bluetooth_le_audio_streams.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
<?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.
 | 
			
		||||
  -->
 | 
			
		||||
 | 
			
		||||
<PreferenceScreen
 | 
			
		||||
    xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:settings="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    android:title="@string/audio_streams_main_page_title">
 | 
			
		||||
 | 
			
		||||
    <com.android.settingslib.widget.TopIntroPreference
 | 
			
		||||
        android:key="audio_streams_top_intro"
 | 
			
		||||
        android:title="@string/audio_streams_main_page_subtitle"
 | 
			
		||||
        settings:searchable="false" />
 | 
			
		||||
 | 
			
		||||
    <Preference
 | 
			
		||||
        android:key="audio_streams_active_device"
 | 
			
		||||
        android:title="@string/audio_streams_main_page_device_title"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsActiveDeviceController" />
 | 
			
		||||
 | 
			
		||||
    <com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference
 | 
			
		||||
        android:key="audio_streams_nearby_category"
 | 
			
		||||
        android:title="@string/audio_streams_main_page_scan_section_title"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryController">
 | 
			
		||||
        <Preference
 | 
			
		||||
            android:icon="@drawable/ic_add_24dp"
 | 
			
		||||
            android:key="audio_streams_scan_qr_code"
 | 
			
		||||
            android:order="0"
 | 
			
		||||
            android:summary="@string/audio_streams_main_page_scan_qr_code_summary"
 | 
			
		||||
            android:title="@string/audio_streams_main_page_scan_qr_code_title"
 | 
			
		||||
            settings:controller="com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController" />
 | 
			
		||||
    </com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsProgressCategoryPreference>
 | 
			
		||||
 | 
			
		||||
</PreferenceScreen>
 | 
			
		||||
@@ -26,9 +26,23 @@
 | 
			
		||||
        settings:allowDividerBelow="true"
 | 
			
		||||
        settings:controller="com.android.settings.slices.SlicePreferenceController" />
 | 
			
		||||
 | 
			
		||||
    <PreferenceCategory
 | 
			
		||||
        android:key="audio_sharing_device_list"
 | 
			
		||||
        android:title="@string/audio_sharing_device_group_title"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingDevicePreferenceController">
 | 
			
		||||
        <Preference
 | 
			
		||||
            android:fragment="com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment"
 | 
			
		||||
            android:icon="@drawable/ic_bt_le_audio_sharing"
 | 
			
		||||
            android:key="connected_device_audio_sharing_settings"
 | 
			
		||||
            android:order="100"
 | 
			
		||||
            android:title="@string/audio_sharing_title"
 | 
			
		||||
            settings:searchable="false" />
 | 
			
		||||
    </PreferenceCategory>
 | 
			
		||||
 | 
			
		||||
    <PreferenceCategory
 | 
			
		||||
        android:key="available_device_list"
 | 
			
		||||
        android:title="@string/connected_device_media_device_title"/>
 | 
			
		||||
        android:title="@string/connected_device_media_device_title"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.AvailableMediaDeviceGroupController" />
 | 
			
		||||
 | 
			
		||||
    <PreferenceCategory
 | 
			
		||||
        android:key="connected_device_list"
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,15 @@
 | 
			
		||||
        android:order="-10"
 | 
			
		||||
        android:title="@string/bluetooth_settings_title" />
 | 
			
		||||
 | 
			
		||||
    <Preference
 | 
			
		||||
        android:fragment="com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment"
 | 
			
		||||
        android:icon="@drawable/ic_bt_le_audio_sharing"
 | 
			
		||||
        android:key="audio_sharing_settings"
 | 
			
		||||
        android:order="-9"
 | 
			
		||||
        android:title="@string/audio_sharing_title"
 | 
			
		||||
        settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPreferenceController"
 | 
			
		||||
        settings:searchable="true" />
 | 
			
		||||
 | 
			
		||||
    <com.android.settingslib.RestrictedPreference
 | 
			
		||||
        android:fragment="com.android.settings.connecteddevice.NfcAndPaymentFragment"
 | 
			
		||||
        android:icon="@drawable/ic_nfc"
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,8 @@ import android.util.Log;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.connecteddevice.DevicePreferenceCallback;
 | 
			
		||||
import com.android.settings.overlay.FeatureFactory;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
 | 
			
		||||
@@ -76,11 +77,17 @@ public class AvailableMediaBluetoothDeviceUpdater extends BluetoothDeviceUpdater
 | 
			
		||||
            // It would show in Available Devices group if the audio sharing flag is disabled or
 | 
			
		||||
            // the device is not in the audio sharing session.
 | 
			
		||||
            if (cachedDevice.isConnectedLeAudioDevice()) {
 | 
			
		||||
                boolean isAudioSharingFilterMatched =
 | 
			
		||||
                        FeatureFactory.getFeatureFactory()
 | 
			
		||||
                                .getAudioSharingFeatureProvider()
 | 
			
		||||
                                .isAudioSharingFilterMatched(cachedDevice, mLocalManager);
 | 
			
		||||
                if (!isAudioSharingFilterMatched) {
 | 
			
		||||
                if (AudioSharingUtils.isFeatureEnabled()
 | 
			
		||||
                        && BluetoothUtils.hasConnectedBroadcastSource(
 | 
			
		||||
                                cachedDevice, mLocalBtManager)) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "Filter out device : "
 | 
			
		||||
                                    + cachedDevice.getName()
 | 
			
		||||
                                    + ", it is in audio sharing.");
 | 
			
		||||
                    return false;
 | 
			
		||||
 | 
			
		||||
                } else {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "isFilterMatched() device : "
 | 
			
		||||
@@ -88,13 +95,6 @@ public class AvailableMediaBluetoothDeviceUpdater extends BluetoothDeviceUpdater
 | 
			
		||||
                                    + ", the LE Audio profile is connected and not in sharing "
 | 
			
		||||
                                    + "if broadcast enabled.");
 | 
			
		||||
                    return true;
 | 
			
		||||
                } else {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "Filter out device : "
 | 
			
		||||
                                    + cachedDevice.getName()
 | 
			
		||||
                                    + ", it is in audio sharing.");
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,10 @@ package com.android.settings.connecteddevice;
 | 
			
		||||
 | 
			
		||||
import static com.android.settingslib.Utils.isAudioModeOngoingCall;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastAssistant;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
@@ -38,13 +42,18 @@ import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothDevicePreference;
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingDialogHandler;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.core.lifecycle.Lifecycle;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controller to maintain the {@link androidx.preference.PreferenceGroup} for all available media
 | 
			
		||||
@@ -57,23 +66,78 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
 | 
			
		||||
    private static final String TAG = "AvailableMediaDeviceGroupController";
 | 
			
		||||
    private static final String KEY = "available_device_list";
 | 
			
		||||
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    @VisibleForTesting @Nullable LocalBluetoothManager mLocalBluetoothManager;
 | 
			
		||||
    @VisibleForTesting @Nullable PreferenceGroup mPreferenceGroup;
 | 
			
		||||
    @VisibleForTesting LocalBluetoothManager mLocalBluetoothManager;
 | 
			
		||||
    @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
 | 
			
		||||
    @Nullable private FragmentManager mFragmentManager;
 | 
			
		||||
    @Nullable private AudioSharingDialogHandler mDialogHandler;
 | 
			
		||||
    private BluetoothLeBroadcastAssistant.Callback mAssistantCallback =
 | 
			
		||||
            new BluetoothLeBroadcastAssistant.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStarted(int reason) {}
 | 
			
		||||
 | 
			
		||||
    public AvailableMediaDeviceGroupController(
 | 
			
		||||
            Context context,
 | 
			
		||||
            @Nullable DashboardFragment fragment,
 | 
			
		||||
            @Nullable Lifecycle lifecycle) {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStartFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStopped(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStopFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAdded(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAddFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink,
 | 
			
		||||
                        @NonNull BluetoothLeBroadcastMetadata source,
 | 
			
		||||
                        int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceModified(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceModifyFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoved(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
                    Log.d(TAG, "onSourceRemoved: update media device list.");
 | 
			
		||||
                    if (mBluetoothDeviceUpdater != null) {
 | 
			
		||||
                        mBluetoothDeviceUpdater.forceUpdate();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoveFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onReceiveStateChanged(
 | 
			
		||||
                        @NonNull BluetoothDevice sink,
 | 
			
		||||
                        int sourceId,
 | 
			
		||||
                        @NonNull BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
                    if (BluetoothUtils.isConnected(state)) {
 | 
			
		||||
                        Log.d(TAG, "onReceiveStateChanged: synced, update media device list.");
 | 
			
		||||
                        if (mBluetoothDeviceUpdater != null) {
 | 
			
		||||
                            mBluetoothDeviceUpdater.forceUpdate();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    public AvailableMediaDeviceGroupController(Context context) {
 | 
			
		||||
        super(context, KEY);
 | 
			
		||||
        if (fragment != null) {
 | 
			
		||||
            init(fragment);
 | 
			
		||||
        }
 | 
			
		||||
        if (lifecycle != null) {
 | 
			
		||||
            lifecycle.addObserver(this);
 | 
			
		||||
        }
 | 
			
		||||
        mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
@@ -82,6 +146,21 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
 | 
			
		||||
            Log.e(TAG, "onStart() Bluetooth is not supported on this device");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            LocalBluetoothLeBroadcastAssistant assistant =
 | 
			
		||||
                    mLocalBluetoothManager
 | 
			
		||||
                            .getProfileManager()
 | 
			
		||||
                            .getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
            if (assistant != null) {
 | 
			
		||||
                if (DEBUG) {
 | 
			
		||||
                    Log.d(TAG, "onStart() Register callbacks for assistant.");
 | 
			
		||||
                }
 | 
			
		||||
                assistant.registerServiceCallBack(mExecutor, mAssistantCallback);
 | 
			
		||||
            }
 | 
			
		||||
            if (mDialogHandler != null) {
 | 
			
		||||
                mDialogHandler.registerCallbacks(mExecutor);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        mLocalBluetoothManager.getEventManager().registerCallback(this);
 | 
			
		||||
        if (mBluetoothDeviceUpdater != null) {
 | 
			
		||||
            mBluetoothDeviceUpdater.registerCallback();
 | 
			
		||||
@@ -95,6 +174,21 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
 | 
			
		||||
            Log.e(TAG, "onStop() Bluetooth is not supported on this device");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            LocalBluetoothLeBroadcastAssistant assistant =
 | 
			
		||||
                    mLocalBluetoothManager
 | 
			
		||||
                            .getProfileManager()
 | 
			
		||||
                            .getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
            if (assistant != null) {
 | 
			
		||||
                if (DEBUG) {
 | 
			
		||||
                    Log.d(TAG, "onStop() Register callbacks for assistant.");
 | 
			
		||||
                }
 | 
			
		||||
                assistant.unregisterServiceCallBack(mAssistantCallback);
 | 
			
		||||
            }
 | 
			
		||||
            if (mDialogHandler != null) {
 | 
			
		||||
                mDialogHandler.unregisterCallbacks();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (mBluetoothDeviceUpdater != null) {
 | 
			
		||||
            mBluetoothDeviceUpdater.unregisterCallback();
 | 
			
		||||
        }
 | 
			
		||||
@@ -155,7 +249,11 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
 | 
			
		||||
    public void onDeviceClick(Preference preference) {
 | 
			
		||||
        final CachedBluetoothDevice cachedDevice =
 | 
			
		||||
                ((BluetoothDevicePreference) preference).getBluetoothDevice();
 | 
			
		||||
        cachedDevice.setActive();
 | 
			
		||||
        if (AudioSharingUtils.isFeatureEnabled() && mDialogHandler != null) {
 | 
			
		||||
            mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ true);
 | 
			
		||||
        } else {
 | 
			
		||||
            cachedDevice.setActive();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void init(DashboardFragment fragment) {
 | 
			
		||||
@@ -165,6 +263,9 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
 | 
			
		||||
                        fragment.getContext(),
 | 
			
		||||
                        AvailableMediaDeviceGroupController.this,
 | 
			
		||||
                        fragment.getMetricsCategory());
 | 
			
		||||
        if (AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            mDialogHandler = new AudioSharingDialogHandler(mContext, fragment);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
@@ -177,6 +278,11 @@ public class AvailableMediaDeviceGroupController extends BasePreferenceControlle
 | 
			
		||||
        mBluetoothDeviceUpdater = bluetoothDeviceUpdater;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setDialogHandler(AudioSharingDialogHandler dialogHandler) {
 | 
			
		||||
        mDialogHandler = dialogHandler;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onAudioModeChanged() {
 | 
			
		||||
        updateTitle();
 | 
			
		||||
 
 | 
			
		||||
@@ -22,12 +22,13 @@ import android.provider.DeviceConfig;
 | 
			
		||||
import android.text.TextUtils;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.SettingsActivity;
 | 
			
		||||
import com.android.settings.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingDevicePreferenceController;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 | 
			
		||||
import com.android.settings.core.SettingsUIDeviceConfig;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settings.overlay.FeatureFactory;
 | 
			
		||||
@@ -35,13 +36,8 @@ import com.android.settings.overlay.SurveyFeatureProvider;
 | 
			
		||||
import com.android.settings.search.BaseSearchIndexProvider;
 | 
			
		||||
import com.android.settings.slices.SlicePreferenceController;
 | 
			
		||||
import com.android.settingslib.bluetooth.HearingAidStatsLogUtils;
 | 
			
		||||
import com.android.settingslib.core.AbstractPreferenceController;
 | 
			
		||||
import com.android.settingslib.core.lifecycle.Lifecycle;
 | 
			
		||||
import com.android.settingslib.search.SearchIndexable;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
 | 
			
		||||
public class ConnectedDeviceDashboardFragment extends DashboardFragment {
 | 
			
		||||
 | 
			
		||||
@@ -91,6 +87,10 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment {
 | 
			
		||||
                            + ", action : "
 | 
			
		||||
                            + action);
 | 
			
		||||
        }
 | 
			
		||||
        if (AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            use(AudioSharingDevicePreferenceController.class).init(this);
 | 
			
		||||
        }
 | 
			
		||||
        use(AvailableMediaDeviceGroupController.class).init(this);
 | 
			
		||||
        use(ConnectedDeviceGroupController.class).init(this);
 | 
			
		||||
        use(PreviouslyConnectedDevicePreferenceController.class).init(this);
 | 
			
		||||
        use(SlicePreferenceController.class)
 | 
			
		||||
@@ -112,31 +112,6 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
 | 
			
		||||
        return buildPreferenceControllers(context, /* fragment= */ this, getSettingsLifecycle());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static List<AbstractPreferenceController> buildPreferenceControllers(
 | 
			
		||||
            Context context,
 | 
			
		||||
            @Nullable ConnectedDeviceDashboardFragment fragment,
 | 
			
		||||
            @Nullable Lifecycle lifecycle) {
 | 
			
		||||
        final List<AbstractPreferenceController> controllers = new ArrayList<>();
 | 
			
		||||
        AbstractPreferenceController availableMediaController =
 | 
			
		||||
                FeatureFactory.getFeatureFactory()
 | 
			
		||||
                        .getAudioSharingFeatureProvider()
 | 
			
		||||
                        .createAvailableMediaDeviceGroupController(context, fragment, lifecycle);
 | 
			
		||||
        controllers.add(availableMediaController);
 | 
			
		||||
        AbstractPreferenceController audioSharingController =
 | 
			
		||||
                FeatureFactory.getFeatureFactory()
 | 
			
		||||
                        .getAudioSharingFeatureProvider()
 | 
			
		||||
                        .createAudioSharingDevicePreferenceController(context, fragment, lifecycle);
 | 
			
		||||
        if (audioSharingController != null) {
 | 
			
		||||
            controllers.add(audioSharingController);
 | 
			
		||||
        }
 | 
			
		||||
        return controllers;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    boolean isAlwaysDiscoverable(String callingAppPackageName, String action) {
 | 
			
		||||
        return TextUtils.equals(SLICE_ACTION, action)
 | 
			
		||||
@@ -147,12 +122,5 @@ public class ConnectedDeviceDashboardFragment extends DashboardFragment {
 | 
			
		||||
 | 
			
		||||
    /** For Search. */
 | 
			
		||||
    public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
 | 
			
		||||
            new BaseSearchIndexProvider(R.xml.connected_devices) {
 | 
			
		||||
                @Override
 | 
			
		||||
                public List<AbstractPreferenceController> createPreferenceControllers(
 | 
			
		||||
                        Context context) {
 | 
			
		||||
                    return buildPreferenceControllers(
 | 
			
		||||
                            context, /* fragment= */ null, /* lifecycle= */ null);
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            new BaseSearchIndexProvider(R.xml.connected_devices);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.SettingsActivity;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingActivity extends SettingsActivity {
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedState) {
 | 
			
		||||
        super.onCreate(savedState);
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            finish();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected boolean isValidFragment(String fragmentName) {
 | 
			
		||||
        return AudioSharingDashboardFragment.class.getName().equals(fragmentName);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,127 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
public abstract class AudioSharingBasePreferenceController extends BasePreferenceController
 | 
			
		||||
        implements DefaultLifecycleObserver {
 | 
			
		||||
    private static final String TAG = "AudioSharingBasePreferenceController";
 | 
			
		||||
 | 
			
		||||
    private final BluetoothAdapter mBluetoothAdapter;
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothProfileManager mProfileManager;
 | 
			
		||||
    @Nullable protected final LocalBluetoothLeBroadcast mBroadcast;
 | 
			
		||||
    @Nullable protected Preference mPreference;
 | 
			
		||||
 | 
			
		||||
    public AudioSharingBasePreferenceController(Context context, String preferenceKey) {
 | 
			
		||||
        super(context, preferenceKey);
 | 
			
		||||
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 | 
			
		||||
        mBtManager = Utils.getLocalBtManager(context);
 | 
			
		||||
        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
 | 
			
		||||
        mBroadcast = mProfileManager == null ? null : mProfileManager.getLeAudioBroadcastProfile();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        mPreference = screen.findPreference(getPreferenceKey());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        updateVisibility();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Update the visibility of the preference. */
 | 
			
		||||
    protected void updateVisibility() {
 | 
			
		||||
        if (mPreference == null) {
 | 
			
		||||
            Log.d(TAG, "Skip updateVisibility, null preference");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            if (!isAvailable()) {
 | 
			
		||||
                                Log.w(TAG, "Skip updateVisibility, unavailable preference");
 | 
			
		||||
                                AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                                        mContext,
 | 
			
		||||
                                        () -> { // Check nullability to pass NullAway check
 | 
			
		||||
                                            if (mPreference != null) {
 | 
			
		||||
                                                mPreference.setVisible(false);
 | 
			
		||||
                                            }
 | 
			
		||||
                                        });
 | 
			
		||||
                                return;
 | 
			
		||||
                            }
 | 
			
		||||
                            boolean isBtOn = isBluetoothStateOn();
 | 
			
		||||
                            boolean isProfileReady =
 | 
			
		||||
                                    AudioSharingUtils.isAudioSharingProfileReady(mProfileManager);
 | 
			
		||||
                            boolean isBroadcasting = isBroadcasting();
 | 
			
		||||
                            boolean isVisible = isBtOn && isProfileReady && isBroadcasting;
 | 
			
		||||
                            Log.d(
 | 
			
		||||
                                    TAG,
 | 
			
		||||
                                    "updateVisibility, isBtOn = "
 | 
			
		||||
                                            + isBtOn
 | 
			
		||||
                                            + ", isProfileReady = "
 | 
			
		||||
                                            + isProfileReady
 | 
			
		||||
                                            + ", isBroadcasting = "
 | 
			
		||||
                                            + isBroadcasting);
 | 
			
		||||
                            AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                                    mContext,
 | 
			
		||||
                                    () -> { // Check nullability to pass NullAway check
 | 
			
		||||
                                        if (mPreference != null) {
 | 
			
		||||
                                            mPreference.setVisible(isVisible);
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Triggered when {@link AudioSharingDashboardFragment} receive onAudioSharingProfilesConnected
 | 
			
		||||
     * callbacks.
 | 
			
		||||
     */
 | 
			
		||||
    protected void onAudioSharingProfilesConnected() {}
 | 
			
		||||
 | 
			
		||||
    protected boolean isBroadcasting() {
 | 
			
		||||
        return mBroadcast != null && mBroadcast.isEnabled(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected boolean isBluetoothStateOn() {
 | 
			
		||||
        return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,91 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.DevicePreferenceCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingBluetoothDeviceUpdater extends BluetoothDeviceUpdater
 | 
			
		||||
        implements Preference.OnPreferenceClickListener {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioSharingBluetoothDeviceUpdater";
 | 
			
		||||
 | 
			
		||||
    private static final String PREF_KEY = "audio_sharing_bt";
 | 
			
		||||
 | 
			
		||||
    @Nullable private LocalBluetoothManager mLocalBtManager;
 | 
			
		||||
 | 
			
		||||
    public AudioSharingBluetoothDeviceUpdater(
 | 
			
		||||
            Context context,
 | 
			
		||||
            DevicePreferenceCallback devicePreferenceCallback,
 | 
			
		||||
            int metricsCategory) {
 | 
			
		||||
        super(context, devicePreferenceCallback, metricsCategory);
 | 
			
		||||
        mLocalBtManager = Utils.getLocalBluetoothManager(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        boolean isFilterMatched = false;
 | 
			
		||||
        if (isDeviceConnected(cachedDevice) && isDeviceInCachedDevicesList(cachedDevice)) {
 | 
			
		||||
            // If device is LE audio device and has a broadcast source,
 | 
			
		||||
            // it would show in audio sharing devices group.
 | 
			
		||||
            if (AudioSharingUtils.isFeatureEnabled()
 | 
			
		||||
                    && cachedDevice.isConnectedLeAudioDevice()
 | 
			
		||||
                    && BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, mLocalBtManager)) {
 | 
			
		||||
                isFilterMatched = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(
 | 
			
		||||
                TAG,
 | 
			
		||||
                "isFilterMatched() device : "
 | 
			
		||||
                        + cachedDevice.getName()
 | 
			
		||||
                        + ", isFilterMatched : "
 | 
			
		||||
                        + isFilterMatched);
 | 
			
		||||
        return isFilterMatched;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean onPreferenceClick(Preference preference) {
 | 
			
		||||
        mMetricsFeatureProvider.logClickedPreference(preference, mMetricsCategory);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String getPreferenceKey() {
 | 
			
		||||
        return PREF_KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String getLogTag() {
 | 
			
		||||
        return TAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void update(CachedBluetoothDevice cachedBluetoothDevice) {
 | 
			
		||||
        super.update(cachedBluetoothDevice);
 | 
			
		||||
        Log.d(TAG, "Map : " + mPreferenceMap);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,252 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcast;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
import androidx.preference.TwoStatePreference;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.TogglePreferenceController;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingCompatibilityPreferenceController extends TogglePreferenceController
 | 
			
		||||
        implements DefaultLifecycleObserver, LocalBluetoothProfileManager.ServiceListener {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioSharingCompatibilityPrefController";
 | 
			
		||||
 | 
			
		||||
    private static final String PREF_KEY = "audio_sharing_stream_compatibility";
 | 
			
		||||
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothProfileManager mProfileManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
 | 
			
		||||
    @Nullable private TwoStatePreference mPreference;
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
 | 
			
		||||
 | 
			
		||||
    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
 | 
			
		||||
            new BluetoothLeBroadcast.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStarted(int reason, int broadcastId) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onBroadcastStarted(), reason = "
 | 
			
		||||
                                    + reason
 | 
			
		||||
                                    + ", broadcastId = "
 | 
			
		||||
                                    + broadcastId);
 | 
			
		||||
                    updateEnabled();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStartFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastMetadataChanged(
 | 
			
		||||
                        int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStopped(int reason, int broadcastId) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onBroadcastStopped(), reason = "
 | 
			
		||||
                                    + reason
 | 
			
		||||
                                    + ", broadcastId = "
 | 
			
		||||
                                    + broadcastId);
 | 
			
		||||
                    updateEnabled();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStopFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastUpdated(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onPlaybackStarted(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onPlaybackStopped(int reason, int broadcastId) {}
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    public AudioSharingCompatibilityPreferenceController(Context context, String preferenceKey) {
 | 
			
		||||
        super(context, preferenceKey);
 | 
			
		||||
        mBtManager = Utils.getLocalBtManager(context);
 | 
			
		||||
        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
 | 
			
		||||
        mBroadcast = mProfileManager == null ? null : mProfileManager.getLeAudioBroadcastProfile();
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks, feature not support");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks, profile not ready");
 | 
			
		||||
            if (mProfileManager != null) {
 | 
			
		||||
                mProfileManager.addServiceListener(this);
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        registerCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip unregister callbacks, feature not support");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mProfileManager != null) {
 | 
			
		||||
            mProfileManager.removeServiceListener(this);
 | 
			
		||||
        }
 | 
			
		||||
        if (mBroadcast == null || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            Log.d(TAG, "Skip unregister callbacks, profile not ready");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mCallbacksRegistered.get()) {
 | 
			
		||||
            Log.d(TAG, "Unregister callbacks");
 | 
			
		||||
            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
 | 
			
		||||
            mCallbacksRegistered.set(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        mPreference = screen.findPreference(getPreferenceKey());
 | 
			
		||||
        updateEnabled();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getPreferenceKey() {
 | 
			
		||||
        return PREF_KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isChecked() {
 | 
			
		||||
        return mBroadcast != null && mBroadcast.getImproveCompatibility();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean setChecked(boolean isChecked) {
 | 
			
		||||
        if (mBroadcast == null || mBroadcast.getImproveCompatibility() == isChecked) {
 | 
			
		||||
            if (mBroadcast != null) {
 | 
			
		||||
                Log.d(TAG, "Skip setting improveCompatibility, unchanged");
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        mBroadcast.setImproveCompatibility(isChecked);
 | 
			
		||||
        // TODO: call updateBroadcast once framework change ready.
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onServiceConnected() {
 | 
			
		||||
        if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            registerCallbacks();
 | 
			
		||||
            AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                    mContext,
 | 
			
		||||
                    () -> {
 | 
			
		||||
                        if (mPreference != null) {
 | 
			
		||||
                            updateState(mPreference);
 | 
			
		||||
                        }
 | 
			
		||||
                        updateEnabled();
 | 
			
		||||
                    });
 | 
			
		||||
            if (mProfileManager != null) {
 | 
			
		||||
                mProfileManager.removeServiceListener(this);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onServiceDisconnected() {
 | 
			
		||||
        // Do nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getSliceHighlightMenuRes() {
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Test only: set callbacks registration state for test setup. */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setCallbacksRegistered(boolean registered) {
 | 
			
		||||
        mCallbacksRegistered.set(registered);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void registerCallbacks() {
 | 
			
		||||
        if (mBroadcast == null) {
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks, profile not ready");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!mCallbacksRegistered.get()) {
 | 
			
		||||
            Log.d(TAG, "Register callbacks");
 | 
			
		||||
            mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
 | 
			
		||||
            mCallbacksRegistered.set(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateEnabled() {
 | 
			
		||||
        int disabledDescriptionRes =
 | 
			
		||||
                R.string.audio_sharing_stream_compatibility_disabled_description;
 | 
			
		||||
        int descriptionRes = R.string.audio_sharing_stream_compatibility_description;
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            boolean isBroadcasting = AudioSharingUtils.isBroadcasting(mBtManager);
 | 
			
		||||
                            AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                                    mContext,
 | 
			
		||||
                                    () -> {
 | 
			
		||||
                                        if (mPreference != null) {
 | 
			
		||||
                                            mPreference.setEnabled(!isBroadcasting);
 | 
			
		||||
                                            mPreference.setSummary(
 | 
			
		||||
                                                    isBroadcasting
 | 
			
		||||
                                                            ? mContext.getString(
 | 
			
		||||
                                                                    disabledDescriptionRes)
 | 
			
		||||
                                                            : mContext.getString(descriptionRes));
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,71 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
import androidx.fragment.app.FragmentManager;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingConfirmDialogFragment extends InstrumentedDialogFragment {
 | 
			
		||||
    private static final String TAG = "AudioSharingConfirmDialog";
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        // TODO: add metrics category.
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the {@link AudioSharingConfirmDialogFragment} dialog.
 | 
			
		||||
     *
 | 
			
		||||
     * @param host The Fragment this dialog will be hosted.
 | 
			
		||||
     */
 | 
			
		||||
    public static void show(Fragment host) {
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) return;
 | 
			
		||||
        FragmentManager manager = host.getChildFragmentManager();
 | 
			
		||||
        AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
 | 
			
		||||
        if (dialog != null) {
 | 
			
		||||
            Log.d(TAG, "Dialog is showing, return.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(TAG, "Show up the confirm dialog.");
 | 
			
		||||
        AudioSharingConfirmDialogFragment dialogFrag = new AudioSharingConfirmDialogFragment();
 | 
			
		||||
        dialogFrag.show(manager, TAG);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
 | 
			
		||||
        AlertDialog dialog =
 | 
			
		||||
                AudioSharingDialogFactory.newBuilder(getActivity())
 | 
			
		||||
                        .setTitle(R.string.audio_sharing_confirm_dialog_title)
 | 
			
		||||
                        .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
 | 
			
		||||
                        .setIsCustomBodyEnabled(true)
 | 
			
		||||
                        .setCustomMessage(R.string.audio_sharing_comfirm_dialog_content)
 | 
			
		||||
                        .setPositiveButton(com.android.settings.R.string.okay, (d, w) -> dismiss())
 | 
			
		||||
                        .build();
 | 
			
		||||
        dialog.setCanceledOnTouchOutside(true);
 | 
			
		||||
        return dialog;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,111 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.SettingsActivity;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsCategoryController;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settings.widget.SettingsMainSwitchBar;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDashboardFragment extends DashboardFragment
 | 
			
		||||
        implements AudioSharingSwitchBarController.OnAudioSharingStateChangedListener {
 | 
			
		||||
    private static final String TAG = "AudioSharingDashboardFrag";
 | 
			
		||||
 | 
			
		||||
    SettingsMainSwitchBar mMainSwitchBar;
 | 
			
		||||
    private AudioSharingSwitchBarController mSwitchBarController;
 | 
			
		||||
    private AudioSharingDeviceVolumeGroupController mAudioSharingDeviceVolumeGroupController;
 | 
			
		||||
    private CallsAndAlarmsPreferenceController mCallsAndAlarmsPreferenceController;
 | 
			
		||||
    private AudioSharingPlaySoundPreferenceController mAudioSharingPlaySoundPreferenceController;
 | 
			
		||||
    private AudioStreamsCategoryController mAudioStreamsCategoryController;
 | 
			
		||||
 | 
			
		||||
    public AudioSharingDashboardFragment() {
 | 
			
		||||
        super();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        return SettingsEnums.AUDIO_SHARING_SETTINGS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String getLogTag() {
 | 
			
		||||
        return TAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getHelpResource() {
 | 
			
		||||
        return R.string.help_url_audio_sharing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected int getPreferenceScreenResId() {
 | 
			
		||||
        return R.xml.bluetooth_le_audio_sharing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onAttach(Context context) {
 | 
			
		||||
        super.onAttach(context);
 | 
			
		||||
        mAudioSharingDeviceVolumeGroupController =
 | 
			
		||||
                use(AudioSharingDeviceVolumeGroupController.class);
 | 
			
		||||
        mAudioSharingDeviceVolumeGroupController.init(this);
 | 
			
		||||
        mCallsAndAlarmsPreferenceController = use(CallsAndAlarmsPreferenceController.class);
 | 
			
		||||
        mCallsAndAlarmsPreferenceController.init(this);
 | 
			
		||||
        mAudioSharingPlaySoundPreferenceController =
 | 
			
		||||
                use(AudioSharingPlaySoundPreferenceController.class);
 | 
			
		||||
        mAudioStreamsCategoryController = use(AudioStreamsCategoryController.class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onActivityCreated(Bundle savedInstanceState) {
 | 
			
		||||
        super.onActivityCreated(savedInstanceState);
 | 
			
		||||
        // Assume we are in a SettingsActivity. This is only safe because we currently use
 | 
			
		||||
        // SettingsActivity as base for all preference fragments.
 | 
			
		||||
        final SettingsActivity activity = (SettingsActivity) getActivity();
 | 
			
		||||
        mMainSwitchBar = activity.getSwitchBar();
 | 
			
		||||
        mMainSwitchBar.setTitle(getText(R.string.audio_sharing_switch_title));
 | 
			
		||||
        mSwitchBarController = new AudioSharingSwitchBarController(activity, mMainSwitchBar, this);
 | 
			
		||||
        mSwitchBarController.init(this);
 | 
			
		||||
        getSettingsLifecycle().addObserver(mSwitchBarController);
 | 
			
		||||
        mMainSwitchBar.show();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onAudioSharingStateChanged() {
 | 
			
		||||
        updateVisibilityForAttachedPreferences();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onAudioSharingProfilesConnected() {
 | 
			
		||||
        onProfilesConnectedForAttachedPreferences();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateVisibilityForAttachedPreferences() {
 | 
			
		||||
        mAudioSharingDeviceVolumeGroupController.updateVisibility();
 | 
			
		||||
        mCallsAndAlarmsPreferenceController.updateVisibility();
 | 
			
		||||
        mAudioSharingPlaySoundPreferenceController.updateVisibility();
 | 
			
		||||
        mAudioStreamsCategoryController.updateVisibility();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onProfilesConnectedForAttachedPreferences() {
 | 
			
		||||
        mAudioSharingDeviceVolumeGroupController.onAudioSharingProfilesConnected();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,116 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
import android.widget.Button;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDeviceAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioSharingDeviceAdapter";
 | 
			
		||||
 | 
			
		||||
    private final Context mContext;
 | 
			
		||||
    private final List<AudioSharingDeviceItem> mDevices;
 | 
			
		||||
    private final OnClickListener mOnClickListener;
 | 
			
		||||
    private final ActionType mType;
 | 
			
		||||
 | 
			
		||||
    public AudioSharingDeviceAdapter(
 | 
			
		||||
            @NonNull Context context,
 | 
			
		||||
            @NonNull List<AudioSharingDeviceItem> devices,
 | 
			
		||||
            @NonNull OnClickListener listener,
 | 
			
		||||
            @NonNull ActionType type) {
 | 
			
		||||
        mContext = context;
 | 
			
		||||
        mDevices = devices;
 | 
			
		||||
        mOnClickListener = listener;
 | 
			
		||||
        mType = type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The action type when user click on the item.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>We choose the item text based on this type.
 | 
			
		||||
     */
 | 
			
		||||
    public enum ActionType {
 | 
			
		||||
        // Click on the item will add the item to audio sharing
 | 
			
		||||
        SHARE,
 | 
			
		||||
        // Click on the item will remove the item from audio sharing
 | 
			
		||||
        REMOVE,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private class AudioSharingDeviceViewHolder extends RecyclerView.ViewHolder {
 | 
			
		||||
        private final Button mButtonView;
 | 
			
		||||
 | 
			
		||||
        AudioSharingDeviceViewHolder(View view) {
 | 
			
		||||
            super(view);
 | 
			
		||||
            mButtonView = view.findViewById(R.id.device_button);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void bindView(int position) {
 | 
			
		||||
            if (mButtonView != null) {
 | 
			
		||||
                String btnText = switch (mType) {
 | 
			
		||||
                    case SHARE ->
 | 
			
		||||
                            mContext.getString(
 | 
			
		||||
                                    R.string.audio_sharing_share_with_button_label,
 | 
			
		||||
                                    mDevices.get(position).getName());
 | 
			
		||||
                    case REMOVE ->
 | 
			
		||||
                            mContext.getString(
 | 
			
		||||
                                    R.string.audio_sharing_disconnect_device_button_label,
 | 
			
		||||
                                    mDevices.get(position).getName());
 | 
			
		||||
                };
 | 
			
		||||
                mButtonView.setText(btnText);
 | 
			
		||||
                mButtonView.setOnClickListener(
 | 
			
		||||
                        v -> mOnClickListener.onClick(mDevices.get(position)));
 | 
			
		||||
            } else {
 | 
			
		||||
                Log.w(TAG, "bind view skipped due to button view is null");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
 | 
			
		||||
        View view =
 | 
			
		||||
                LayoutInflater.from(parent.getContext())
 | 
			
		||||
                        .inflate(R.layout.audio_sharing_device_item, parent, false);
 | 
			
		||||
        return new AudioSharingDeviceViewHolder(view);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
 | 
			
		||||
        ((AudioSharingDeviceViewHolder) holder).bindView(position);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getItemCount() {
 | 
			
		||||
        return mDevices.size();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public interface OnClickListener {
 | 
			
		||||
        /** Called when an item has been clicked. */
 | 
			
		||||
        void onClick(AudioSharingDeviceItem item);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,75 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.os.Parcel;
 | 
			
		||||
import android.os.Parcelable;
 | 
			
		||||
 | 
			
		||||
public final class AudioSharingDeviceItem implements Parcelable {
 | 
			
		||||
    private final String mName;
 | 
			
		||||
    private final int mGroupId;
 | 
			
		||||
    private final boolean mIsActive;
 | 
			
		||||
 | 
			
		||||
    public AudioSharingDeviceItem(String name, int groupId, boolean isActive) {
 | 
			
		||||
        mName = name;
 | 
			
		||||
        mGroupId = groupId;
 | 
			
		||||
        mIsActive = isActive;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public String getName() {
 | 
			
		||||
        return mName;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int getGroupId() {
 | 
			
		||||
        return mGroupId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean isActive() {
 | 
			
		||||
        return mIsActive;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioSharingDeviceItem(Parcel in) {
 | 
			
		||||
        mName = in.readString();
 | 
			
		||||
        mGroupId = in.readInt();
 | 
			
		||||
        mIsActive = in.readBoolean();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void writeToParcel(Parcel dest, int flags) {
 | 
			
		||||
        dest.writeString(mName);
 | 
			
		||||
        dest.writeInt(mGroupId);
 | 
			
		||||
        dest.writeBoolean(mIsActive);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int describeContents() {
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static final Creator<AudioSharingDeviceItem> CREATOR =
 | 
			
		||||
            new Creator<AudioSharingDeviceItem>() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public AudioSharingDeviceItem createFromParcel(Parcel in) {
 | 
			
		||||
                    return new AudioSharingDeviceItem(in);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public AudioSharingDeviceItem[] newArray(int size) {
 | 
			
		||||
                    return new AudioSharingDeviceItem[size];
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,486 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastAssistant;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
import androidx.preference.PreferenceGroup;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.SettingsActivity;
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.DevicePreferenceCallback;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.A2dpProfile;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothEventManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.HeadsetProfile;
 | 
			
		||||
import com.android.settingslib.bluetooth.HearingAidProfile;
 | 
			
		||||
import com.android.settingslib.bluetooth.LeAudioProfile;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDevicePreferenceController extends BasePreferenceController
 | 
			
		||||
        implements DefaultLifecycleObserver,
 | 
			
		||||
                DevicePreferenceCallback,
 | 
			
		||||
                BluetoothCallback,
 | 
			
		||||
                LocalBluetoothProfileManager.ServiceListener {
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioSharingDevicePrefController";
 | 
			
		||||
    private static final String KEY = "audio_sharing_device_list";
 | 
			
		||||
    private static final String KEY_AUDIO_SHARING_SETTINGS =
 | 
			
		||||
            "connected_device_audio_sharing_settings";
 | 
			
		||||
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final CachedBluetoothDeviceManager mDeviceManager;
 | 
			
		||||
    @Nullable private final BluetoothEventManager mEventManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothProfileManager mProfileManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    @Nullable private PreferenceGroup mPreferenceGroup;
 | 
			
		||||
    @Nullable private Preference mAudioSharingSettingsPreference;
 | 
			
		||||
    @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
 | 
			
		||||
    @Nullable private DashboardFragment mFragment;
 | 
			
		||||
    @Nullable private AudioSharingDialogHandler mDialogHandler;
 | 
			
		||||
    private AtomicBoolean mIntentHandled = new AtomicBoolean(false);
 | 
			
		||||
 | 
			
		||||
    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
 | 
			
		||||
            new BluetoothLeBroadcastAssistant.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStarted(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStartFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStopped(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStopFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAdded(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAddFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink,
 | 
			
		||||
                        @NonNull BluetoothLeBroadcastMetadata source,
 | 
			
		||||
                        int reason) {
 | 
			
		||||
                    AudioSharingUtils.toastMessage(
 | 
			
		||||
                            mContext,
 | 
			
		||||
                            String.format(
 | 
			
		||||
                                    Locale.US,
 | 
			
		||||
                                    "Fail to add source to %s reason %d",
 | 
			
		||||
                                    sink.getAddress(),
 | 
			
		||||
                                    reason));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceModified(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceModifyFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoved(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
                    Log.d(TAG, "onSourceRemoved: update sharing device list.");
 | 
			
		||||
                    if (mBluetoothDeviceUpdater != null) {
 | 
			
		||||
                        mBluetoothDeviceUpdater.forceUpdate();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoveFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
                    AudioSharingUtils.toastMessage(
 | 
			
		||||
                            mContext,
 | 
			
		||||
                            String.format(
 | 
			
		||||
                                    Locale.US,
 | 
			
		||||
                                    "Fail to remove source from %s reason %d",
 | 
			
		||||
                                    sink.getAddress(),
 | 
			
		||||
                                    reason));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onReceiveStateChanged(
 | 
			
		||||
                        @NonNull BluetoothDevice sink,
 | 
			
		||||
                        int sourceId,
 | 
			
		||||
                        @NonNull BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
                    if (BluetoothUtils.isConnected(state)) {
 | 
			
		||||
                        Log.d(TAG, "onSourceAdded: update sharing device list.");
 | 
			
		||||
                        if (mBluetoothDeviceUpdater != null) {
 | 
			
		||||
                            mBluetoothDeviceUpdater.forceUpdate();
 | 
			
		||||
                        }
 | 
			
		||||
                        if (mDeviceManager != null && mDialogHandler != null) {
 | 
			
		||||
                            CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(sink);
 | 
			
		||||
                            if (cachedDevice != null) {
 | 
			
		||||
                                mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    public AudioSharingDevicePreferenceController(Context context) {
 | 
			
		||||
        super(context, KEY);
 | 
			
		||||
        mBtManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
        mEventManager = mBtManager == null ? null : mBtManager.getEventManager();
 | 
			
		||||
        mDeviceManager = mBtManager == null ? null : mBtManager.getCachedDeviceManager();
 | 
			
		||||
        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
 | 
			
		||||
        mAssistant =
 | 
			
		||||
                mProfileManager == null
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : mProfileManager.getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip onStart(), feature is not supported.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)
 | 
			
		||||
                && mProfileManager != null) {
 | 
			
		||||
            Log.d(TAG, "Register profile service listener");
 | 
			
		||||
            mProfileManager.addServiceListener(this);
 | 
			
		||||
        }
 | 
			
		||||
        if (mEventManager == null
 | 
			
		||||
                || mAssistant == null
 | 
			
		||||
                || mDialogHandler == null
 | 
			
		||||
                || mBluetoothDeviceUpdater == null) {
 | 
			
		||||
            Log.d(TAG, "Skip onStart(), profile is not ready.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(TAG, "onStart() Register callbacks.");
 | 
			
		||||
        mEventManager.registerCallback(this);
 | 
			
		||||
        mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
 | 
			
		||||
        mDialogHandler.registerCallbacks(mExecutor);
 | 
			
		||||
        mBluetoothDeviceUpdater.registerCallback();
 | 
			
		||||
        mBluetoothDeviceUpdater.refreshPreference();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip onStop(), feature is not supported.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mProfileManager != null) {
 | 
			
		||||
            mProfileManager.removeServiceListener(this);
 | 
			
		||||
        }
 | 
			
		||||
        if (mEventManager == null
 | 
			
		||||
                || mAssistant == null
 | 
			
		||||
                || mDialogHandler == null
 | 
			
		||||
                || mBluetoothDeviceUpdater == null) {
 | 
			
		||||
            Log.d(TAG, "Skip onStop(), profile is not ready.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(TAG, "onStop() Unregister callbacks.");
 | 
			
		||||
        mEventManager.unregisterCallback(this);
 | 
			
		||||
        mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
 | 
			
		||||
        mDialogHandler.unregisterCallbacks();
 | 
			
		||||
        mBluetoothDeviceUpdater.unregisterCallback();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onServiceConnected() {
 | 
			
		||||
        if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            if (mProfileManager != null) {
 | 
			
		||||
                mProfileManager.removeServiceListener(this);
 | 
			
		||||
            }
 | 
			
		||||
            if (!mIntentHandled.get()) {
 | 
			
		||||
                Log.d(TAG, "onServiceConnected: handleDeviceClickFromIntent");
 | 
			
		||||
                handleDeviceClickFromIntent();
 | 
			
		||||
                mIntentHandled.set(true);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onServiceDisconnected() {
 | 
			
		||||
        // Do nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        mPreferenceGroup = screen.findPreference(KEY);
 | 
			
		||||
        if (mPreferenceGroup != null) {
 | 
			
		||||
            mAudioSharingSettingsPreference =
 | 
			
		||||
                    mPreferenceGroup.findPreference(KEY_AUDIO_SHARING_SETTINGS);
 | 
			
		||||
            mPreferenceGroup.setVisible(false);
 | 
			
		||||
        }
 | 
			
		||||
        if (mAudioSharingSettingsPreference != null) {
 | 
			
		||||
            mAudioSharingSettingsPreference.setVisible(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isAvailable()) {
 | 
			
		||||
            if (mBluetoothDeviceUpdater != null) {
 | 
			
		||||
                mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
 | 
			
		||||
                mBluetoothDeviceUpdater.forceUpdate();
 | 
			
		||||
            }
 | 
			
		||||
            if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
                if (!mIntentHandled.get()) {
 | 
			
		||||
                    Log.d(TAG, "displayPreference: profile ready, handleDeviceClickFromIntent");
 | 
			
		||||
                    handleDeviceClickFromIntent();
 | 
			
		||||
                    mIntentHandled.set(true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AudioSharingUtils.isFeatureEnabled() && mBluetoothDeviceUpdater != null
 | 
			
		||||
                ? AVAILABLE_UNSEARCHABLE
 | 
			
		||||
                : UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getPreferenceKey() {
 | 
			
		||||
        return KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onDeviceAdded(Preference preference) {
 | 
			
		||||
        if (mPreferenceGroup != null) {
 | 
			
		||||
            if (mPreferenceGroup.getPreferenceCount() == 1) {
 | 
			
		||||
                mPreferenceGroup.setVisible(true);
 | 
			
		||||
                if (mAudioSharingSettingsPreference != null) {
 | 
			
		||||
                    mAudioSharingSettingsPreference.setVisible(true);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            mPreferenceGroup.addPreference(preference);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onDeviceRemoved(Preference preference) {
 | 
			
		||||
        if (mPreferenceGroup != null) {
 | 
			
		||||
            mPreferenceGroup.removePreference(preference);
 | 
			
		||||
            if (mPreferenceGroup.getPreferenceCount() == 1) {
 | 
			
		||||
                mPreferenceGroup.setVisible(false);
 | 
			
		||||
                if (mAudioSharingSettingsPreference != null) {
 | 
			
		||||
                    mAudioSharingSettingsPreference.setVisible(false);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onProfileConnectionStateChanged(
 | 
			
		||||
            @NonNull CachedBluetoothDevice cachedDevice,
 | 
			
		||||
            @ConnectionState int state,
 | 
			
		||||
            int bluetoothProfile) {
 | 
			
		||||
        if (mDialogHandler == null || mAssistant == null || mFragment == null) {
 | 
			
		||||
            Log.d(TAG, "Ignore onProfileConnectionStateChanged, not init correctly");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!isMediaDevice(cachedDevice)) {
 | 
			
		||||
            Log.d(TAG, "Ignore onProfileConnectionStateChanged, not a media device");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // Close related dialogs if the BT remote device is disconnected.
 | 
			
		||||
        if (state == BluetoothAdapter.STATE_DISCONNECTED) {
 | 
			
		||||
            boolean isLeAudioSupported = AudioSharingUtils.isLeAudioSupported(cachedDevice);
 | 
			
		||||
            if (isLeAudioSupported
 | 
			
		||||
                    && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
 | 
			
		||||
                mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (!isLeAudioSupported && !cachedDevice.isConnected()) {
 | 
			
		||||
                mDialogHandler.closeOpeningDialogsForNonLeaDevice(cachedDevice);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (state != BluetoothAdapter.STATE_CONNECTED || !cachedDevice.getDevice().isConnected()) {
 | 
			
		||||
            Log.d(TAG, "Ignore onProfileConnectionStateChanged, not connected state");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        handleOnProfileStateChanged(cachedDevice, bluetoothProfile);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize the controller.
 | 
			
		||||
     *
 | 
			
		||||
     * @param fragment The fragment to provide the context and metrics category for {@link
 | 
			
		||||
     *     AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
 | 
			
		||||
     */
 | 
			
		||||
    public void init(DashboardFragment fragment) {
 | 
			
		||||
        mFragment = fragment;
 | 
			
		||||
        mBluetoothDeviceUpdater =
 | 
			
		||||
                new AudioSharingBluetoothDeviceUpdater(
 | 
			
		||||
                        fragment.getContext(),
 | 
			
		||||
                        AudioSharingDevicePreferenceController.this,
 | 
			
		||||
                        fragment.getMetricsCategory());
 | 
			
		||||
        mDialogHandler = new AudioSharingDialogHandler(mContext, fragment);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setBluetoothDeviceUpdater(@Nullable BluetoothDeviceUpdater bluetoothDeviceUpdater) {
 | 
			
		||||
        mBluetoothDeviceUpdater = bluetoothDeviceUpdater;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setDialogHandler(@Nullable AudioSharingDialogHandler dialogHandler) {
 | 
			
		||||
        mDialogHandler = dialogHandler;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setHostFragment(@Nullable DashboardFragment fragment) {
 | 
			
		||||
        mFragment = fragment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Test only: set intent handle state for test. */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setIntentHandled(boolean handled) {
 | 
			
		||||
        mIntentHandled.set(handled);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handleOnProfileStateChanged(
 | 
			
		||||
            @NonNull CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
 | 
			
		||||
        boolean isLeAudioSupported = AudioSharingUtils.isLeAudioSupported(cachedDevice);
 | 
			
		||||
        // For eligible (LE audio) remote device, we only check its connected LE audio assistant
 | 
			
		||||
        // profile.
 | 
			
		||||
        if (isLeAudioSupported
 | 
			
		||||
                && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "Ignore onProfileConnectionStateChanged, not the le assistant profile for"
 | 
			
		||||
                            + " le audio device");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile);
 | 
			
		||||
        // For ineligible (non LE audio) remote device, we only check its first connected profile.
 | 
			
		||||
        if (!isLeAudioSupported && !isFirstConnectedProfile) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "Ignore onProfileConnectionStateChanged, not the first connected profile for"
 | 
			
		||||
                            + " non le audio device");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "Start handling onProfileConnectionStateChanged for "
 | 
			
		||||
                            + cachedDevice.getDevice().getAnonymizedAddress());
 | 
			
		||||
        }
 | 
			
		||||
        // Check nullability to pass NullAway check
 | 
			
		||||
        if (mDialogHandler != null) {
 | 
			
		||||
            mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isMediaDevice(CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        return cachedDevice.getConnectableProfiles().stream()
 | 
			
		||||
                .anyMatch(
 | 
			
		||||
                        profile ->
 | 
			
		||||
                                profile instanceof A2dpProfile
 | 
			
		||||
                                        || profile instanceof HearingAidProfile
 | 
			
		||||
                                        || profile instanceof LeAudioProfile
 | 
			
		||||
                                        || profile instanceof HeadsetProfile);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isFirstConnectedProfile(
 | 
			
		||||
            CachedBluetoothDevice cachedDevice, int bluetoothProfile) {
 | 
			
		||||
        return cachedDevice.getProfiles().stream()
 | 
			
		||||
                .noneMatch(
 | 
			
		||||
                        profile ->
 | 
			
		||||
                                profile.getProfileId() != bluetoothProfile
 | 
			
		||||
                                        && profile.getConnectionStatus(cachedDevice.getDevice())
 | 
			
		||||
                                                == BluetoothProfile.STATE_CONNECTED);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle device click triggered by intent.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>When user click device from BT QS dialog, BT QS will send intent to open {@link
 | 
			
		||||
     * com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment} and handle device
 | 
			
		||||
     * click event under some conditions.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>This method will be called when displayPreference if the audio sharing profiles are ready.
 | 
			
		||||
     * If the profiles are not ready when the preference display, this method will be called when
 | 
			
		||||
     * onServiceConnected.
 | 
			
		||||
     */
 | 
			
		||||
    private void handleDeviceClickFromIntent() {
 | 
			
		||||
        if (mFragment == null
 | 
			
		||||
                || mFragment.getActivity() == null
 | 
			
		||||
                || mFragment.getActivity().getIntent() == null) {
 | 
			
		||||
            Log.d(TAG, "Skip handleDeviceClickFromIntent, fragment intent is null");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        Intent intent = mFragment.getActivity().getIntent();
 | 
			
		||||
        Bundle args = intent.getBundleExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS);
 | 
			
		||||
        BluetoothDevice device =
 | 
			
		||||
                args == null
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : args.getParcelable(EXTRA_BLUETOOTH_DEVICE, BluetoothDevice.class);
 | 
			
		||||
        CachedBluetoothDevice cachedDevice =
 | 
			
		||||
                (device == null || mDeviceManager == null)
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : mDeviceManager.findDevice(device);
 | 
			
		||||
        if (cachedDevice == null) {
 | 
			
		||||
            Log.d(TAG, "Skip handleDeviceClickFromIntent, device is null");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        // Check nullability to pass NullAway check
 | 
			
		||||
        if (device != null && !device.isConnected()) {
 | 
			
		||||
            Log.d(TAG, "handleDeviceClickFromIntent: connect");
 | 
			
		||||
            cachedDevice.connect();
 | 
			
		||||
        } else if (mDialogHandler != null) {
 | 
			
		||||
            Log.d(TAG, "handleDeviceClickFromIntent: trigger dialog handler");
 | 
			
		||||
            mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,177 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothCsipSetCoordinator;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.media.AudioManager;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.widget.SeekBar;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothDevicePreference;
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.DevicePreferenceCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.VolumeControlProfile;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDeviceVolumeControlUpdater extends BluetoothDeviceUpdater
 | 
			
		||||
        implements Preference.OnPreferenceClickListener {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioSharingDeviceVolumeControlUpdater";
 | 
			
		||||
 | 
			
		||||
    private static final String PREF_KEY = "audio_sharing_volume_control";
 | 
			
		||||
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final VolumeControlProfile mVolumeControl;
 | 
			
		||||
 | 
			
		||||
    public AudioSharingDeviceVolumeControlUpdater(
 | 
			
		||||
            Context context,
 | 
			
		||||
            DevicePreferenceCallback devicePreferenceCallback,
 | 
			
		||||
            int metricsCategory) {
 | 
			
		||||
        super(context, devicePreferenceCallback, metricsCategory);
 | 
			
		||||
        mBtManager = Utils.getLocalBluetoothManager(context);
 | 
			
		||||
        mVolumeControl =
 | 
			
		||||
                mBtManager == null
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : mBtManager.getProfileManager().getVolumeControlProfile();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isFilterMatched(CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        boolean isFilterMatched = false;
 | 
			
		||||
        if (isDeviceConnected(cachedDevice) && isDeviceInCachedDevicesList(cachedDevice)) {
 | 
			
		||||
            // If device is LE audio device and in a sharing session on current sharing device,
 | 
			
		||||
            // it would show in volume control group.
 | 
			
		||||
            if (cachedDevice.isConnectedLeAudioDevice()
 | 
			
		||||
                    && AudioSharingUtils.isBroadcasting(mBtManager)
 | 
			
		||||
                    && BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, mBtManager)) {
 | 
			
		||||
                isFilterMatched = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(
 | 
			
		||||
                TAG,
 | 
			
		||||
                "isFilterMatched() device : "
 | 
			
		||||
                        + cachedDevice.getName()
 | 
			
		||||
                        + ", isFilterMatched : "
 | 
			
		||||
                        + isFilterMatched);
 | 
			
		||||
        return isFilterMatched;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean onPreferenceClick(Preference preference) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void addPreference(CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        if (cachedDevice == null) return;
 | 
			
		||||
        final BluetoothDevice device = cachedDevice.getDevice();
 | 
			
		||||
        if (!mPreferenceMap.containsKey(device)) {
 | 
			
		||||
            SeekBar.OnSeekBarChangeListener listener =
 | 
			
		||||
                    new SeekBar.OnSeekBarChangeListener() {
 | 
			
		||||
                        @Override
 | 
			
		||||
                        public void onProgressChanged(
 | 
			
		||||
                                SeekBar seekBar, int progress, boolean fromUser) {}
 | 
			
		||||
 | 
			
		||||
                        @Override
 | 
			
		||||
                        public void onStartTrackingTouch(SeekBar seekBar) {}
 | 
			
		||||
 | 
			
		||||
                        @Override
 | 
			
		||||
                        public void onStopTrackingTouch(SeekBar seekBar) {
 | 
			
		||||
                            int progress = seekBar.getProgress();
 | 
			
		||||
                            int groupId = AudioSharingUtils.getGroupId(cachedDevice);
 | 
			
		||||
                            if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
 | 
			
		||||
                                    && groupId
 | 
			
		||||
                                            == AudioSharingUtils.getFallbackActiveGroupId(
 | 
			
		||||
                                                    mContext)) {
 | 
			
		||||
                                // Set media stream volume for primary buds, audio manager will
 | 
			
		||||
                                // update all buds volume in the audio sharing.
 | 
			
		||||
                                setAudioManagerStreamVolume(progress);
 | 
			
		||||
                            } else {
 | 
			
		||||
                                // Set buds volume for other buds.
 | 
			
		||||
                                setDeviceVolume(cachedDevice, progress);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    };
 | 
			
		||||
            AudioSharingDeviceVolumePreference vPreference =
 | 
			
		||||
                    new AudioSharingDeviceVolumePreference(mPrefContext, cachedDevice);
 | 
			
		||||
            vPreference.initialize();
 | 
			
		||||
            vPreference.setOnSeekBarChangeListener(listener);
 | 
			
		||||
            vPreference.setKey(getPreferenceKey());
 | 
			
		||||
            vPreference.setIcon(com.android.settingslib.R.drawable.ic_bt_untethered_earbuds);
 | 
			
		||||
            vPreference.setTitle(cachedDevice.getName());
 | 
			
		||||
            mPreferenceMap.put(device, vPreference);
 | 
			
		||||
            mDevicePreferenceCallback.onDeviceAdded(vPreference);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String getPreferenceKey() {
 | 
			
		||||
        return PREF_KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String getLogTag() {
 | 
			
		||||
        return TAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void update(CachedBluetoothDevice cachedBluetoothDevice) {
 | 
			
		||||
        super.update(cachedBluetoothDevice);
 | 
			
		||||
        Log.d(TAG, "Map : " + mPreferenceMap);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void addPreference(
 | 
			
		||||
            CachedBluetoothDevice cachedDevice, @BluetoothDevicePreference.SortType int type) {}
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void launchDeviceDetails(Preference preference) {}
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void refreshPreference() {}
 | 
			
		||||
 | 
			
		||||
    private void setDeviceVolume(CachedBluetoothDevice cachedDevice, int progress) {
 | 
			
		||||
        if (mVolumeControl != null) {
 | 
			
		||||
            mVolumeControl.setDeviceVolume(
 | 
			
		||||
                    cachedDevice.getDevice(), progress, /* isGroupOp= */ true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void setAudioManagerStreamVolume(int progress) {
 | 
			
		||||
        int seekbarRange =
 | 
			
		||||
                AudioSharingDeviceVolumePreference.MAX_VOLUME
 | 
			
		||||
                        - AudioSharingDeviceVolumePreference.MIN_VOLUME;
 | 
			
		||||
        try {
 | 
			
		||||
            AudioManager audioManager = mContext.getSystemService(AudioManager.class);
 | 
			
		||||
            int streamVolumeRange =
 | 
			
		||||
                    audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
 | 
			
		||||
                            - audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
 | 
			
		||||
            int volume = Math.round((float) progress * streamVolumeRange / seekbarRange);
 | 
			
		||||
            audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
 | 
			
		||||
        } catch (RuntimeException e) {
 | 
			
		||||
            Log.e(TAG, "Fail to setAudioManagerStreamVolumeForFallbackDevice, error = " + e);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,427 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID;
 | 
			
		||||
 | 
			
		||||
import android.annotation.IntRange;
 | 
			
		||||
import android.bluetooth.BluetoothCsipSetCoordinator;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastAssistant;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.bluetooth.BluetoothVolumeControl;
 | 
			
		||||
import android.content.ContentResolver;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.database.ContentObserver;
 | 
			
		||||
import android.media.AudioManager;
 | 
			
		||||
import android.os.Handler;
 | 
			
		||||
import android.os.Looper;
 | 
			
		||||
import android.provider.Settings;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
import androidx.preference.PreferenceGroup;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothDeviceUpdater;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.DevicePreferenceCallback;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.VolumeControlProfile;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDeviceVolumeGroupController extends AudioSharingBasePreferenceController
 | 
			
		||||
        implements DevicePreferenceCallback {
 | 
			
		||||
    private static final String TAG = "AudioSharingDeviceVolumeGroupController";
 | 
			
		||||
    private static final String KEY = "audio_sharing_device_volume_group";
 | 
			
		||||
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothProfileManager mProfileManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
 | 
			
		||||
    @Nullable private final VolumeControlProfile mVolumeControl;
 | 
			
		||||
    @Nullable private final ContentResolver mContentResolver;
 | 
			
		||||
    @Nullable private BluetoothDeviceUpdater mBluetoothDeviceUpdater;
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    private final ContentObserver mSettingsObserver;
 | 
			
		||||
    @Nullable private PreferenceGroup mPreferenceGroup;
 | 
			
		||||
    private List<AudioSharingDeviceVolumePreference> mVolumePreferences = new ArrayList<>();
 | 
			
		||||
    private Map<Integer, Integer> mValueMap = new HashMap<Integer, Integer>();
 | 
			
		||||
    private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
 | 
			
		||||
 | 
			
		||||
    private BluetoothVolumeControl.Callback mVolumeControlCallback =
 | 
			
		||||
            new BluetoothVolumeControl.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onVolumeOffsetChanged(
 | 
			
		||||
                        @NonNull BluetoothDevice device, int volumeOffset) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onDeviceVolumeChanged(
 | 
			
		||||
                        @NonNull BluetoothDevice device,
 | 
			
		||||
                        @IntRange(from = -255, to = 255) int volume) {
 | 
			
		||||
                    CachedBluetoothDevice cachedDevice =
 | 
			
		||||
                            mBtManager == null
 | 
			
		||||
                                    ? null
 | 
			
		||||
                                    : mBtManager.getCachedDeviceManager().findDevice(device);
 | 
			
		||||
                    if (cachedDevice == null) return;
 | 
			
		||||
                    int groupId = AudioSharingUtils.getGroupId(cachedDevice);
 | 
			
		||||
                    mValueMap.put(groupId, volume);
 | 
			
		||||
                    for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) {
 | 
			
		||||
                        if (preference.getCachedDevice() != null
 | 
			
		||||
                                && AudioSharingUtils.getGroupId(preference.getCachedDevice())
 | 
			
		||||
                                        == groupId) {
 | 
			
		||||
                            // If the callback return invalid volume, try to
 | 
			
		||||
                            // get the volume from AudioManager.STREAM_MUSIC
 | 
			
		||||
                            int finalVolume = getAudioVolumeIfNeeded(volume);
 | 
			
		||||
                            Log.d(
 | 
			
		||||
                                    TAG,
 | 
			
		||||
                                    "onDeviceVolumeChanged: set volume to "
 | 
			
		||||
                                            + finalVolume
 | 
			
		||||
                                            + " for "
 | 
			
		||||
                                            + device.getAnonymizedAddress());
 | 
			
		||||
                            mContext.getMainExecutor()
 | 
			
		||||
                                    .execute(() -> preference.setProgress(finalVolume));
 | 
			
		||||
                            break;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
 | 
			
		||||
            new BluetoothLeBroadcastAssistant.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStarted(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStartFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStopped(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStopFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAdded(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAddFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink,
 | 
			
		||||
                        @NonNull BluetoothLeBroadcastMetadata source,
 | 
			
		||||
                        int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceModified(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceModifyFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoved(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
                    Log.d(TAG, "onSourceRemoved: update volume list.");
 | 
			
		||||
                    if (mBluetoothDeviceUpdater != null) {
 | 
			
		||||
                        mBluetoothDeviceUpdater.forceUpdate();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoveFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onReceiveStateChanged(
 | 
			
		||||
                        @NonNull BluetoothDevice sink,
 | 
			
		||||
                        int sourceId,
 | 
			
		||||
                        @NonNull BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
                    if (BluetoothUtils.isConnected(state)) {
 | 
			
		||||
                        Log.d(TAG, "onReceiveStateChanged: synced, update volume list.");
 | 
			
		||||
                        if (mBluetoothDeviceUpdater != null) {
 | 
			
		||||
                            mBluetoothDeviceUpdater.forceUpdate();
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    public AudioSharingDeviceVolumeGroupController(Context context) {
 | 
			
		||||
        super(context, KEY);
 | 
			
		||||
        mBtManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
 | 
			
		||||
        mAssistant =
 | 
			
		||||
                mProfileManager == null
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : mProfileManager.getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
        mVolumeControl = mProfileManager == null ? null : mProfileManager.getVolumeControlProfile();
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
        mContentResolver = context.getContentResolver();
 | 
			
		||||
        mSettingsObserver = new SettingsObserver();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private class SettingsObserver extends ContentObserver {
 | 
			
		||||
        SettingsObserver() {
 | 
			
		||||
            super(new Handler(Looper.getMainLooper()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onChange(boolean selfChange) {
 | 
			
		||||
            Log.d(TAG, "onChange, fallback device group id has been changed");
 | 
			
		||||
            for (AudioSharingDeviceVolumePreference preference : mVolumePreferences) {
 | 
			
		||||
                preference.setOrder(getPreferenceOrderForDevice(preference.getCachedDevice()));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        super.onStart(owner);
 | 
			
		||||
        registerCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        super.onStop(owner);
 | 
			
		||||
        unregisterCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onDestroy(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        mVolumePreferences.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
 | 
			
		||||
        mPreferenceGroup = screen.findPreference(KEY);
 | 
			
		||||
        if (mPreferenceGroup != null) {
 | 
			
		||||
            mPreferenceGroup.setVisible(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (isAvailable() && mBluetoothDeviceUpdater != null) {
 | 
			
		||||
            mBluetoothDeviceUpdater.setPrefContext(screen.getContext());
 | 
			
		||||
            mBluetoothDeviceUpdater.forceUpdate();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getPreferenceKey() {
 | 
			
		||||
        return KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onDeviceAdded(Preference preference) {
 | 
			
		||||
        if (mPreferenceGroup != null) {
 | 
			
		||||
            if (mPreferenceGroup.getPreferenceCount() == 0) {
 | 
			
		||||
                mPreferenceGroup.setVisible(true);
 | 
			
		||||
            }
 | 
			
		||||
            mPreferenceGroup.addPreference(preference);
 | 
			
		||||
        }
 | 
			
		||||
        if (preference instanceof AudioSharingDeviceVolumePreference) {
 | 
			
		||||
            var volumePref = (AudioSharingDeviceVolumePreference) preference;
 | 
			
		||||
            CachedBluetoothDevice cachedDevice = volumePref.getCachedDevice();
 | 
			
		||||
            volumePref.setOrder(getPreferenceOrderForDevice(cachedDevice));
 | 
			
		||||
            mVolumePreferences.add(volumePref);
 | 
			
		||||
            if (volumePref.getProgress() > 0) return;
 | 
			
		||||
            int volume = mValueMap.getOrDefault(AudioSharingUtils.getGroupId(cachedDevice), -1);
 | 
			
		||||
            // If the volume is invalid, try to get the volume from AudioManager.STREAM_MUSIC
 | 
			
		||||
            int finalVolume = getAudioVolumeIfNeeded(volume);
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "onDeviceAdded: set volume to "
 | 
			
		||||
                            + finalVolume
 | 
			
		||||
                            + " for "
 | 
			
		||||
                            + cachedDevice.getDevice().getAnonymizedAddress());
 | 
			
		||||
            AudioSharingUtils.postOnMainThread(mContext, () -> volumePref.setProgress(finalVolume));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onDeviceRemoved(Preference preference) {
 | 
			
		||||
        if (mPreferenceGroup != null) {
 | 
			
		||||
            mPreferenceGroup.removePreference(preference);
 | 
			
		||||
            if (mPreferenceGroup.getPreferenceCount() == 0) {
 | 
			
		||||
                mPreferenceGroup.setVisible(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (preference instanceof AudioSharingDeviceVolumePreference) {
 | 
			
		||||
            var volumePref = (AudioSharingDeviceVolumePreference) preference;
 | 
			
		||||
            if (mVolumePreferences.contains(volumePref)) {
 | 
			
		||||
                mVolumePreferences.remove(volumePref);
 | 
			
		||||
            }
 | 
			
		||||
            CachedBluetoothDevice device = volumePref.getCachedDevice();
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "onDeviceRemoved: "
 | 
			
		||||
                            + (device == null
 | 
			
		||||
                                    ? "null"
 | 
			
		||||
                                    : device.getDevice().getAnonymizedAddress()));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void updateVisibility() {
 | 
			
		||||
        if (mPreferenceGroup != null && mPreferenceGroup.getPreferenceCount() == 0) {
 | 
			
		||||
            mPreferenceGroup.setVisible(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        super.updateVisibility();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onAudioSharingProfilesConnected() {
 | 
			
		||||
        registerCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize the controller.
 | 
			
		||||
     *
 | 
			
		||||
     * @param fragment The fragment to provide the context and metrics category for {@link
 | 
			
		||||
     *     AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs.
 | 
			
		||||
     */
 | 
			
		||||
    public void init(DashboardFragment fragment) {
 | 
			
		||||
        mBluetoothDeviceUpdater =
 | 
			
		||||
                new AudioSharingDeviceVolumeControlUpdater(
 | 
			
		||||
                        fragment.getContext(),
 | 
			
		||||
                        AudioSharingDeviceVolumeGroupController.this,
 | 
			
		||||
                        fragment.getMetricsCategory());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setDeviceUpdater(@Nullable AudioSharingDeviceVolumeControlUpdater updater) {
 | 
			
		||||
        mBluetoothDeviceUpdater = updater;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Test only: set callback registration status in tests. */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setCallbacksRegistered(boolean registered) {
 | 
			
		||||
        mCallbacksRegistered.set(registered);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Test only: set volume map in tests. */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setVolumeMap(@Nullable Map<Integer, Integer> map) {
 | 
			
		||||
        mValueMap.clear();
 | 
			
		||||
        mValueMap.putAll(map);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Test only: set value for private preferenceGroup in tests. */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setPreferenceGroup(@Nullable PreferenceGroup group) {
 | 
			
		||||
        mPreferenceGroup = group;
 | 
			
		||||
        mPreference = group;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    ContentObserver getSettingsObserver() {
 | 
			
		||||
        return mSettingsObserver;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void registerCallbacks() {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip registerCallbacks(). Feature is not available.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mAssistant == null
 | 
			
		||||
                || mVolumeControl == null
 | 
			
		||||
                || mBluetoothDeviceUpdater == null
 | 
			
		||||
                || mContentResolver == null
 | 
			
		||||
                || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            Log.d(TAG, "Skip registerCallbacks(). Profile is not ready.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!mCallbacksRegistered.get()) {
 | 
			
		||||
            Log.d(TAG, "registerCallbacks()");
 | 
			
		||||
            mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
 | 
			
		||||
            mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback);
 | 
			
		||||
            mBluetoothDeviceUpdater.registerCallback();
 | 
			
		||||
            mContentResolver.registerContentObserver(
 | 
			
		||||
                    Settings.Secure.getUriFor(SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID),
 | 
			
		||||
                    false,
 | 
			
		||||
                    mSettingsObserver);
 | 
			
		||||
            mCallbacksRegistered.set(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void unregisterCallbacks() {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip unregister callbacks. Feature is not available.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mAssistant == null
 | 
			
		||||
                || mVolumeControl == null
 | 
			
		||||
                || mBluetoothDeviceUpdater == null
 | 
			
		||||
                || mContentResolver == null
 | 
			
		||||
                || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            Log.d(TAG, "Skip unregisterCallbacks(). Profile is not ready.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mCallbacksRegistered.get()) {
 | 
			
		||||
            Log.d(TAG, "unregisterCallbacks()");
 | 
			
		||||
            mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
 | 
			
		||||
            mVolumeControl.unregisterCallback(mVolumeControlCallback);
 | 
			
		||||
            mBluetoothDeviceUpdater.unregisterCallback();
 | 
			
		||||
            mContentResolver.unregisterContentObserver(mSettingsObserver);
 | 
			
		||||
            mValueMap.clear();
 | 
			
		||||
            mCallbacksRegistered.set(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private int getAudioVolumeIfNeeded(int volume) {
 | 
			
		||||
        if (volume >= 0) return volume;
 | 
			
		||||
        try {
 | 
			
		||||
            AudioManager audioManager = mContext.getSystemService(AudioManager.class);
 | 
			
		||||
            int max = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
 | 
			
		||||
            int min = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC);
 | 
			
		||||
            return Math.round(
 | 
			
		||||
                    audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) * 255f / (max - min));
 | 
			
		||||
        } catch (RuntimeException e) {
 | 
			
		||||
            Log.e(TAG, "Fail to fetch current music stream volume, error = " + e);
 | 
			
		||||
            return volume;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private int getPreferenceOrderForDevice(@NonNull CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        int groupId = AudioSharingUtils.getGroupId(cachedDevice);
 | 
			
		||||
        // The fallback device rank first among the audio sharing device list.
 | 
			
		||||
        return (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
 | 
			
		||||
                        && groupId == AudioSharingUtils.getFallbackActiveGroupId(mContext))
 | 
			
		||||
                ? 0
 | 
			
		||||
                : 1;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.widget.SeekBar;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.widget.SeekBarPreference;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDeviceVolumePreference extends SeekBarPreference {
 | 
			
		||||
    public static final int MIN_VOLUME = 0;
 | 
			
		||||
    public static final int MAX_VOLUME = 255;
 | 
			
		||||
 | 
			
		||||
    private final CachedBluetoothDevice mCachedDevice;
 | 
			
		||||
    @Nullable protected SeekBar mSeekBar;
 | 
			
		||||
 | 
			
		||||
    public AudioSharingDeviceVolumePreference(
 | 
			
		||||
            Context context, @NonNull CachedBluetoothDevice device) {
 | 
			
		||||
        super(context);
 | 
			
		||||
        setLayoutResource(R.layout.preference_volume_slider);
 | 
			
		||||
        mCachedDevice = device;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @NonNull
 | 
			
		||||
    public CachedBluetoothDevice getCachedDevice() {
 | 
			
		||||
        return mCachedDevice;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize {@link AudioSharingDeviceVolumePreference}.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>Need to be called after creating the preference.
 | 
			
		||||
     */
 | 
			
		||||
    public void initialize() {
 | 
			
		||||
        setMax(MAX_VOLUME);
 | 
			
		||||
        setMin(MIN_VOLUME);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,348 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.DialogInterface;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.widget.Button;
 | 
			
		||||
import android.widget.ImageView;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.DrawableRes;
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.StringRes;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager;
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.CheckReturnValue;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDialogFactory {
 | 
			
		||||
    private static final String TAG = "AudioSharingDialogFactory";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initializes a builder for the dialog to be shown for audio sharing.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The {@link Context} that will be used to create the dialog.
 | 
			
		||||
     * @return A configurable builder for the dialog.
 | 
			
		||||
     */
 | 
			
		||||
    @NonNull
 | 
			
		||||
    public static AudioSharingDialogFactory.DialogBuilder newBuilder(@NonNull Context context) {
 | 
			
		||||
        return new AudioSharingDialogFactory.DialogBuilder(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Builder class with configurable options for the dialog to be shown for audio sharing. */
 | 
			
		||||
    public static class DialogBuilder {
 | 
			
		||||
        private Context mContext;
 | 
			
		||||
        private AlertDialog.Builder mBuilder;
 | 
			
		||||
        private View mCustomTitle;
 | 
			
		||||
        private View mCustomBody;
 | 
			
		||||
        private boolean mIsCustomBodyEnabled;
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Private constructor for the dialog builder class. Should not be invoked directly;
 | 
			
		||||
         * instead, use {@link AudioSharingDialogFactory#newBuilder(Context)}.
 | 
			
		||||
         *
 | 
			
		||||
         * @param context The {@link Context} that will be used to create the dialog.
 | 
			
		||||
         */
 | 
			
		||||
        private DialogBuilder(@NonNull Context context) {
 | 
			
		||||
            mContext = context;
 | 
			
		||||
            mBuilder = new AlertDialog.Builder(context);
 | 
			
		||||
            LayoutInflater inflater = LayoutInflater.from(mBuilder.getContext());
 | 
			
		||||
            mCustomTitle =
 | 
			
		||||
                    inflater.inflate(R.layout.dialog_custom_title_audio_sharing, /* root= */ null);
 | 
			
		||||
            mCustomBody =
 | 
			
		||||
                    inflater.inflate(R.layout.dialog_custom_body_audio_sharing, /* parent= */ null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets title of the dialog custom title.
 | 
			
		||||
         *
 | 
			
		||||
         * @param titleRes Resource ID of the string to be used for the dialog title.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setTitle(@StringRes int titleRes) {
 | 
			
		||||
            TextView title = mCustomTitle.findViewById(R.id.title_text);
 | 
			
		||||
            title.setText(titleRes);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets title of the dialog custom title.
 | 
			
		||||
         *
 | 
			
		||||
         * @param titleText The text to be used for the title.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setTitle(@NonNull CharSequence titleText) {
 | 
			
		||||
            TextView title = mCustomTitle.findViewById(R.id.title_text);
 | 
			
		||||
            title.setText(titleText);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the title icon of the dialog custom title.
 | 
			
		||||
         *
 | 
			
		||||
         * @param iconRes The text to be used for the title.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setTitleIcon(@DrawableRes int iconRes) {
 | 
			
		||||
            ImageView icon = mCustomTitle.findViewById(R.id.title_icon);
 | 
			
		||||
            icon.setImageResource(iconRes);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the message body of the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param messageRes Resource ID of the string to be used for the message body.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setMessage(@StringRes int messageRes) {
 | 
			
		||||
            mBuilder.setMessage(messageRes);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the message body of the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param message The text to be used for the message body.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setMessage(@NonNull CharSequence message) {
 | 
			
		||||
            mBuilder.setMessage(message);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /** Whether to use custom body. */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setIsCustomBodyEnabled(
 | 
			
		||||
                boolean isCustomBodyEnabled) {
 | 
			
		||||
            mIsCustomBodyEnabled = isCustomBodyEnabled;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the custom image of the dialog custom body.
 | 
			
		||||
         *
 | 
			
		||||
         * @param iconRes The text to be used for the title.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setCustomImage(@DrawableRes int iconRes) {
 | 
			
		||||
            ImageView image = mCustomBody.findViewById(R.id.description_image);
 | 
			
		||||
            image.setImageResource(iconRes);
 | 
			
		||||
            image.setVisibility(View.VISIBLE);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the custom message of the dialog custom body.
 | 
			
		||||
         *
 | 
			
		||||
         * @param messageRes Resource ID of the string to be used for the message body.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setCustomMessage(@StringRes int messageRes) {
 | 
			
		||||
            TextView subTitle = mCustomBody.findViewById(R.id.description_text);
 | 
			
		||||
            subTitle.setText(messageRes);
 | 
			
		||||
            subTitle.setVisibility(View.VISIBLE);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the custom message of the dialog custom body.
 | 
			
		||||
         *
 | 
			
		||||
         * @param message The text to be used for the custom message body.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setCustomMessage(
 | 
			
		||||
                @NonNull CharSequence message) {
 | 
			
		||||
            TextView subTitle = mCustomBody.findViewById(R.id.description_text);
 | 
			
		||||
            subTitle.setText(message);
 | 
			
		||||
            subTitle.setVisibility(View.VISIBLE);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the custom device actions of the dialog custom body.
 | 
			
		||||
         *
 | 
			
		||||
         * @param adapter The adapter for device items to build dialog actions.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setCustomDeviceActions(
 | 
			
		||||
                @NonNull AudioSharingDeviceAdapter adapter) {
 | 
			
		||||
            RecyclerView recyclerView = mCustomBody.findViewById(R.id.device_btn_list);
 | 
			
		||||
            recyclerView.setAdapter(adapter);
 | 
			
		||||
            recyclerView.setLayoutManager(
 | 
			
		||||
                    new LinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, false));
 | 
			
		||||
            recyclerView.setVisibility(View.VISIBLE);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the positive button label and listener for the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param labelRes Resource ID of the string to be used for the positive button label.
 | 
			
		||||
         * @param listener The listener to be invoked when the positive button is pressed.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setPositiveButton(
 | 
			
		||||
                @StringRes int labelRes, @NonNull DialogInterface.OnClickListener listener) {
 | 
			
		||||
            mBuilder.setPositiveButton(labelRes, listener);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the positive button label and listener for the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param label The text to be used for the positive button label.
 | 
			
		||||
         * @param listener The listener to be invoked when the positive button is pressed.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setPositiveButton(
 | 
			
		||||
                @NonNull CharSequence label, @NonNull DialogInterface.OnClickListener listener) {
 | 
			
		||||
            mBuilder.setPositiveButton(label, listener);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the custom positive button label and listener for the dialog custom body.
 | 
			
		||||
         *
 | 
			
		||||
         * @param labelRes Resource ID of the string to be used for the positive button label.
 | 
			
		||||
         * @param listener The listener to be invoked when the positive button is pressed.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setCustomPositiveButton(
 | 
			
		||||
                @StringRes int labelRes, @NonNull View.OnClickListener listener) {
 | 
			
		||||
            Button positiveBtn = mCustomBody.findViewById(R.id.positive_btn);
 | 
			
		||||
            positiveBtn.setText(labelRes);
 | 
			
		||||
            positiveBtn.setOnClickListener(listener);
 | 
			
		||||
            positiveBtn.setVisibility(View.VISIBLE);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the custom positive button label and listener for the dialog custom body.
 | 
			
		||||
         *
 | 
			
		||||
         * @param label The text to be used for the positive button label.
 | 
			
		||||
         * @param listener The listener to be invoked when the positive button is pressed.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setCustomPositiveButton(
 | 
			
		||||
                @NonNull CharSequence label, @NonNull View.OnClickListener listener) {
 | 
			
		||||
            Button positiveBtn = mCustomBody.findViewById(R.id.positive_btn);
 | 
			
		||||
            positiveBtn.setText(label);
 | 
			
		||||
            positiveBtn.setOnClickListener(listener);
 | 
			
		||||
            positiveBtn.setVisibility(View.VISIBLE);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the negative button label and listener for the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param labelRes Resource ID of the string to be used for the negative button label.
 | 
			
		||||
         * @param listener The listener to be invoked when the negative button is pressed.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setNegativeButton(
 | 
			
		||||
                @StringRes int labelRes, @NonNull DialogInterface.OnClickListener listener) {
 | 
			
		||||
            mBuilder.setNegativeButton(labelRes, listener);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the negative button label and listener for the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param label The text to be used for the negative button label.
 | 
			
		||||
         * @param listener The listener to be invoked when the negative button is pressed.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setNegativeButton(
 | 
			
		||||
                @NonNull CharSequence label, @NonNull DialogInterface.OnClickListener listener) {
 | 
			
		||||
            mBuilder.setNegativeButton(label, listener);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the custom negative button label and listener for the dialog custom body.
 | 
			
		||||
         *
 | 
			
		||||
         * @param labelRes Resource ID of the string to be used for the negative button label.
 | 
			
		||||
         * @param listener The listener to be invoked when the negative button is pressed.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setCustomNegativeButton(
 | 
			
		||||
                @StringRes int labelRes, @NonNull View.OnClickListener listener) {
 | 
			
		||||
            Button negativeBtn = mCustomBody.findViewById(R.id.negative_btn);
 | 
			
		||||
            negativeBtn.setText(labelRes);
 | 
			
		||||
            negativeBtn.setOnClickListener(listener);
 | 
			
		||||
            negativeBtn.setVisibility(View.VISIBLE);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the custom negative button label and listener for the dialog custom body.
 | 
			
		||||
         *
 | 
			
		||||
         * @param label The text to be used for the negative button label.
 | 
			
		||||
         * @param listener The listener to be invoked when the negative button is pressed.
 | 
			
		||||
         * @return This builder.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        public AudioSharingDialogFactory.DialogBuilder setCustomNegativeButton(
 | 
			
		||||
                @NonNull CharSequence label, @NonNull View.OnClickListener listener) {
 | 
			
		||||
            Button negativeBtn = mCustomBody.findViewById(R.id.negative_btn);
 | 
			
		||||
            negativeBtn.setText(label);
 | 
			
		||||
            negativeBtn.setOnClickListener(listener);
 | 
			
		||||
            negativeBtn.setVisibility(View.VISIBLE);
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Builds a dialog with the current configs.
 | 
			
		||||
         *
 | 
			
		||||
         * @return The dialog to be shown for audio sharing.
 | 
			
		||||
         */
 | 
			
		||||
        @NonNull
 | 
			
		||||
        @CheckReturnValue
 | 
			
		||||
        public AlertDialog build() {
 | 
			
		||||
            if (mIsCustomBodyEnabled) {
 | 
			
		||||
                mBuilder.setView(mCustomBody);
 | 
			
		||||
            }
 | 
			
		||||
            final AlertDialog dialog =
 | 
			
		||||
                    mBuilder.setCustomTitle(mCustomTitle).setCancelable(false).create();
 | 
			
		||||
            dialog.setCanceledOnTouchOutside(false);
 | 
			
		||||
            return dialog;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,137 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog;
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
import androidx.fragment.app.FragmentManager;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.Iterables;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDialogFragment extends InstrumentedDialogFragment {
 | 
			
		||||
    private static final String TAG = "AudioSharingDialog";
 | 
			
		||||
 | 
			
		||||
    private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
 | 
			
		||||
 | 
			
		||||
    // The host creates an instance of this dialog fragment must implement this interface to receive
 | 
			
		||||
    // event callbacks.
 | 
			
		||||
    public interface DialogEventListener {
 | 
			
		||||
        /**
 | 
			
		||||
         * Called when users click the device item for sharing in the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param item The device item clicked.
 | 
			
		||||
         */
 | 
			
		||||
        void onItemClick(AudioSharingDeviceItem item);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable private static DialogEventListener sListener;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        return SettingsEnums.DIALOG_START_AUDIO_SHARING;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the {@link AudioSharingDialogFragment} dialog.
 | 
			
		||||
     *
 | 
			
		||||
     * @param host The Fragment this dialog will be hosted.
 | 
			
		||||
     * @param deviceItems The connected device items eligible for audio sharing.
 | 
			
		||||
     * @param listener The callback to handle the user action on this dialog.
 | 
			
		||||
     */
 | 
			
		||||
    public static void show(
 | 
			
		||||
            @NonNull Fragment host,
 | 
			
		||||
            @NonNull List<AudioSharingDeviceItem> deviceItems,
 | 
			
		||||
            @NonNull DialogEventListener listener) {
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) return;
 | 
			
		||||
        final FragmentManager manager = host.getChildFragmentManager();
 | 
			
		||||
        sListener = listener;
 | 
			
		||||
        AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
 | 
			
		||||
        if (dialog != null) {
 | 
			
		||||
            Log.d(TAG, "Dialog is showing, return.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(TAG, "Show up the dialog.");
 | 
			
		||||
        final Bundle bundle = new Bundle();
 | 
			
		||||
        bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
 | 
			
		||||
        AudioSharingDialogFragment dialogFrag = new AudioSharingDialogFragment();
 | 
			
		||||
        dialogFrag.setArguments(bundle);
 | 
			
		||||
        dialogFrag.show(manager, TAG);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Dialog onCreateDialog(Bundle savedInstanceState) {
 | 
			
		||||
        Bundle arguments = requireArguments();
 | 
			
		||||
        List<AudioSharingDeviceItem> deviceItems =
 | 
			
		||||
                arguments.getParcelable(BUNDLE_KEY_DEVICE_ITEMS, List.class);
 | 
			
		||||
        AudioSharingDialogFactory.DialogBuilder builder =
 | 
			
		||||
                AudioSharingDialogFactory.newBuilder(getActivity())
 | 
			
		||||
                        .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
 | 
			
		||||
                        .setIsCustomBodyEnabled(true);
 | 
			
		||||
        if (deviceItems.isEmpty()) {
 | 
			
		||||
            builder.setTitle(R.string.audio_sharing_share_dialog_title)
 | 
			
		||||
                    .setCustomImage(R.drawable.audio_sharing_guidance)
 | 
			
		||||
                    .setCustomMessage(R.string.audio_sharing_dialog_connect_device_content)
 | 
			
		||||
                    .setNegativeButton(
 | 
			
		||||
                            R.string.audio_sharing_close_button_label, (dig, which) -> dismiss());
 | 
			
		||||
        } else if (deviceItems.size() == 1) {
 | 
			
		||||
            AudioSharingDeviceItem deviceItem = Iterables.getOnlyElement(deviceItems);
 | 
			
		||||
            builder.setTitle(
 | 
			
		||||
                            getString(
 | 
			
		||||
                                    R.string.audio_sharing_share_with_dialog_title,
 | 
			
		||||
                                    deviceItem.getName()))
 | 
			
		||||
                    .setCustomMessage(R.string.audio_sharing_dialog_share_content)
 | 
			
		||||
                    .setCustomPositiveButton(
 | 
			
		||||
                            R.string.audio_sharing_share_button_label,
 | 
			
		||||
                            v -> {
 | 
			
		||||
                                if (sListener != null) {
 | 
			
		||||
                                    sListener.onItemClick(deviceItem);
 | 
			
		||||
                                }
 | 
			
		||||
                                dismiss();
 | 
			
		||||
                            })
 | 
			
		||||
                    .setCustomNegativeButton(
 | 
			
		||||
                            R.string.audio_sharing_no_thanks_button_label, v -> dismiss());
 | 
			
		||||
        } else {
 | 
			
		||||
            builder.setTitle(R.string.audio_sharing_share_with_more_dialog_title)
 | 
			
		||||
                    .setCustomMessage(R.string.audio_sharing_dialog_share_more_content)
 | 
			
		||||
                    .setCustomDeviceActions(
 | 
			
		||||
                            new AudioSharingDeviceAdapter(
 | 
			
		||||
                                    getContext(),
 | 
			
		||||
                                    deviceItems,
 | 
			
		||||
                                    (AudioSharingDeviceItem item) -> {
 | 
			
		||||
                                        if (sListener != null) {
 | 
			
		||||
                                            sListener.onItemClick(item);
 | 
			
		||||
                                        }
 | 
			
		||||
                                        dismiss();
 | 
			
		||||
                                    },
 | 
			
		||||
                                    AudioSharingDeviceAdapter.ActionType.SHARE))
 | 
			
		||||
                    .setCustomNegativeButton(com.android.settings.R.string.cancel, v -> dismiss());
 | 
			
		||||
        }
 | 
			
		||||
        return builder.build();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,452 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.bluetooth.BluetoothCsipSetCoordinator;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcast;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.fragment.app.DialogFragment;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.SubSettingLauncher;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDialogHandler {
 | 
			
		||||
    private static final String TAG = "AudioSharingDialogHandler";
 | 
			
		||||
    private final Context mContext;
 | 
			
		||||
    private final Fragment mHostFragment;
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mLocalBtManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
 | 
			
		||||
    private List<BluetoothDevice> mTargetSinks = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
 | 
			
		||||
            new BluetoothLeBroadcast.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStarted(int reason, int broadcastId) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onBroadcastStarted(), reason = "
 | 
			
		||||
                                    + reason
 | 
			
		||||
                                    + ", broadcastId = "
 | 
			
		||||
                                    + broadcastId);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStartFailed(int reason) {
 | 
			
		||||
                    Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
 | 
			
		||||
                    AudioSharingUtils.toastMessage(
 | 
			
		||||
                            mContext, "Fail to start broadcast, reason " + reason);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastMetadataChanged(
 | 
			
		||||
                        int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onBroadcastMetadataChanged(), broadcastId = "
 | 
			
		||||
                                    + broadcastId
 | 
			
		||||
                                    + ", metadata = "
 | 
			
		||||
                                    + metadata);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStopped(int reason, int broadcastId) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onBroadcastStopped(), reason = "
 | 
			
		||||
                                    + reason
 | 
			
		||||
                                    + ", broadcastId = "
 | 
			
		||||
                                    + broadcastId);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStopFailed(int reason) {
 | 
			
		||||
                    Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
 | 
			
		||||
                    AudioSharingUtils.toastMessage(
 | 
			
		||||
                            mContext, "Fail to stop broadcast, reason " + reason);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastUpdated(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onPlaybackStarted(int reason, int broadcastId) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onPlaybackStarted(), reason = "
 | 
			
		||||
                                    + reason
 | 
			
		||||
                                    + ", broadcastId = "
 | 
			
		||||
                                    + broadcastId);
 | 
			
		||||
                    if (!mTargetSinks.isEmpty()) {
 | 
			
		||||
                        AudioSharingUtils.addSourceToTargetSinks(mTargetSinks, mLocalBtManager);
 | 
			
		||||
                        new SubSettingLauncher(mContext)
 | 
			
		||||
                                .setDestination(AudioSharingDashboardFragment.class.getName())
 | 
			
		||||
                                .setSourceMetricsCategory(
 | 
			
		||||
                                        (mHostFragment != null
 | 
			
		||||
                                                        && mHostFragment
 | 
			
		||||
                                                                instanceof DashboardFragment)
 | 
			
		||||
                                                ? ((DashboardFragment) mHostFragment)
 | 
			
		||||
                                                        .getMetricsCategory()
 | 
			
		||||
                                                : SettingsEnums.PAGE_UNKNOWN)
 | 
			
		||||
                                .launch();
 | 
			
		||||
                        mTargetSinks = new ArrayList<>();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onPlaybackStopped(int reason, int broadcastId) {}
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    public AudioSharingDialogHandler(@NonNull Context context, @NonNull Fragment fragment) {
 | 
			
		||||
        mContext = context;
 | 
			
		||||
        mHostFragment = fragment;
 | 
			
		||||
        mLocalBtManager = Utils.getLocalBluetoothManager(context);
 | 
			
		||||
        mBroadcast =
 | 
			
		||||
                mLocalBtManager != null
 | 
			
		||||
                        ? mLocalBtManager.getProfileManager().getLeAudioBroadcastProfile()
 | 
			
		||||
                        : null;
 | 
			
		||||
        mAssistant =
 | 
			
		||||
                mLocalBtManager != null
 | 
			
		||||
                        ? mLocalBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile()
 | 
			
		||||
                        : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Register callbacks for dialog handler */
 | 
			
		||||
    public void registerCallbacks(Executor executor) {
 | 
			
		||||
        if (mBroadcast != null) {
 | 
			
		||||
            mBroadcast.registerServiceCallBack(executor, mBroadcastCallback);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Unregister callbacks for dialog handler */
 | 
			
		||||
    public void unregisterCallbacks() {
 | 
			
		||||
        if (mBroadcast != null) {
 | 
			
		||||
            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Handle dialog pop-up logic when device is connected. */
 | 
			
		||||
    public void handleDeviceConnected(
 | 
			
		||||
            @NonNull CachedBluetoothDevice cachedDevice, boolean userTriggered) {
 | 
			
		||||
        String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress();
 | 
			
		||||
        boolean isBroadcasting = isBroadcasting();
 | 
			
		||||
        boolean isLeAudioSupported = AudioSharingUtils.isLeAudioSupported(cachedDevice);
 | 
			
		||||
        if (!isLeAudioSupported) {
 | 
			
		||||
            Log.d(TAG, "Handle non LE audio device connected, device = " + anonymizedAddress);
 | 
			
		||||
            // Handle connected ineligible (non LE audio) remote device
 | 
			
		||||
            handleNonLeAudioDeviceConnected(cachedDevice, isBroadcasting, userTriggered);
 | 
			
		||||
        } else {
 | 
			
		||||
            Log.d(TAG, "Handle LE audio device connected, device = " + anonymizedAddress);
 | 
			
		||||
            // Handle connected eligible (LE audio) remote device
 | 
			
		||||
            handleLeAudioDeviceConnected(cachedDevice, isBroadcasting, userTriggered);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handleNonLeAudioDeviceConnected(
 | 
			
		||||
            @NonNull CachedBluetoothDevice cachedDevice,
 | 
			
		||||
            boolean isBroadcasting,
 | 
			
		||||
            boolean userTriggered) {
 | 
			
		||||
        if (isBroadcasting) {
 | 
			
		||||
            // Show stop audio sharing dialog when an ineligible (non LE audio) remote device
 | 
			
		||||
            // connected during a sharing session.
 | 
			
		||||
            Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
 | 
			
		||||
                    AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
 | 
			
		||||
            List<AudioSharingDeviceItem> deviceItemsInSharingSession =
 | 
			
		||||
                    AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
 | 
			
		||||
                            mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
 | 
			
		||||
            postOnMainThread(
 | 
			
		||||
                    () -> {
 | 
			
		||||
                        closeOpeningDialogsOtherThan(AudioSharingStopDialogFragment.tag());
 | 
			
		||||
                        AudioSharingStopDialogFragment.show(
 | 
			
		||||
                                mHostFragment,
 | 
			
		||||
                                deviceItemsInSharingSession,
 | 
			
		||||
                                cachedDevice,
 | 
			
		||||
                                () -> {
 | 
			
		||||
                                    cachedDevice.setActive();
 | 
			
		||||
                                    AudioSharingUtils.stopBroadcasting(mLocalBtManager);
 | 
			
		||||
                                });
 | 
			
		||||
                    });
 | 
			
		||||
        } else {
 | 
			
		||||
            if (userTriggered) {
 | 
			
		||||
                cachedDevice.setActive();
 | 
			
		||||
            }
 | 
			
		||||
            // Do nothing for ineligible (non LE audio) remote device when no sharing session.
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "Ignore onProfileConnectionStateChanged for non LE audio without"
 | 
			
		||||
                            + " sharing session");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handleLeAudioDeviceConnected(
 | 
			
		||||
            @NonNull CachedBluetoothDevice cachedDevice,
 | 
			
		||||
            boolean isBroadcasting,
 | 
			
		||||
            boolean userTriggered) {
 | 
			
		||||
        Map<Integer, List<CachedBluetoothDevice>> groupedDevices =
 | 
			
		||||
                AudioSharingUtils.fetchConnectedDevicesByGroupId(mLocalBtManager);
 | 
			
		||||
        if (isBroadcasting) {
 | 
			
		||||
            // If another device within the same is already in the sharing session, add source to
 | 
			
		||||
            // the device automatically.
 | 
			
		||||
            int groupId = AudioSharingUtils.getGroupId(cachedDevice);
 | 
			
		||||
            if (groupedDevices.containsKey(groupId)
 | 
			
		||||
                    && groupedDevices.get(groupId).stream()
 | 
			
		||||
                            .anyMatch(
 | 
			
		||||
                                    device ->
 | 
			
		||||
                                            BluetoothUtils.hasConnectedBroadcastSource(
 | 
			
		||||
                                                    device, mLocalBtManager))) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "Automatically add another device within the same group to the sharing: "
 | 
			
		||||
                                + cachedDevice.getDevice().getAnonymizedAddress());
 | 
			
		||||
                if (mAssistant != null && mBroadcast != null) {
 | 
			
		||||
                    mAssistant.addSource(
 | 
			
		||||
                            cachedDevice.getDevice(),
 | 
			
		||||
                            mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
 | 
			
		||||
                            /* isGroupOp= */ false);
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Show audio sharing switch or join dialog according to device count in the sharing
 | 
			
		||||
            // session.
 | 
			
		||||
            List<AudioSharingDeviceItem> deviceItemsInSharingSession =
 | 
			
		||||
                    AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
 | 
			
		||||
                            mLocalBtManager, groupedDevices, /* filterByInSharing= */ true);
 | 
			
		||||
            // Show audio sharing switch dialog when the third eligible (LE audio) remote device
 | 
			
		||||
            // connected during a sharing session.
 | 
			
		||||
            if (deviceItemsInSharingSession.size() >= 2) {
 | 
			
		||||
                postOnMainThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            closeOpeningDialogsOtherThan(
 | 
			
		||||
                                    AudioSharingDisconnectDialogFragment.tag());
 | 
			
		||||
                            AudioSharingDisconnectDialogFragment.show(
 | 
			
		||||
                                    mHostFragment,
 | 
			
		||||
                                    deviceItemsInSharingSession,
 | 
			
		||||
                                    cachedDevice,
 | 
			
		||||
                                    (AudioSharingDeviceItem item) -> {
 | 
			
		||||
                                        // Remove all sources from the device user clicked
 | 
			
		||||
                                        removeSourceForGroup(item.getGroupId(), groupedDevices);
 | 
			
		||||
                                        // Add current broadcast to the latest connected device
 | 
			
		||||
                                        addSourceForGroup(groupId, groupedDevices);
 | 
			
		||||
                                    });
 | 
			
		||||
                        });
 | 
			
		||||
            } else {
 | 
			
		||||
                // Show audio sharing join dialog when the first or second eligible (LE audio)
 | 
			
		||||
                // remote device connected during a sharing session.
 | 
			
		||||
                postOnMainThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            closeOpeningDialogsOtherThan(AudioSharingJoinDialogFragment.tag());
 | 
			
		||||
                            AudioSharingJoinDialogFragment.show(
 | 
			
		||||
                                    mHostFragment,
 | 
			
		||||
                                    deviceItemsInSharingSession,
 | 
			
		||||
                                    cachedDevice,
 | 
			
		||||
                                    new AudioSharingJoinDialogFragment.DialogEventListener() {
 | 
			
		||||
                                        @Override
 | 
			
		||||
                                        public void onShareClick() {
 | 
			
		||||
                                            addSourceForGroup(groupId, groupedDevices);
 | 
			
		||||
                                        }
 | 
			
		||||
 | 
			
		||||
                                        @Override
 | 
			
		||||
                                        public void onCancelClick() {}
 | 
			
		||||
                                    });
 | 
			
		||||
                        });
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            List<AudioSharingDeviceItem> deviceItems = new ArrayList<>();
 | 
			
		||||
            for (List<CachedBluetoothDevice> devices : groupedDevices.values()) {
 | 
			
		||||
                // Use random device in the group within the sharing session to represent the group.
 | 
			
		||||
                CachedBluetoothDevice device = devices.get(0);
 | 
			
		||||
                if (AudioSharingUtils.getGroupId(device)
 | 
			
		||||
                        == AudioSharingUtils.getGroupId(cachedDevice)) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
                deviceItems.add(AudioSharingUtils.buildAudioSharingDeviceItem(device));
 | 
			
		||||
            }
 | 
			
		||||
            // Show audio sharing join dialog when the second eligible (LE audio) remote
 | 
			
		||||
            // device connect and no sharing session.
 | 
			
		||||
            if (deviceItems.size() == 1) {
 | 
			
		||||
                postOnMainThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            closeOpeningDialogsOtherThan(AudioSharingJoinDialogFragment.tag());
 | 
			
		||||
                            AudioSharingJoinDialogFragment.show(
 | 
			
		||||
                                    mHostFragment,
 | 
			
		||||
                                    deviceItems,
 | 
			
		||||
                                    cachedDevice,
 | 
			
		||||
                                    new AudioSharingJoinDialogFragment.DialogEventListener() {
 | 
			
		||||
                                        @Override
 | 
			
		||||
                                        public void onShareClick() {
 | 
			
		||||
                                            mTargetSinks = new ArrayList<>();
 | 
			
		||||
                                            for (List<CachedBluetoothDevice> devices :
 | 
			
		||||
                                                    groupedDevices.values()) {
 | 
			
		||||
                                                for (CachedBluetoothDevice device : devices) {
 | 
			
		||||
                                                    mTargetSinks.add(device.getDevice());
 | 
			
		||||
                                                }
 | 
			
		||||
                                            }
 | 
			
		||||
                                            Log.d(
 | 
			
		||||
                                                    TAG,
 | 
			
		||||
                                                    "Start broadcast with sinks: "
 | 
			
		||||
                                                            + mTargetSinks.size());
 | 
			
		||||
                                            if (mBroadcast != null) {
 | 
			
		||||
                                                mBroadcast.startPrivateBroadcast();
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
 | 
			
		||||
                                        @Override
 | 
			
		||||
                                        public void onCancelClick() {
 | 
			
		||||
                                            if (userTriggered) {
 | 
			
		||||
                                                cachedDevice.setActive();
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                        });
 | 
			
		||||
            } else if (userTriggered) {
 | 
			
		||||
                cachedDevice.setActive();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void closeOpeningDialogsOtherThan(String tag) {
 | 
			
		||||
        if (mHostFragment == null) return;
 | 
			
		||||
        List<Fragment> fragments = mHostFragment.getChildFragmentManager().getFragments();
 | 
			
		||||
        for (Fragment fragment : fragments) {
 | 
			
		||||
            if (fragment instanceof DialogFragment && !fragment.getTag().equals(tag)) {
 | 
			
		||||
                Log.d(TAG, "Remove staled opening dialog " + fragment.getTag());
 | 
			
		||||
                ((DialogFragment) fragment).dismiss();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Close opening dialogs for le audio device */
 | 
			
		||||
    public void closeOpeningDialogsForLeaDevice(@NonNull CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        if (mHostFragment == null) return;
 | 
			
		||||
        int groupId = AudioSharingUtils.getGroupId(cachedDevice);
 | 
			
		||||
        List<Fragment> fragments = mHostFragment.getChildFragmentManager().getFragments();
 | 
			
		||||
        for (Fragment fragment : fragments) {
 | 
			
		||||
            CachedBluetoothDevice device = getCachedBluetoothDeviceFromDialog(fragment);
 | 
			
		||||
            if (device != null
 | 
			
		||||
                    && groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID
 | 
			
		||||
                    && AudioSharingUtils.getGroupId(device) == groupId) {
 | 
			
		||||
                Log.d(TAG, "Remove staled opening dialog for group " + groupId);
 | 
			
		||||
                ((DialogFragment) fragment).dismiss();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Close opening dialogs for non le audio device */
 | 
			
		||||
    public void closeOpeningDialogsForNonLeaDevice(@NonNull CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        if (mHostFragment == null) return;
 | 
			
		||||
        String address = cachedDevice.getAddress();
 | 
			
		||||
        List<Fragment> fragments = mHostFragment.getChildFragmentManager().getFragments();
 | 
			
		||||
        for (Fragment fragment : fragments) {
 | 
			
		||||
            CachedBluetoothDevice device = getCachedBluetoothDeviceFromDialog(fragment);
 | 
			
		||||
            if (device != null && address != null && address.equals(device.getAddress())) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "Remove staled opening dialog for device "
 | 
			
		||||
                                + cachedDevice.getDevice().getAnonymizedAddress());
 | 
			
		||||
                ((DialogFragment) fragment).dismiss();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private CachedBluetoothDevice getCachedBluetoothDeviceFromDialog(Fragment fragment) {
 | 
			
		||||
        CachedBluetoothDevice device = null;
 | 
			
		||||
        if (fragment instanceof AudioSharingJoinDialogFragment) {
 | 
			
		||||
            device = ((AudioSharingJoinDialogFragment) fragment).getDevice();
 | 
			
		||||
        } else if (fragment instanceof AudioSharingStopDialogFragment) {
 | 
			
		||||
            device = ((AudioSharingStopDialogFragment) fragment).getDevice();
 | 
			
		||||
        } else if (fragment instanceof AudioSharingDisconnectDialogFragment) {
 | 
			
		||||
            device = ((AudioSharingDisconnectDialogFragment) fragment).getDevice();
 | 
			
		||||
        }
 | 
			
		||||
        return device;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void removeSourceForGroup(
 | 
			
		||||
            int groupId, Map<Integer, List<CachedBluetoothDevice>> groupedDevices) {
 | 
			
		||||
        if (mAssistant == null) {
 | 
			
		||||
            Log.d(TAG, "Fail to add source due to null profiles, group = " + groupId);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!groupedDevices.containsKey(groupId)) {
 | 
			
		||||
            Log.d(TAG, "Fail to remove source for group " + groupId);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        groupedDevices.get(groupId).stream()
 | 
			
		||||
                .map(CachedBluetoothDevice::getDevice)
 | 
			
		||||
                .filter(device -> device != null)
 | 
			
		||||
                .forEach(
 | 
			
		||||
                        device -> {
 | 
			
		||||
                            for (BluetoothLeBroadcastReceiveState source :
 | 
			
		||||
                                    mAssistant.getAllSources(device)) {
 | 
			
		||||
                                mAssistant.removeSource(device, source.getSourceId());
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void addSourceForGroup(
 | 
			
		||||
            int groupId, Map<Integer, List<CachedBluetoothDevice>> groupedDevices) {
 | 
			
		||||
        if (mBroadcast == null || mAssistant == null) {
 | 
			
		||||
            Log.d(TAG, "Fail to add source due to null profiles, group = " + groupId);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!groupedDevices.containsKey(groupId)) {
 | 
			
		||||
            Log.d(TAG, "Fail to add source due to invalid group id, group = " + groupId);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        groupedDevices.get(groupId).stream()
 | 
			
		||||
                .map(CachedBluetoothDevice::getDevice)
 | 
			
		||||
                .filter(device -> device != null)
 | 
			
		||||
                .forEach(
 | 
			
		||||
                        device ->
 | 
			
		||||
                                mAssistant.addSource(
 | 
			
		||||
                                        device,
 | 
			
		||||
                                        mBroadcast.getLatestBluetoothLeBroadcastMetadata(),
 | 
			
		||||
                                        /* isGroupOp= */ false));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void postOnMainThread(@NonNull Runnable runnable) {
 | 
			
		||||
        mContext.getMainExecutor().execute(runnable);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isBroadcasting() {
 | 
			
		||||
        return mBroadcast != null && mBroadcast.isEnabled(null);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,62 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.graphics.Typeface;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.util.TypedValue;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.fragment.app.DialogFragment;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
import androidx.fragment.app.FragmentManager;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDialogHelper {
 | 
			
		||||
    private static final String TAG = "AudioSharingDialogHelper";
 | 
			
		||||
 | 
			
		||||
    /** Updates the alert dialog message style. */
 | 
			
		||||
    public static void updateMessageStyle(@NonNull AlertDialog dialog) {
 | 
			
		||||
        TextView messageView = dialog.findViewById(android.R.id.message);
 | 
			
		||||
        if (messageView != null) {
 | 
			
		||||
            Typeface typeface = Typeface.create(Typeface.DEFAULT_FAMILY, Typeface.NORMAL);
 | 
			
		||||
            messageView.setTypeface(typeface);
 | 
			
		||||
            messageView.setTextDirection(View.TEXT_DIRECTION_LOCALE);
 | 
			
		||||
            messageView.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
 | 
			
		||||
            messageView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);
 | 
			
		||||
        } else {
 | 
			
		||||
            Log.w(TAG, "Fail to update dialog: message view is null");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Returns the alert dialog by tag if it is showing. */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static AlertDialog getDialogIfShowing(
 | 
			
		||||
            @NonNull FragmentManager manager, @NonNull String tag) {
 | 
			
		||||
        Fragment dialog = manager.findFragmentByTag(tag);
 | 
			
		||||
        return dialog != null
 | 
			
		||||
                        && dialog instanceof DialogFragment
 | 
			
		||||
                        && ((DialogFragment) dialog).getDialog() != null
 | 
			
		||||
                        && ((DialogFragment) dialog).getDialog().isShowing()
 | 
			
		||||
                        && ((DialogFragment) dialog).getDialog() instanceof AlertDialog
 | 
			
		||||
                ? (AlertDialog) ((DialogFragment) dialog).getDialog()
 | 
			
		||||
                : null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,152 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog;
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
import androidx.fragment.app.FragmentManager;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingDisconnectDialogFragment extends InstrumentedDialogFragment {
 | 
			
		||||
    private static final String TAG = "AudioSharingDisconnectDialog";
 | 
			
		||||
 | 
			
		||||
    private static final String BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS =
 | 
			
		||||
            "bundle_key_device_to_disconnect_items";
 | 
			
		||||
    private static final String BUNDLE_KEY_NEW_DEVICE_NAME = "bundle_key_new_device_name";
 | 
			
		||||
 | 
			
		||||
    // The host creates an instance of this dialog fragment must implement this interface to receive
 | 
			
		||||
    // event callbacks.
 | 
			
		||||
    public interface DialogEventListener {
 | 
			
		||||
        /**
 | 
			
		||||
         * Called when users click the device item to disconnect from the audio sharing in the
 | 
			
		||||
         * dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param item The device item clicked.
 | 
			
		||||
         */
 | 
			
		||||
        void onItemClick(AudioSharingDeviceItem item);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable private static DialogEventListener sListener;
 | 
			
		||||
    @Nullable private static CachedBluetoothDevice sNewDevice;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        return SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the {@link AudioSharingDisconnectDialogFragment} dialog.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>If the dialog is showing for the same group, update the dialog event listener.
 | 
			
		||||
     *
 | 
			
		||||
     * @param host The Fragment this dialog will be hosted.
 | 
			
		||||
     * @param deviceItems The existing connected device items in audio sharing session.
 | 
			
		||||
     * @param newDevice The latest connected device triggered this dialog.
 | 
			
		||||
     * @param listener The callback to handle the user action on this dialog.
 | 
			
		||||
     */
 | 
			
		||||
    public static void show(
 | 
			
		||||
            @NonNull Fragment host,
 | 
			
		||||
            @NonNull List<AudioSharingDeviceItem> deviceItems,
 | 
			
		||||
            @NonNull CachedBluetoothDevice newDevice,
 | 
			
		||||
            @NonNull DialogEventListener listener) {
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) return;
 | 
			
		||||
        FragmentManager manager = host.getChildFragmentManager();
 | 
			
		||||
        AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
 | 
			
		||||
        if (dialog != null) {
 | 
			
		||||
            int newGroupId = AudioSharingUtils.getGroupId(newDevice);
 | 
			
		||||
            if (sNewDevice != null && newGroupId == AudioSharingUtils.getGroupId(sNewDevice)) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        String.format(
 | 
			
		||||
                                Locale.US,
 | 
			
		||||
                                "Dialog is showing for the same device group %d, "
 | 
			
		||||
                                        + "update the content.",
 | 
			
		||||
                                newGroupId));
 | 
			
		||||
                sListener = listener;
 | 
			
		||||
                sNewDevice = newDevice;
 | 
			
		||||
                return;
 | 
			
		||||
            } else {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        String.format(
 | 
			
		||||
                                Locale.US,
 | 
			
		||||
                                "Dialog is showing for new device group %d, "
 | 
			
		||||
                                        + "dismiss current dialog.",
 | 
			
		||||
                                newGroupId));
 | 
			
		||||
                dialog.dismiss();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        sListener = listener;
 | 
			
		||||
        sNewDevice = newDevice;
 | 
			
		||||
        Log.d(TAG, "Show up the dialog.");
 | 
			
		||||
        final Bundle bundle = new Bundle();
 | 
			
		||||
        bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
 | 
			
		||||
        bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
 | 
			
		||||
        AudioSharingDisconnectDialogFragment dialogFrag =
 | 
			
		||||
                new AudioSharingDisconnectDialogFragment();
 | 
			
		||||
        dialogFrag.setArguments(bundle);
 | 
			
		||||
        dialogFrag.show(manager, TAG);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Return the tag of {@link AudioSharingDisconnectDialogFragment} dialog. */
 | 
			
		||||
    public static @NonNull String tag() {
 | 
			
		||||
        return TAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Get the latest connected device which triggers the dialog. */
 | 
			
		||||
    public @Nullable CachedBluetoothDevice getDevice() {
 | 
			
		||||
        return sNewDevice;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
 | 
			
		||||
        Bundle arguments = requireArguments();
 | 
			
		||||
        List<AudioSharingDeviceItem> deviceItems =
 | 
			
		||||
                arguments.getParcelable(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, List.class);
 | 
			
		||||
        return AudioSharingDialogFactory.newBuilder(getActivity())
 | 
			
		||||
                .setTitle(R.string.audio_sharing_disconnect_dialog_title)
 | 
			
		||||
                .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
 | 
			
		||||
                .setIsCustomBodyEnabled(true)
 | 
			
		||||
                .setCustomMessage(R.string.audio_sharing_dialog_disconnect_content)
 | 
			
		||||
                .setCustomDeviceActions(
 | 
			
		||||
                        new AudioSharingDeviceAdapter(
 | 
			
		||||
                                getContext(),
 | 
			
		||||
                                deviceItems,
 | 
			
		||||
                                (AudioSharingDeviceItem item) -> {
 | 
			
		||||
                                    if (sListener != null) {
 | 
			
		||||
                                        sListener.onItemClick(item);
 | 
			
		||||
                                    }
 | 
			
		||||
                                    dismiss();
 | 
			
		||||
                                },
 | 
			
		||||
                                AudioSharingDeviceAdapter.ActionType.REMOVE))
 | 
			
		||||
                .setCustomNegativeButton(com.android.settings.R.string.cancel, v -> dismiss())
 | 
			
		||||
                .build();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.core.AbstractPreferenceController;
 | 
			
		||||
import com.android.settingslib.core.lifecycle.Lifecycle;
 | 
			
		||||
 | 
			
		||||
/** Feature provider for the audio sharing related features, */
 | 
			
		||||
public interface AudioSharingFeatureProvider {
 | 
			
		||||
 | 
			
		||||
    /** Create audio sharing device preference controller. */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    AbstractPreferenceController createAudioSharingDevicePreferenceController(
 | 
			
		||||
            @NonNull Context context,
 | 
			
		||||
            @Nullable DashboardFragment fragment,
 | 
			
		||||
            @Nullable Lifecycle lifecycle);
 | 
			
		||||
 | 
			
		||||
    /** Create available media device preference controller. */
 | 
			
		||||
    AbstractPreferenceController createAvailableMediaDeviceGroupController(
 | 
			
		||||
            @NonNull Context context,
 | 
			
		||||
            @Nullable DashboardFragment fragment,
 | 
			
		||||
            @Nullable Lifecycle lifecycle);
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the device match the audio sharing filter.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>The filter is used to filter device in "Media devices" section.
 | 
			
		||||
     */
 | 
			
		||||
    boolean isAudioSharingFilterMatched(
 | 
			
		||||
            @NonNull CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,55 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.connecteddevice.AvailableMediaDeviceGroupController;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.core.AbstractPreferenceController;
 | 
			
		||||
import com.android.settingslib.core.lifecycle.Lifecycle;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingFeatureProviderImpl implements AudioSharingFeatureProvider {
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Override
 | 
			
		||||
    public AbstractPreferenceController createAudioSharingDevicePreferenceController(
 | 
			
		||||
            @NonNull Context context,
 | 
			
		||||
            @Nullable DashboardFragment fragment,
 | 
			
		||||
            @Nullable Lifecycle lifecycle) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public AbstractPreferenceController createAvailableMediaDeviceGroupController(
 | 
			
		||||
            @NonNull Context context,
 | 
			
		||||
            @Nullable DashboardFragment fragment,
 | 
			
		||||
            @Nullable Lifecycle lifecycle) {
 | 
			
		||||
        return new AvailableMediaDeviceGroupController(context, fragment, lifecycle);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isAudioSharingFilterMatched(
 | 
			
		||||
            @NonNull CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,159 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog;
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
import androidx.fragment.app.FragmentManager;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingJoinDialogFragment extends InstrumentedDialogFragment {
 | 
			
		||||
    private static final String TAG = "AudioSharingJoinDialog";
 | 
			
		||||
 | 
			
		||||
    private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
 | 
			
		||||
    private static final String BUNDLE_KEY_NEW_DEVICE_NAME = "bundle_key_new_device_name";
 | 
			
		||||
 | 
			
		||||
    // The host creates an instance of this dialog fragment must implement this interface to receive
 | 
			
		||||
    // event callbacks.
 | 
			
		||||
    public interface DialogEventListener {
 | 
			
		||||
        /** Called when users click the share audio button in the dialog. */
 | 
			
		||||
        void onShareClick();
 | 
			
		||||
 | 
			
		||||
        /** Called when users click the cancel button in the dialog. */
 | 
			
		||||
        void onCancelClick();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable private static DialogEventListener sListener;
 | 
			
		||||
    @Nullable private static CachedBluetoothDevice sNewDevice;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        return AudioSharingUtils.isBroadcasting(Utils.getLocalBtManager(getContext()))
 | 
			
		||||
                ? SettingsEnums.DIALOG_START_AUDIO_SHARING
 | 
			
		||||
                : SettingsEnums.DIALOG_START_AUDIO_SHARING;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the {@link AudioSharingJoinDialogFragment} dialog.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>If the dialog is showing, update the dialog message and event listener.
 | 
			
		||||
     *
 | 
			
		||||
     * @param host The Fragment this dialog will be hosted.
 | 
			
		||||
     * @param deviceItems The existing connected device items eligible for audio sharing.
 | 
			
		||||
     * @param newDevice The latest connected device triggered this dialog.
 | 
			
		||||
     * @param listener The callback to handle the user action on this dialog.
 | 
			
		||||
     */
 | 
			
		||||
    public static void show(
 | 
			
		||||
            @NonNull Fragment host,
 | 
			
		||||
            @NonNull List<AudioSharingDeviceItem> deviceItems,
 | 
			
		||||
            @NonNull CachedBluetoothDevice newDevice,
 | 
			
		||||
            @NonNull DialogEventListener listener) {
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) return;
 | 
			
		||||
        final FragmentManager manager = host.getChildFragmentManager();
 | 
			
		||||
        sListener = listener;
 | 
			
		||||
        sNewDevice = newDevice;
 | 
			
		||||
        AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
 | 
			
		||||
        if (dialog != null) {
 | 
			
		||||
            Log.d(TAG, "Dialog is showing, update the content.");
 | 
			
		||||
            updateDialog(deviceItems, newDevice.getName(), dialog);
 | 
			
		||||
        } else {
 | 
			
		||||
            Log.d(TAG, "Show up the dialog.");
 | 
			
		||||
            final Bundle bundle = new Bundle();
 | 
			
		||||
            bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
 | 
			
		||||
            bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
 | 
			
		||||
            final AudioSharingJoinDialogFragment dialogFrag = new AudioSharingJoinDialogFragment();
 | 
			
		||||
            dialogFrag.setArguments(bundle);
 | 
			
		||||
            dialogFrag.show(manager, TAG);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Return the tag of {@link AudioSharingJoinDialogFragment} dialog. */
 | 
			
		||||
    public static @NonNull String tag() {
 | 
			
		||||
        return TAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Get the latest connected device which triggers the dialog. */
 | 
			
		||||
    public @Nullable CachedBluetoothDevice getDevice() {
 | 
			
		||||
        return sNewDevice;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
 | 
			
		||||
        Bundle arguments = requireArguments();
 | 
			
		||||
        List<AudioSharingDeviceItem> deviceItems =
 | 
			
		||||
                arguments.getParcelable(BUNDLE_KEY_DEVICE_ITEMS, List.class);
 | 
			
		||||
        String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
 | 
			
		||||
        AlertDialog dialog =
 | 
			
		||||
                AudioSharingDialogFactory.newBuilder(getActivity())
 | 
			
		||||
                        .setTitle(R.string.audio_sharing_share_dialog_title)
 | 
			
		||||
                        .setTitleIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
 | 
			
		||||
                        .setIsCustomBodyEnabled(true)
 | 
			
		||||
                        .setCustomMessage(R.string.audio_sharing_dialog_share_content)
 | 
			
		||||
                        .setCustomPositiveButton(
 | 
			
		||||
                                R.string.audio_sharing_share_button_label,
 | 
			
		||||
                                v -> {
 | 
			
		||||
                                    if (sListener != null) {
 | 
			
		||||
                                        sListener.onShareClick();
 | 
			
		||||
                                    }
 | 
			
		||||
                                    dismiss();
 | 
			
		||||
                                })
 | 
			
		||||
                        .setCustomNegativeButton(
 | 
			
		||||
                                R.string.audio_sharing_no_thanks_button_label,
 | 
			
		||||
                                v -> {
 | 
			
		||||
                                    if (sListener != null) {
 | 
			
		||||
                                        sListener.onCancelClick();
 | 
			
		||||
                                    }
 | 
			
		||||
                                    dismiss();
 | 
			
		||||
                                })
 | 
			
		||||
                        .build();
 | 
			
		||||
        updateDialog(deviceItems, newDeviceName, dialog);
 | 
			
		||||
        dialog.show();
 | 
			
		||||
        AudioSharingDialogHelper.updateMessageStyle(dialog);
 | 
			
		||||
        return dialog;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void updateDialog(
 | 
			
		||||
            List<AudioSharingDeviceItem> deviceItems,
 | 
			
		||||
            String newDeviceName,
 | 
			
		||||
            @NonNull AlertDialog dialog) {
 | 
			
		||||
        // Only dialog message can be updated when the dialog is showing.
 | 
			
		||||
        // Thus we put the device name for sharing as the dialog message.
 | 
			
		||||
        if (deviceItems.isEmpty()) {
 | 
			
		||||
            dialog.setMessage(newDeviceName);
 | 
			
		||||
        } else {
 | 
			
		||||
            dialog.setMessage(
 | 
			
		||||
                    dialog.getContext()
 | 
			
		||||
                            .getString(
 | 
			
		||||
                                    R.string.audio_sharing_share_dialog_subtitle,
 | 
			
		||||
                                    deviceItems.get(0).getName(),
 | 
			
		||||
                                    newDeviceName));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,110 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.AttributeSet;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.widget.ImageButton;
 | 
			
		||||
 | 
			
		||||
import androidx.preference.PreferenceViewHolder;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsQrCodeFragment;
 | 
			
		||||
import com.android.settings.core.SubSettingLauncher;
 | 
			
		||||
import com.android.settings.widget.ValidatedEditTextPreference;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingNamePreference extends ValidatedEditTextPreference {
 | 
			
		||||
    private static final String TAG = "AudioSharingNamePreference";
 | 
			
		||||
    private boolean mShowQrCodeIcon = false;
 | 
			
		||||
 | 
			
		||||
    public AudioSharingNamePreference(
 | 
			
		||||
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
 | 
			
		||||
        super(context, attrs, defStyleAttr, defStyleRes);
 | 
			
		||||
        initialize();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioSharingNamePreference(Context context, AttributeSet attrs, int defStyleAttr) {
 | 
			
		||||
        super(context, attrs, defStyleAttr);
 | 
			
		||||
        initialize();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioSharingNamePreference(Context context, AttributeSet attrs) {
 | 
			
		||||
        super(context, attrs);
 | 
			
		||||
        initialize();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioSharingNamePreference(Context context) {
 | 
			
		||||
        super(context);
 | 
			
		||||
        initialize();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void initialize() {
 | 
			
		||||
        setLayoutResource(
 | 
			
		||||
                com.android.settingslib.widget.preference.twotarget.R.layout.preference_two_target);
 | 
			
		||||
        setWidgetLayoutResource(R.layout.preference_widget_qrcode);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setShowQrCodeIcon(boolean show) {
 | 
			
		||||
        mShowQrCodeIcon = show;
 | 
			
		||||
        notifyChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onBindViewHolder(PreferenceViewHolder holder) {
 | 
			
		||||
        super.onBindViewHolder(holder);
 | 
			
		||||
 | 
			
		||||
        ImageButton shareButton = (ImageButton) holder.findViewById(R.id.button_icon);
 | 
			
		||||
        View divider =
 | 
			
		||||
                holder.findViewById(
 | 
			
		||||
                        com.android.settingslib.widget.preference.twotarget.R.id
 | 
			
		||||
                                .two_target_divider);
 | 
			
		||||
 | 
			
		||||
        if (shareButton != null && divider != null) {
 | 
			
		||||
            if (mShowQrCodeIcon) {
 | 
			
		||||
                configureVisibleStateForQrCodeIcon(shareButton, divider);
 | 
			
		||||
            } else {
 | 
			
		||||
                configureInvisibleStateForQrCodeIcon(shareButton, divider);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Log.w(TAG, "onBindViewHolder() : shareButton or divider is null!");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void configureVisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
 | 
			
		||||
        divider.setVisibility(View.VISIBLE);
 | 
			
		||||
        shareButton.setVisibility(View.VISIBLE);
 | 
			
		||||
        shareButton.setImageDrawable(getContext().getDrawable(R.drawable.ic_qrcode_24dp));
 | 
			
		||||
        shareButton.setOnClickListener(unused -> launchAudioSharingQrCodeFragment());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void configureInvisibleStateForQrCodeIcon(ImageButton shareButton, View divider) {
 | 
			
		||||
        divider.setVisibility(View.INVISIBLE);
 | 
			
		||||
        shareButton.setVisibility(View.INVISIBLE);
 | 
			
		||||
        shareButton.setOnClickListener(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void launchAudioSharingQrCodeFragment() {
 | 
			
		||||
        new SubSettingLauncher(getContext())
 | 
			
		||||
                .setTitleText(getContext().getString(R.string.audio_streams_qr_code_page_title))
 | 
			
		||||
                .setDestination(AudioStreamsQrCodeFragment.class.getName())
 | 
			
		||||
                .setSourceMetricsCategory(SettingsEnums.AUDIO_SHARING_SETTINGS)
 | 
			
		||||
                .launch();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,275 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcast;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settings.widget.ValidatedEditTextPreference;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingNamePreferenceController extends BasePreferenceController
 | 
			
		||||
        implements ValidatedEditTextPreference.Validator,
 | 
			
		||||
                Preference.OnPreferenceChangeListener,
 | 
			
		||||
                DefaultLifecycleObserver,
 | 
			
		||||
                LocalBluetoothProfileManager.ServiceListener {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioSharingNamePreferenceController";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
    private static final String PREF_KEY = "audio_sharing_stream_name";
 | 
			
		||||
 | 
			
		||||
    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
 | 
			
		||||
            new BluetoothLeBroadcast.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastMetadataChanged(
 | 
			
		||||
                        int broadcastId, BluetoothLeBroadcastMetadata metadata) {
 | 
			
		||||
                    if (DEBUG) {
 | 
			
		||||
                        Log.d(
 | 
			
		||||
                                TAG,
 | 
			
		||||
                                "onBroadcastMetadataChanged() broadcastId : "
 | 
			
		||||
                                        + broadcastId
 | 
			
		||||
                                        + " metadata: "
 | 
			
		||||
                                        + metadata);
 | 
			
		||||
                    }
 | 
			
		||||
                    updateQrCodeIcon(true);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStartFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStarted(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStopFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStopped(int reason, int broadcastId) {
 | 
			
		||||
                    if (DEBUG) {
 | 
			
		||||
                        Log.d(
 | 
			
		||||
                                TAG,
 | 
			
		||||
                                "onBroadcastStopped() reason : "
 | 
			
		||||
                                        + reason
 | 
			
		||||
                                        + " broadcastId: "
 | 
			
		||||
                                        + broadcastId);
 | 
			
		||||
                    }
 | 
			
		||||
                    updateQrCodeIcon(false);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastUpdateFailed(int reason, int broadcastId) {
 | 
			
		||||
                    Log.w(TAG, "onBroadcastUpdateFailed() reason : " + reason);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastUpdated(int reason, int broadcastId) {
 | 
			
		||||
                    if (DEBUG) {
 | 
			
		||||
                        Log.d(TAG, "onBroadcastUpdated() reason : " + reason);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onPlaybackStarted(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onPlaybackStopped(int reason, int broadcastId) {}
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothProfileManager mProfileManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
 | 
			
		||||
    @Nullable private AudioSharingNamePreference mPreference;
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    private final AudioSharingNameTextValidator mAudioSharingNameTextValidator;
 | 
			
		||||
    private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
 | 
			
		||||
 | 
			
		||||
    public AudioSharingNamePreferenceController(Context context, String preferenceKey) {
 | 
			
		||||
        super(context, preferenceKey);
 | 
			
		||||
        mBtManager = Utils.getLocalBluetoothManager(context);
 | 
			
		||||
        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
 | 
			
		||||
        mBroadcast =
 | 
			
		||||
                (mProfileManager != null) ? mProfileManager.getLeAudioBroadcastProfile() : null;
 | 
			
		||||
        mAudioSharingNameTextValidator = new AudioSharingNameTextValidator();
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks, feature not support");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks, profile not ready");
 | 
			
		||||
            if (mProfileManager != null) {
 | 
			
		||||
                mProfileManager.addServiceListener(this);
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        registerCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip unregister callbacks, feature not support");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mProfileManager != null) {
 | 
			
		||||
            mProfileManager.removeServiceListener(this);
 | 
			
		||||
        }
 | 
			
		||||
        if (mBroadcast == null || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            Log.d(TAG, "Skip unregister callbacks, profile not ready");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mCallbacksRegistered.get()) {
 | 
			
		||||
            Log.d(TAG, "Unregister callbacks");
 | 
			
		||||
            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
 | 
			
		||||
            mCallbacksRegistered.set(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        mPreference = screen.findPreference(getPreferenceKey());
 | 
			
		||||
        if (mPreference != null) {
 | 
			
		||||
            mPreference.setValidator(this);
 | 
			
		||||
            updateBroadcastName();
 | 
			
		||||
            updateQrCodeIcon(isBroadcasting(mBtManager));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onServiceConnected() {
 | 
			
		||||
        if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            registerCallbacks();
 | 
			
		||||
            updateBroadcastName();
 | 
			
		||||
            updateQrCodeIcon(isBroadcasting(mBtManager));
 | 
			
		||||
            if (mProfileManager != null) {
 | 
			
		||||
                mProfileManager.removeServiceListener(this);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onServiceDisconnected() {
 | 
			
		||||
        // Do nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getPreferenceKey() {
 | 
			
		||||
        return PREF_KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean onPreferenceChange(Preference preference, Object newValue) {
 | 
			
		||||
        if (mPreference != null
 | 
			
		||||
                && mPreference.getSummary() != null
 | 
			
		||||
                && ((String) newValue).contentEquals(mPreference.getSummary())) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            if (mBroadcast != null) {
 | 
			
		||||
                                mBroadcast.setProgramInfo((String) newValue);
 | 
			
		||||
                                if (isBroadcasting(mBtManager)) {
 | 
			
		||||
                                    mBroadcast.updateBroadcast();
 | 
			
		||||
                                }
 | 
			
		||||
                                updateBroadcastName();
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void registerCallbacks() {
 | 
			
		||||
        if (mBroadcast == null) {
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks, profile not ready");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!mCallbacksRegistered.get()) {
 | 
			
		||||
            Log.d(TAG, "Register callbacks");
 | 
			
		||||
            mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
 | 
			
		||||
            mCallbacksRegistered.set(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateBroadcastName() {
 | 
			
		||||
        if (mPreference != null) {
 | 
			
		||||
            var unused =
 | 
			
		||||
                    ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                            () -> {
 | 
			
		||||
                                if (mBroadcast != null) {
 | 
			
		||||
                                    String name = mBroadcast.getProgramInfo();
 | 
			
		||||
                                    AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                                            mContext,
 | 
			
		||||
                                            () -> {
 | 
			
		||||
                                                if (mPreference != null) {
 | 
			
		||||
                                                    mPreference.setText(name);
 | 
			
		||||
                                                    mPreference.setSummary(name);
 | 
			
		||||
                                                }
 | 
			
		||||
                                            });
 | 
			
		||||
                                }
 | 
			
		||||
                            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateQrCodeIcon(boolean show) {
 | 
			
		||||
        if (mPreference != null) {
 | 
			
		||||
            AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                    mContext,
 | 
			
		||||
                    () -> {
 | 
			
		||||
                        if (mPreference != null) {
 | 
			
		||||
                            mPreference.setShowQrCodeIcon(show);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isTextValid(String value) {
 | 
			
		||||
        return mAudioSharingNameTextValidator.isTextValid(value);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.widget.ValidatedEditTextPreference;
 | 
			
		||||
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validator for Audio Sharing Name, which should be a UTF-8 encoded string containing a minimum of
 | 
			
		||||
 * 4 characters and a maximum of 32 human-readable characters.
 | 
			
		||||
 */
 | 
			
		||||
public class AudioSharingNameTextValidator implements ValidatedEditTextPreference.Validator {
 | 
			
		||||
    private static final int MIN_LENGTH = 4;
 | 
			
		||||
    private static final int MAX_LENGTH = 32;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isTextValid(String value) {
 | 
			
		||||
        if (value == null || value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return isValidUTF8(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean isValidUTF8(String value) {
 | 
			
		||||
        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
 | 
			
		||||
        String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
 | 
			
		||||
        return value.equals(reconstructedString);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,142 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import static android.view.View.GONE;
 | 
			
		||||
import static android.view.View.VISIBLE;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.DialogInterface;
 | 
			
		||||
import android.util.AttributeSet;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.widget.CheckBox;
 | 
			
		||||
import android.widget.EditText;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.widget.ValidatedEditTextPreference;
 | 
			
		||||
import com.android.settingslib.utils.ColorUtil;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingPasswordPreference extends ValidatedEditTextPreference {
 | 
			
		||||
    private static final String TAG = "AudioSharingPasswordPreference";
 | 
			
		||||
    @Nullable private OnDialogEventListener mOnDialogEventListener;
 | 
			
		||||
    @Nullable private EditText mEditText;
 | 
			
		||||
    @Nullable private CheckBox mCheckBox;
 | 
			
		||||
    @Nullable private View mDialogMessage;
 | 
			
		||||
    private boolean mEditable = true;
 | 
			
		||||
 | 
			
		||||
    interface OnDialogEventListener {
 | 
			
		||||
        void onBindDialogView();
 | 
			
		||||
 | 
			
		||||
        void onPreferenceDataChanged(@NonNull String editTextValue, boolean checkBoxValue);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setOnDialogEventListener(OnDialogEventListener listener) {
 | 
			
		||||
        mOnDialogEventListener = listener;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioSharingPasswordPreference(
 | 
			
		||||
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
 | 
			
		||||
        super(context, attrs, defStyleAttr, defStyleRes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioSharingPasswordPreference(Context context, AttributeSet attrs, int defStyleAttr) {
 | 
			
		||||
        super(context, attrs, defStyleAttr);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioSharingPasswordPreference(Context context, AttributeSet attrs) {
 | 
			
		||||
        super(context, attrs);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioSharingPasswordPreference(Context context) {
 | 
			
		||||
        super(context);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onBindDialogView(View view) {
 | 
			
		||||
        super.onBindDialogView(view);
 | 
			
		||||
 | 
			
		||||
        mEditText = view.findViewById(android.R.id.edit);
 | 
			
		||||
        mCheckBox = view.findViewById(R.id.audio_sharing_stream_password_checkbox);
 | 
			
		||||
        mDialogMessage = view.findViewById(android.R.id.message);
 | 
			
		||||
 | 
			
		||||
        if (mEditText == null || mCheckBox == null || mDialogMessage == null) {
 | 
			
		||||
            Log.w(TAG, "onBindDialogView() : Invalid layout");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mCheckBox.setOnCheckedChangeListener((unused, checked) -> setEditTextEnabled(!checked));
 | 
			
		||||
        if (mOnDialogEventListener != null) {
 | 
			
		||||
            mOnDialogEventListener.onBindDialogView();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onPrepareDialogBuilder(
 | 
			
		||||
            AlertDialog.Builder builder, DialogInterface.OnClickListener listener) {
 | 
			
		||||
        if (!mEditable) {
 | 
			
		||||
            builder.setPositiveButton(null, null);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onClick(DialogInterface dialog, int which) {
 | 
			
		||||
        if (mEditText == null || mCheckBox == null) {
 | 
			
		||||
            Log.w(TAG, "onClick() : Invalid layout");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (mOnDialogEventListener != null
 | 
			
		||||
                && which == DialogInterface.BUTTON_POSITIVE
 | 
			
		||||
                && mEditText.getText() != null) {
 | 
			
		||||
            mOnDialogEventListener.onPreferenceDataChanged(
 | 
			
		||||
                    mEditText.getText().toString(), mCheckBox.isChecked());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setEditable(boolean editable) {
 | 
			
		||||
        if (mEditText == null || mCheckBox == null || mDialogMessage == null) {
 | 
			
		||||
            Log.w(TAG, "setEditable() : Invalid layout");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mEditable = editable;
 | 
			
		||||
        setEditTextEnabled(editable);
 | 
			
		||||
        mCheckBox.setEnabled(editable);
 | 
			
		||||
        mDialogMessage.setVisibility(editable ? GONE : VISIBLE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setChecked(boolean checked) {
 | 
			
		||||
        if (mCheckBox == null) {
 | 
			
		||||
            Log.w(TAG, "setChecked() : Invalid layout");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mCheckBox.setChecked(checked);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void setEditTextEnabled(boolean enabled) {
 | 
			
		||||
        if (mEditText == null) {
 | 
			
		||||
            Log.w(TAG, "setEditTextEnabled() : Invalid layout");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mEditText.setEnabled(enabled);
 | 
			
		||||
        mEditText.setAlpha(enabled ? 1.0f : ColorUtil.getDisabledAlpha(getContext()));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,178 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.isBroadcasting;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.SharedPreferences;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settings.widget.ValidatedEditTextPreference;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingPasswordPreferenceController extends BasePreferenceController
 | 
			
		||||
        implements ValidatedEditTextPreference.Validator,
 | 
			
		||||
                AudioSharingPasswordPreference.OnDialogEventListener {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioSharingPasswordPreferenceController";
 | 
			
		||||
    private static final String PREF_KEY = "audio_sharing_stream_password";
 | 
			
		||||
    private static final String SHARED_PREF_NAME = "audio_sharing_settings";
 | 
			
		||||
    private static final String SHARED_PREF_KEY = "default_password";
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
 | 
			
		||||
    @Nullable private AudioSharingPasswordPreference mPreference;
 | 
			
		||||
    private final AudioSharingPasswordValidator mAudioSharingPasswordValidator;
 | 
			
		||||
 | 
			
		||||
    public AudioSharingPasswordPreferenceController(Context context, String preferenceKey) {
 | 
			
		||||
        super(context, preferenceKey);
 | 
			
		||||
        mBtManager = Utils.getLocalBluetoothManager(context);
 | 
			
		||||
        mBroadcast =
 | 
			
		||||
                mBtManager != null
 | 
			
		||||
                        ? mBtManager.getProfileManager().getLeAudioBroadcastProfile()
 | 
			
		||||
                        : null;
 | 
			
		||||
        mAudioSharingPasswordValidator = new AudioSharingPasswordValidator();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        mPreference = screen.findPreference(getPreferenceKey());
 | 
			
		||||
        if (mPreference != null) {
 | 
			
		||||
            mPreference.setValidator(this);
 | 
			
		||||
            mPreference.setIsPassword(true);
 | 
			
		||||
            mPreference.setDialogLayoutResource(R.layout.audio_sharing_password_dialog);
 | 
			
		||||
            mPreference.setOnDialogEventListener(this);
 | 
			
		||||
            updatePreference();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getPreferenceKey() {
 | 
			
		||||
        return PREF_KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isTextValid(String value) {
 | 
			
		||||
        return mAudioSharingPasswordValidator.isTextValid(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onBindDialogView() {
 | 
			
		||||
        if (mPreference == null || mBroadcast == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mPreference.setEditable(!isBroadcasting(mBtManager));
 | 
			
		||||
        var password = mBroadcast.getBroadcastCode();
 | 
			
		||||
        mPreference.setChecked(password == null || password.length == 0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onPreferenceDataChanged(@NonNull String password, boolean isPublicBroadcast) {
 | 
			
		||||
        if (mBroadcast == null || isBroadcasting(mBtManager)) {
 | 
			
		||||
            Log.w(TAG, "onPreferenceDataChanged() changing password when broadcasting or null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        persistDefaultPassword(mContext, password);
 | 
			
		||||
        mBroadcast.setBroadcastCode(isPublicBroadcast ? new byte[0] : password.getBytes());
 | 
			
		||||
        updatePreference();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updatePreference() {
 | 
			
		||||
        if (mBroadcast == null || mPreference == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            byte[] password = mBroadcast.getBroadcastCode();
 | 
			
		||||
                            boolean noPassword = (password == null || password.length == 0);
 | 
			
		||||
                            String passwordToDisplay =
 | 
			
		||||
                                    noPassword
 | 
			
		||||
                                            ? getDefaultPassword(mContext)
 | 
			
		||||
                                            : new String(password, StandardCharsets.UTF_8);
 | 
			
		||||
                            String passwordSummary =
 | 
			
		||||
                                    noPassword
 | 
			
		||||
                                            ? mContext.getString(
 | 
			
		||||
                                                    R.string.audio_streams_no_password_summary)
 | 
			
		||||
                                            : "********";
 | 
			
		||||
 | 
			
		||||
                            AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                                    mContext,
 | 
			
		||||
                                    () -> {
 | 
			
		||||
                                        // Check nullability to pass NullAway check
 | 
			
		||||
                                        if (mPreference != null) {
 | 
			
		||||
                                            mPreference.setText(passwordToDisplay);
 | 
			
		||||
                                            mPreference.setSummary(passwordSummary);
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void persistDefaultPassword(Context context, String defaultPassword) {
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            if (getDefaultPassword(context).equals(defaultPassword)) {
 | 
			
		||||
                                return;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            SharedPreferences sharedPref =
 | 
			
		||||
                                    context.getSharedPreferences(
 | 
			
		||||
                                            SHARED_PREF_NAME, Context.MODE_PRIVATE);
 | 
			
		||||
                            if (sharedPref == null) {
 | 
			
		||||
                                Log.w(TAG, "persistDefaultPassword(): sharedPref is empty!");
 | 
			
		||||
                                return;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            SharedPreferences.Editor editor = sharedPref.edit();
 | 
			
		||||
                            editor.putString(SHARED_PREF_KEY, defaultPassword);
 | 
			
		||||
                            editor.apply();
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static String getDefaultPassword(Context context) {
 | 
			
		||||
        SharedPreferences sharedPref =
 | 
			
		||||
                context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
 | 
			
		||||
        if (sharedPref == null) {
 | 
			
		||||
            Log.w(TAG, "getDefaultPassword(): sharedPref is empty!");
 | 
			
		||||
            return "";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        String value = sharedPref.getString(SHARED_PREF_KEY, "");
 | 
			
		||||
        if (value != null && value.isEmpty()) {
 | 
			
		||||
            Log.w(TAG, "getDefaultPassword(): default password is empty!");
 | 
			
		||||
        }
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.widget.ValidatedEditTextPreference;
 | 
			
		||||
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Validator for Audio Sharing Password, which should be a UTF-8 string that has at least 4 octets
 | 
			
		||||
 * and should not exceed 16 octets.
 | 
			
		||||
 */
 | 
			
		||||
public class AudioSharingPasswordValidator implements ValidatedEditTextPreference.Validator {
 | 
			
		||||
    private static final int MIN_OCTETS = 4;
 | 
			
		||||
    private static final int MAX_OCTETS = 16;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isTextValid(String value) {
 | 
			
		||||
        if (value == null
 | 
			
		||||
                || getOctetsCount(value) < MIN_OCTETS
 | 
			
		||||
                || getOctetsCount(value) > MAX_OCTETS) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return isValidUTF8(value);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static int getOctetsCount(String value) {
 | 
			
		||||
        return value.getBytes(StandardCharsets.UTF_8).length;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean isValidUTF8(String value) {
 | 
			
		||||
        byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
 | 
			
		||||
        String reconstructedString = new String(bytes, StandardCharsets.UTF_8);
 | 
			
		||||
        return value.equals(reconstructedString);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,112 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.content.ContentResolver;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.media.AudioAttributes;
 | 
			
		||||
import android.media.AudioManager;
 | 
			
		||||
import android.media.Ringtone;
 | 
			
		||||
import android.media.RingtoneManager;
 | 
			
		||||
import android.net.Uri;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingPlaySoundPreferenceController
 | 
			
		||||
        extends AudioSharingBasePreferenceController {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioSharingPlaySoundPreferenceController";
 | 
			
		||||
 | 
			
		||||
    private static final String PREF_KEY = "audio_sharing_play_sound";
 | 
			
		||||
 | 
			
		||||
    private Ringtone mRingtone;
 | 
			
		||||
 | 
			
		||||
    public AudioSharingPlaySoundPreferenceController(Context context) {
 | 
			
		||||
        super(context, PREF_KEY);
 | 
			
		||||
        mRingtone = RingtoneManager.getRingtone(context, getMediaVolumeUri());
 | 
			
		||||
        if (mRingtone != null) {
 | 
			
		||||
            mRingtone.setStreamType(AudioManager.STREAM_MUSIC);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return (mRingtone != null && AudioSharingUtils.isFeatureEnabled())
 | 
			
		||||
                ? AVAILABLE
 | 
			
		||||
                : UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        if (mPreference != null) {
 | 
			
		||||
            mPreference.setOnPreferenceClickListener(
 | 
			
		||||
                    (v) -> {
 | 
			
		||||
                        if (mRingtone == null) {
 | 
			
		||||
                            Log.d(TAG, "Skip onClick due to ringtone is null");
 | 
			
		||||
                            return true;
 | 
			
		||||
                        }
 | 
			
		||||
                        try {
 | 
			
		||||
                            mRingtone.setAudioAttributes(
 | 
			
		||||
                                    new AudioAttributes.Builder(mRingtone.getAudioAttributes())
 | 
			
		||||
                                            .setFlags(AudioAttributes.FLAG_BYPASS_MUTE)
 | 
			
		||||
                                            .addTag("VX_AOSP_SAMPLESOUND")
 | 
			
		||||
                                            .build());
 | 
			
		||||
                            if (!mRingtone.isPlaying()) {
 | 
			
		||||
                                mRingtone.play();
 | 
			
		||||
                            }
 | 
			
		||||
                        } catch (Throwable e) {
 | 
			
		||||
                            Log.w(TAG, "Fail to play sample, error = " + e);
 | 
			
		||||
                        }
 | 
			
		||||
                        return true;
 | 
			
		||||
                    });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        super.onStop(owner);
 | 
			
		||||
        if (mRingtone != null && mRingtone.isPlaying()) {
 | 
			
		||||
            mRingtone.stop();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getPreferenceKey() {
 | 
			
		||||
        return PREF_KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    protected void setRingtone(Ringtone ringtone) {
 | 
			
		||||
        mRingtone = ringtone;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Uri getMediaVolumeUri() {
 | 
			
		||||
        return Uri.parse(
 | 
			
		||||
                ContentResolver.SCHEME_ANDROID_RESOURCE
 | 
			
		||||
                        + "://"
 | 
			
		||||
                        + mContext.getPackageName()
 | 
			
		||||
                        + "/"
 | 
			
		||||
                        + R.raw.media_volume);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,168 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcast;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothEventManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingPreferenceController extends BasePreferenceController
 | 
			
		||||
        implements DefaultLifecycleObserver, BluetoothCallback {
 | 
			
		||||
    private static final String TAG = "AudioSharingPreferenceController";
 | 
			
		||||
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final BluetoothEventManager mEventManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
 | 
			
		||||
    @Nullable private Preference mPreference;
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
 | 
			
		||||
    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
 | 
			
		||||
            new BluetoothLeBroadcast.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStarted(int reason, int broadcastId) {
 | 
			
		||||
                    refreshSummary();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStartFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastMetadataChanged(
 | 
			
		||||
                        int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStopped(int reason, int broadcastId) {
 | 
			
		||||
                    refreshSummary();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStopFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastUpdated(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onPlaybackStarted(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onPlaybackStopped(int reason, int broadcastId) {}
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    public AudioSharingPreferenceController(Context context, String preferenceKey) {
 | 
			
		||||
        super(context, preferenceKey);
 | 
			
		||||
        mBtManager = Utils.getLocalBtManager(context);
 | 
			
		||||
        mEventManager = mBtManager == null ? null : mBtManager.getEventManager();
 | 
			
		||||
        mBroadcast =
 | 
			
		||||
                mBtManager == null
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : mBtManager.getProfileManager().getLeAudioBroadcastProfile();
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks, feature not support");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mEventManager == null || mBroadcast == null) {
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks, profile not ready");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mEventManager.registerCallback(this);
 | 
			
		||||
        mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip unregister callbacks, feature not support");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mEventManager == null || mBroadcast == null) {
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks, profile not ready");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mEventManager.unregisterCallback(this);
 | 
			
		||||
        mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(@NonNull PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        mPreference = screen.findPreference(getPreferenceKey());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public CharSequence getSummary() {
 | 
			
		||||
        return AudioSharingUtils.isBroadcasting(mBtManager)
 | 
			
		||||
                ? mContext.getString(R.string.audio_sharing_summary_on)
 | 
			
		||||
                : mContext.getString(R.string.audio_sharing_summary_off);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onBluetoothStateChanged(@AdapterState int bluetoothState) {
 | 
			
		||||
        refreshSummary();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void refreshSummary() {
 | 
			
		||||
        if (mPreference == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            final CharSequence summary = getSummary();
 | 
			
		||||
                            AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                                    mContext,
 | 
			
		||||
                                    () -> {
 | 
			
		||||
                                        // Check nullability to pass NullAway check
 | 
			
		||||
                                        if (mPreference != null) {
 | 
			
		||||
                                            mPreference.setSummary(summary);
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,149 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.app.Notification;
 | 
			
		||||
import android.app.NotificationChannel;
 | 
			
		||||
import android.app.NotificationManager;
 | 
			
		||||
import android.app.PendingIntent;
 | 
			
		||||
import android.content.BroadcastReceiver;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.core.app.NotificationCompat;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingReceiver extends BroadcastReceiver {
 | 
			
		||||
    private static final String TAG = "AudioSharingNotification";
 | 
			
		||||
    private static final String ACTION_LE_AUDIO_SHARING_SETTINGS =
 | 
			
		||||
            "com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS";
 | 
			
		||||
    private static final String ACTION_LE_AUDIO_SHARING_STOP =
 | 
			
		||||
            "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STOP";
 | 
			
		||||
    private static final String CHANNEL_ID = "bluetooth_notification_channel";
 | 
			
		||||
    private static final int NOTIFICATION_ID =
 | 
			
		||||
            com.android.settingslib.R.drawable.ic_bt_le_audio_sharing;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onReceive(Context context, Intent intent) {
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            Log.w(TAG, "Skip handling received intent, flag is off.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        String action = intent.getAction();
 | 
			
		||||
        if (action == null) {
 | 
			
		||||
            Log.w(TAG, "Received unexpected intent with null action.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        switch (action) {
 | 
			
		||||
            case LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_STATE_CHANGE:
 | 
			
		||||
                int state =
 | 
			
		||||
                        intent.getIntExtra(
 | 
			
		||||
                                LocalBluetoothLeBroadcast.EXTRA_LE_AUDIO_SHARING_STATE, -1);
 | 
			
		||||
                if (state == LocalBluetoothLeBroadcast.BROADCAST_STATE_ON) {
 | 
			
		||||
                    showSharingNotification(context);
 | 
			
		||||
                } else if (state == LocalBluetoothLeBroadcast.BROADCAST_STATE_OFF) {
 | 
			
		||||
                    cancelSharingNotification(context);
 | 
			
		||||
                } else {
 | 
			
		||||
                    Log.w(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "Skip handling ACTION_LE_AUDIO_SHARING_STATE_CHANGE, invalid extras.");
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            case ACTION_LE_AUDIO_SHARING_STOP:
 | 
			
		||||
                LocalBluetoothManager manager = Utils.getLocalBtManager(context);
 | 
			
		||||
                AudioSharingUtils.stopBroadcasting(manager);
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                Log.w(TAG, "Received unexpected intent " + intent.getAction());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void showSharingNotification(Context context) {
 | 
			
		||||
        NotificationManager nm = context.getSystemService(NotificationManager.class);
 | 
			
		||||
        if (nm.getNotificationChannel(CHANNEL_ID) == null) {
 | 
			
		||||
            Log.d(TAG, "Create bluetooth notification channel");
 | 
			
		||||
            NotificationChannel notificationChannel =
 | 
			
		||||
                    new NotificationChannel(
 | 
			
		||||
                            CHANNEL_ID,
 | 
			
		||||
                            context.getString(com.android.settings.R.string.bluetooth),
 | 
			
		||||
                            NotificationManager.IMPORTANCE_HIGH);
 | 
			
		||||
            nm.createNotificationChannel(notificationChannel);
 | 
			
		||||
        }
 | 
			
		||||
        Intent stopIntent =
 | 
			
		||||
                new Intent(ACTION_LE_AUDIO_SHARING_STOP).setPackage(context.getPackageName());
 | 
			
		||||
        PendingIntent stopPendingIntent =
 | 
			
		||||
                PendingIntent.getBroadcast(
 | 
			
		||||
                        context,
 | 
			
		||||
                        R.string.audio_sharing_stop_button_label,
 | 
			
		||||
                        stopIntent,
 | 
			
		||||
                        PendingIntent.FLAG_IMMUTABLE);
 | 
			
		||||
        Intent settingsIntent =
 | 
			
		||||
                new Intent(ACTION_LE_AUDIO_SHARING_SETTINGS).setPackage(context.getPackageName());
 | 
			
		||||
        PendingIntent settingsPendingIntent =
 | 
			
		||||
                PendingIntent.getActivity(
 | 
			
		||||
                        context,
 | 
			
		||||
                        R.string.audio_sharing_settings_button_label,
 | 
			
		||||
                        settingsIntent,
 | 
			
		||||
                        PendingIntent.FLAG_IMMUTABLE);
 | 
			
		||||
        NotificationCompat.Action stopAction =
 | 
			
		||||
                new NotificationCompat.Action.Builder(
 | 
			
		||||
                                0,
 | 
			
		||||
                                context.getString(R.string.audio_sharing_stop_button_label),
 | 
			
		||||
                                stopPendingIntent)
 | 
			
		||||
                        .build();
 | 
			
		||||
        NotificationCompat.Action settingsAction =
 | 
			
		||||
                new NotificationCompat.Action.Builder(
 | 
			
		||||
                                0,
 | 
			
		||||
                                context.getString(R.string.audio_sharing_settings_button_label),
 | 
			
		||||
                                settingsPendingIntent)
 | 
			
		||||
                        .build();
 | 
			
		||||
        final Bundle extras = new Bundle();
 | 
			
		||||
        extras.putString(
 | 
			
		||||
                Notification.EXTRA_SUBSTITUTE_APP_NAME,
 | 
			
		||||
                context.getString(R.string.audio_sharing_title));
 | 
			
		||||
        NotificationCompat.Builder builder =
 | 
			
		||||
                new NotificationCompat.Builder(context, CHANNEL_ID)
 | 
			
		||||
                        .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
 | 
			
		||||
                        .setLocalOnly(true)
 | 
			
		||||
                        .setContentTitle(
 | 
			
		||||
                                context.getString(R.string.audio_sharing_notification_title))
 | 
			
		||||
                        .setContentText(
 | 
			
		||||
                                context.getString(R.string.audio_sharing_notification_content))
 | 
			
		||||
                        .setOngoing(true)
 | 
			
		||||
                        .setSilent(true)
 | 
			
		||||
                        .setColor(
 | 
			
		||||
                                context.getColor(
 | 
			
		||||
                                        com.android.internal.R.color
 | 
			
		||||
                                                .system_notification_accent_color))
 | 
			
		||||
                        .setContentIntent(settingsPendingIntent)
 | 
			
		||||
                        .addAction(stopAction)
 | 
			
		||||
                        .addAction(settingsAction)
 | 
			
		||||
                        .addExtras(extras);
 | 
			
		||||
        nm.notify(NOTIFICATION_ID, builder.build());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void cancelSharingNotification(Context context) {
 | 
			
		||||
        NotificationManager nm = context.getSystemService(NotificationManager.class);
 | 
			
		||||
        nm.cancel(NOTIFICATION_ID);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,162 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog;
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
import androidx.fragment.app.FragmentManager;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.Iterables;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingStopDialogFragment extends InstrumentedDialogFragment {
 | 
			
		||||
    private static final String TAG = "AudioSharingStopDialog";
 | 
			
		||||
 | 
			
		||||
    private static final String BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS =
 | 
			
		||||
            "bundle_key_device_to_disconnect_items";
 | 
			
		||||
    private static final String BUNDLE_KEY_NEW_DEVICE_NAME = "bundle_key_new_device_name";
 | 
			
		||||
 | 
			
		||||
    // The host creates an instance of this dialog fragment must implement this interface to receive
 | 
			
		||||
    // event callbacks.
 | 
			
		||||
    public interface DialogEventListener {
 | 
			
		||||
        /** Called when users click the stop sharing button in the dialog. */
 | 
			
		||||
        void onStopSharingClick();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable private static DialogEventListener sListener;
 | 
			
		||||
    @Nullable private static CachedBluetoothDevice sCachedDevice;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        return SettingsEnums.DIALOG_STOP_AUDIO_SHARING;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the {@link AudioSharingStopDialogFragment} dialog.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>If the dialog is showing, update the dialog message and event listener.
 | 
			
		||||
     *
 | 
			
		||||
     * @param host The Fragment this dialog will be hosted.
 | 
			
		||||
     * @param deviceItems The existing connected device items in audio sharing session.
 | 
			
		||||
     * @param newDevice The latest connected device triggered this dialog.
 | 
			
		||||
     * @param listener The callback to handle the user action on this dialog.
 | 
			
		||||
     */
 | 
			
		||||
    public static void show(
 | 
			
		||||
            @NonNull Fragment host,
 | 
			
		||||
            @NonNull List<AudioSharingDeviceItem> deviceItems,
 | 
			
		||||
            @NonNull CachedBluetoothDevice newDevice,
 | 
			
		||||
            @NonNull DialogEventListener listener) {
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) return;
 | 
			
		||||
        final FragmentManager manager = host.getChildFragmentManager();
 | 
			
		||||
        AlertDialog dialog = AudioSharingDialogHelper.getDialogIfShowing(manager, TAG);
 | 
			
		||||
        if (dialog != null) {
 | 
			
		||||
            int newGroupId = AudioSharingUtils.getGroupId(newDevice);
 | 
			
		||||
            if (sCachedDevice != null
 | 
			
		||||
                    && newGroupId == AudioSharingUtils.getGroupId(sCachedDevice)) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        String.format(
 | 
			
		||||
                                Locale.US,
 | 
			
		||||
                                "Dialog is showing for the same device group %d, return.",
 | 
			
		||||
                                newGroupId));
 | 
			
		||||
                sListener = listener;
 | 
			
		||||
                sCachedDevice = newDevice;
 | 
			
		||||
                return;
 | 
			
		||||
            } else {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        String.format(
 | 
			
		||||
                                Locale.US,
 | 
			
		||||
                                "Dialog is showing for new device group %d, "
 | 
			
		||||
                                        + "dismiss current dialog.",
 | 
			
		||||
                                newGroupId));
 | 
			
		||||
                dialog.dismiss();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        sListener = listener;
 | 
			
		||||
        sCachedDevice = newDevice;
 | 
			
		||||
        Log.d(TAG, "Show up the dialog.");
 | 
			
		||||
        final Bundle bundle = new Bundle();
 | 
			
		||||
        bundle.putParcelableList(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, deviceItems);
 | 
			
		||||
        bundle.putString(BUNDLE_KEY_NEW_DEVICE_NAME, newDevice.getName());
 | 
			
		||||
        AudioSharingStopDialogFragment dialogFrag = new AudioSharingStopDialogFragment();
 | 
			
		||||
        dialogFrag.setArguments(bundle);
 | 
			
		||||
        dialogFrag.show(manager, TAG);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Return the tag of {@link AudioSharingStopDialogFragment} dialog. */
 | 
			
		||||
    public static @NonNull String tag() {
 | 
			
		||||
        return TAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Get the latest connected device which triggers the dialog. */
 | 
			
		||||
    public @Nullable CachedBluetoothDevice getDevice() {
 | 
			
		||||
        return sCachedDevice;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
 | 
			
		||||
        Bundle arguments = requireArguments();
 | 
			
		||||
        List<AudioSharingDeviceItem> deviceItems =
 | 
			
		||||
                arguments.getParcelable(BUNDLE_KEY_DEVICE_TO_DISCONNECT_ITEMS, List.class);
 | 
			
		||||
        String newDeviceName = arguments.getString(BUNDLE_KEY_NEW_DEVICE_NAME);
 | 
			
		||||
        String customMessage =
 | 
			
		||||
                deviceItems.size() == 1
 | 
			
		||||
                        ? getString(
 | 
			
		||||
                                R.string.audio_sharing_stop_dialog_content,
 | 
			
		||||
                                Iterables.getOnlyElement(deviceItems).getName())
 | 
			
		||||
                        : (deviceItems.size() == 2
 | 
			
		||||
                                ? getString(
 | 
			
		||||
                                        R.string.audio_sharing_stop_dialog_with_two_content,
 | 
			
		||||
                                        deviceItems.get(0).getName(),
 | 
			
		||||
                                        deviceItems.get(1).getName())
 | 
			
		||||
                                : getString(R.string.audio_sharing_stop_dialog_with_more_content));
 | 
			
		||||
        AlertDialog dialog =
 | 
			
		||||
                AudioSharingDialogFactory.newBuilder(getActivity())
 | 
			
		||||
                        .setTitle(
 | 
			
		||||
                                getString(R.string.audio_sharing_stop_dialog_title, newDeviceName))
 | 
			
		||||
                        .setTitleIcon(com.android.settings.R.drawable.ic_warning_24dp)
 | 
			
		||||
                        .setIsCustomBodyEnabled(true)
 | 
			
		||||
                        .setCustomMessage(customMessage)
 | 
			
		||||
                        .setPositiveButton(
 | 
			
		||||
                                R.string.audio_sharing_connect_button_label,
 | 
			
		||||
                                (dlg, which) -> {
 | 
			
		||||
                                    if (sListener != null) {
 | 
			
		||||
                                        sListener.onStopSharingClick();
 | 
			
		||||
                                    }
 | 
			
		||||
                                })
 | 
			
		||||
                        .setNegativeButton(
 | 
			
		||||
                                com.android.settings.R.string.cancel, (dlg, which) -> dismiss())
 | 
			
		||||
                        .build();
 | 
			
		||||
        dialog.show();
 | 
			
		||||
        AudioSharingDialogHelper.updateMessageStyle(dialog);
 | 
			
		||||
        return dialog;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,522 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcast;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastAssistant;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.content.BroadcastReceiver;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.IntentFilter;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.widget.CompoundButton;
 | 
			
		||||
import android.widget.CompoundButton.OnCheckedChangeListener;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settings.widget.SettingsMainSwitchBar;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingSwitchBarController extends BasePreferenceController
 | 
			
		||||
        implements DefaultLifecycleObserver,
 | 
			
		||||
                OnCheckedChangeListener,
 | 
			
		||||
                LocalBluetoothProfileManager.ServiceListener {
 | 
			
		||||
    private static final String TAG = "AudioSharingSwitchBarCtl";
 | 
			
		||||
    private static final String PREF_KEY = "audio_sharing_main_switch";
 | 
			
		||||
 | 
			
		||||
    interface OnAudioSharingStateChangedListener {
 | 
			
		||||
        /**
 | 
			
		||||
         * The callback which will be triggered when:
 | 
			
		||||
         *
 | 
			
		||||
         * <p>1. Bluetooth on/off state changes. 2. Broadcast and assistant profile
 | 
			
		||||
         * connect/disconnect state changes. 3. Audio sharing start/stop state changes.
 | 
			
		||||
         */
 | 
			
		||||
        void onAudioSharingStateChanged();
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * The callback which will be triggered when:
 | 
			
		||||
         *
 | 
			
		||||
         * <p>Broadcast and assistant profile connected.
 | 
			
		||||
         */
 | 
			
		||||
        void onAudioSharingProfilesConnected();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private final SettingsMainSwitchBar mSwitchBar;
 | 
			
		||||
    private final BluetoothAdapter mBluetoothAdapter;
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothProfileManager mProfileManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcast mBroadcast;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
 | 
			
		||||
    @Nullable private DashboardFragment mFragment;
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    private final OnAudioSharingStateChangedListener mListener;
 | 
			
		||||
    private Map<Integer, List<CachedBluetoothDevice>> mGroupedConnectedDevices = new HashMap<>();
 | 
			
		||||
    private List<BluetoothDevice> mTargetActiveSinks = new ArrayList<>();
 | 
			
		||||
    private List<AudioSharingDeviceItem> mDeviceItemsForSharing = new ArrayList<>();
 | 
			
		||||
    @VisibleForTesting IntentFilter mIntentFilter;
 | 
			
		||||
    private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    BroadcastReceiver mReceiver =
 | 
			
		||||
            new BroadcastReceiver() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onReceive(Context context, Intent intent) {
 | 
			
		||||
                    updateSwitch();
 | 
			
		||||
                    mListener.onAudioSharingStateChanged();
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    private final BluetoothLeBroadcast.Callback mBroadcastCallback =
 | 
			
		||||
            new BluetoothLeBroadcast.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStarted(int reason, int broadcastId) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onBroadcastStarted(), reason = "
 | 
			
		||||
                                    + reason
 | 
			
		||||
                                    + ", broadcastId = "
 | 
			
		||||
                                    + broadcastId);
 | 
			
		||||
                    updateSwitch();
 | 
			
		||||
                    mListener.onAudioSharingStateChanged();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStartFailed(int reason) {
 | 
			
		||||
                    Log.d(TAG, "onBroadcastStartFailed(), reason = " + reason);
 | 
			
		||||
                    // TODO: handle broadcast start fail
 | 
			
		||||
                    updateSwitch();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastMetadataChanged(
 | 
			
		||||
                        int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onBroadcastMetadataChanged(), broadcastId = "
 | 
			
		||||
                                    + broadcastId
 | 
			
		||||
                                    + ", metadata = "
 | 
			
		||||
                                    + metadata.getBroadcastName());
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStopped(int reason, int broadcastId) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onBroadcastStopped(), reason = "
 | 
			
		||||
                                    + reason
 | 
			
		||||
                                    + ", broadcastId = "
 | 
			
		||||
                                    + broadcastId);
 | 
			
		||||
                    updateSwitch();
 | 
			
		||||
                    mListener.onAudioSharingStateChanged();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastStopFailed(int reason) {
 | 
			
		||||
                    Log.d(TAG, "onBroadcastStopFailed(), reason = " + reason);
 | 
			
		||||
                    // TODO: handle broadcast stop fail
 | 
			
		||||
                    updateSwitch();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastUpdated(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBroadcastUpdateFailed(int reason, int broadcastId) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onPlaybackStarted(int reason, int broadcastId) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onPlaybackStarted(), reason = "
 | 
			
		||||
                                    + reason
 | 
			
		||||
                                    + ", broadcastId = "
 | 
			
		||||
                                    + broadcastId);
 | 
			
		||||
                    handleOnBroadcastReady();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onPlaybackStopped(int reason, int broadcastId) {}
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
 | 
			
		||||
            new BluetoothLeBroadcastAssistant.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStarted(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStartFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStopped(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStopFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onSourceAdded(), sink = "
 | 
			
		||||
                                    + sink
 | 
			
		||||
                                    + ", sourceId = "
 | 
			
		||||
                                    + sourceId
 | 
			
		||||
                                    + ", reason = "
 | 
			
		||||
                                    + reason);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAddFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink,
 | 
			
		||||
                        @NonNull BluetoothLeBroadcastMetadata source,
 | 
			
		||||
                        int reason) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onSourceAddFailed(), sink = "
 | 
			
		||||
                                    + sink
 | 
			
		||||
                                    + ", source = "
 | 
			
		||||
                                    + source
 | 
			
		||||
                                    + ", reason = "
 | 
			
		||||
                                    + reason);
 | 
			
		||||
                    AudioSharingUtils.toastMessage(
 | 
			
		||||
                            mContext,
 | 
			
		||||
                            String.format(
 | 
			
		||||
                                    Locale.US,
 | 
			
		||||
                                    "Fail to add source to %s reason %d",
 | 
			
		||||
                                    sink.getAddress(),
 | 
			
		||||
                                    reason));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceModified(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceModifyFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoved(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoveFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onReceiveStateChanged(
 | 
			
		||||
                        BluetoothDevice sink,
 | 
			
		||||
                        int sourceId,
 | 
			
		||||
                        BluetoothLeBroadcastReceiveState state) {}
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    AudioSharingSwitchBarController(
 | 
			
		||||
            Context context,
 | 
			
		||||
            SettingsMainSwitchBar switchBar,
 | 
			
		||||
            OnAudioSharingStateChangedListener listener) {
 | 
			
		||||
        super(context, PREF_KEY);
 | 
			
		||||
        mSwitchBar = switchBar;
 | 
			
		||||
        mListener = listener;
 | 
			
		||||
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 | 
			
		||||
        mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
 | 
			
		||||
        mBtManager = Utils.getLocalBtManager(context);
 | 
			
		||||
        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
 | 
			
		||||
        mBroadcast = mProfileManager == null ? null : mProfileManager.getLeAudioBroadcastProfile();
 | 
			
		||||
        mAssistant =
 | 
			
		||||
                mProfileManager == null
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : mProfileManager.getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks. Feature is not available.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mContext.registerReceiver(mReceiver, mIntentFilter, Context.RECEIVER_EXPORTED_UNAUDITED);
 | 
			
		||||
        updateSwitch();
 | 
			
		||||
        if (!AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            if (mProfileManager != null) {
 | 
			
		||||
                mProfileManager.addServiceListener(this);
 | 
			
		||||
            }
 | 
			
		||||
            Log.d(TAG, "Skip register callbacks. Profile is not ready.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        registerCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip unregister callbacks. Feature is not available.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mContext.unregisterReceiver(mReceiver);
 | 
			
		||||
        if (mProfileManager != null) {
 | 
			
		||||
            mProfileManager.removeServiceListener(this);
 | 
			
		||||
        }
 | 
			
		||||
        unregisterCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
 | 
			
		||||
        // Filter out unnecessary callbacks when switch is disabled.
 | 
			
		||||
        if (!buttonView.isEnabled()) return;
 | 
			
		||||
        if (isChecked) {
 | 
			
		||||
            mSwitchBar.setEnabled(false);
 | 
			
		||||
            boolean isBroadcasting = AudioSharingUtils.isBroadcasting(mBtManager);
 | 
			
		||||
            if (mAssistant == null || mBroadcast == null || isBroadcasting) {
 | 
			
		||||
                Log.d(TAG, "Skip startAudioSharing, already broadcasting or not support.");
 | 
			
		||||
                mSwitchBar.setEnabled(true);
 | 
			
		||||
                if (!isBroadcasting) {
 | 
			
		||||
                    mSwitchBar.setChecked(false);
 | 
			
		||||
                }
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (mAssistant
 | 
			
		||||
                    .getDevicesMatchingConnectionStates(
 | 
			
		||||
                            new int[] {BluetoothProfile.STATE_CONNECTED})
 | 
			
		||||
                    .isEmpty()) {
 | 
			
		||||
                // Pop up dialog to ask users to connect at least one lea buds before audio sharing.
 | 
			
		||||
                AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                        mContext,
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            mSwitchBar.setEnabled(true);
 | 
			
		||||
                            mSwitchBar.setChecked(false);
 | 
			
		||||
                            if (mFragment != null) {
 | 
			
		||||
                                AudioSharingConfirmDialogFragment.show(mFragment);
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            startAudioSharing();
 | 
			
		||||
        } else {
 | 
			
		||||
            stopAudioSharing();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onServiceConnected() {
 | 
			
		||||
        Log.d(TAG, "onServiceConnected()");
 | 
			
		||||
        if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            registerCallbacks();
 | 
			
		||||
            updateSwitch();
 | 
			
		||||
            mListener.onAudioSharingProfilesConnected();
 | 
			
		||||
            mListener.onAudioSharingStateChanged();
 | 
			
		||||
            if (mProfileManager != null) {
 | 
			
		||||
                mProfileManager.removeServiceListener(this);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onServiceDisconnected() {
 | 
			
		||||
        Log.d(TAG, "onServiceDisconnected()");
 | 
			
		||||
        // Do nothing.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize the controller.
 | 
			
		||||
     *
 | 
			
		||||
     * @param fragment The fragment to host the {@link AudioSharingSwitchBarController} dialog.
 | 
			
		||||
     */
 | 
			
		||||
    public void init(DashboardFragment fragment) {
 | 
			
		||||
        this.mFragment = fragment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Test only: set callback registration status in tests. */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setCallbacksRegistered(boolean registered) {
 | 
			
		||||
        mCallbacksRegistered.set(registered);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void registerCallbacks() {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip registerCallbacks(). Feature is not available.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mBroadcast == null || mAssistant == null) {
 | 
			
		||||
            Log.d(TAG, "Skip registerCallbacks(). Profile not support on this device.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!mCallbacksRegistered.get()) {
 | 
			
		||||
            Log.d(TAG, "registerCallbacks()");
 | 
			
		||||
            mSwitchBar.addOnSwitchChangeListener(this);
 | 
			
		||||
            mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback);
 | 
			
		||||
            mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
 | 
			
		||||
            mCallbacksRegistered.set(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void unregisterCallbacks() {
 | 
			
		||||
        if (!isAvailable() || !AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            Log.d(TAG, "Skip unregisterCallbacks(). Feature is not available.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mBroadcast == null || mAssistant == null) {
 | 
			
		||||
            Log.d(TAG, "Skip unregisterCallbacks(). Profile not support on this device.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mCallbacksRegistered.get()) {
 | 
			
		||||
            Log.d(TAG, "unregisterCallbacks()");
 | 
			
		||||
            mSwitchBar.removeOnSwitchChangeListener(this);
 | 
			
		||||
            mBroadcast.unregisterServiceCallBack(mBroadcastCallback);
 | 
			
		||||
            mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
 | 
			
		||||
            mCallbacksRegistered.set(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void startAudioSharing() {
 | 
			
		||||
        // Compute the device connection state before start audio sharing since the devices will
 | 
			
		||||
        // be set to inactive after the broadcast started.
 | 
			
		||||
        mGroupedConnectedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(mBtManager);
 | 
			
		||||
        List<AudioSharingDeviceItem> deviceItems =
 | 
			
		||||
                AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
 | 
			
		||||
                        mBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ false);
 | 
			
		||||
        // deviceItems is ordered. The active device is the first place if exits.
 | 
			
		||||
        mDeviceItemsForSharing = new ArrayList<>(deviceItems);
 | 
			
		||||
        mTargetActiveSinks = new ArrayList<>();
 | 
			
		||||
        if (!deviceItems.isEmpty() && deviceItems.get(0).isActive()) {
 | 
			
		||||
            for (CachedBluetoothDevice device :
 | 
			
		||||
                    mGroupedConnectedDevices.getOrDefault(
 | 
			
		||||
                            deviceItems.get(0).getGroupId(), ImmutableList.of())) {
 | 
			
		||||
                // If active device exists for audio sharing, share to it
 | 
			
		||||
                // automatically once the broadcast is started.
 | 
			
		||||
                mTargetActiveSinks.add(device.getDevice());
 | 
			
		||||
            }
 | 
			
		||||
            mDeviceItemsForSharing.remove(0);
 | 
			
		||||
        }
 | 
			
		||||
        if (mBroadcast != null) {
 | 
			
		||||
            mBroadcast.startPrivateBroadcast();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void stopAudioSharing() {
 | 
			
		||||
        mSwitchBar.setEnabled(false);
 | 
			
		||||
        if (!AudioSharingUtils.isBroadcasting(mBtManager)) {
 | 
			
		||||
            Log.d(TAG, "Skip stopAudioSharing, already not broadcasting or broadcast not support.");
 | 
			
		||||
            mSwitchBar.setEnabled(true);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mBroadcast != null) {
 | 
			
		||||
            mBroadcast.stopBroadcast(mBroadcast.getLatestBroadcastId());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateSwitch() {
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            boolean isBroadcasting = AudioSharingUtils.isBroadcasting(mBtManager);
 | 
			
		||||
                            boolean isStateReady =
 | 
			
		||||
                                    isBluetoothOn()
 | 
			
		||||
                                            && AudioSharingUtils.isAudioSharingProfileReady(
 | 
			
		||||
                                                    mProfileManager);
 | 
			
		||||
                            AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                                    mContext,
 | 
			
		||||
                                    () -> {
 | 
			
		||||
                                        if (mSwitchBar.isChecked() != isBroadcasting) {
 | 
			
		||||
                                            mSwitchBar.setChecked(isBroadcasting);
 | 
			
		||||
                                        }
 | 
			
		||||
                                        if (mSwitchBar.isEnabled() != isStateReady) {
 | 
			
		||||
                                            mSwitchBar.setEnabled(isStateReady);
 | 
			
		||||
                                        }
 | 
			
		||||
                                        Log.d(
 | 
			
		||||
                                                TAG,
 | 
			
		||||
                                                "updateSwitch, checked = "
 | 
			
		||||
                                                        + isBroadcasting
 | 
			
		||||
                                                        + ", enabled = "
 | 
			
		||||
                                                        + isStateReady);
 | 
			
		||||
                                    });
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isBluetoothOn() {
 | 
			
		||||
        return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handleOnBroadcastReady() {
 | 
			
		||||
        AudioSharingUtils.addSourceToTargetSinks(mTargetActiveSinks, mBtManager);
 | 
			
		||||
        mTargetActiveSinks.clear();
 | 
			
		||||
        if (mFragment == null) {
 | 
			
		||||
            Log.w(TAG, "Dialog fail to show due to null fragment.");
 | 
			
		||||
            mGroupedConnectedDevices.clear();
 | 
			
		||||
            mDeviceItemsForSharing.clear();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                mContext,
 | 
			
		||||
                () -> {
 | 
			
		||||
                    // Check nullability to pass NullAway check
 | 
			
		||||
                    if (mFragment != null) {
 | 
			
		||||
                        AudioSharingDialogFragment.show(
 | 
			
		||||
                                mFragment,
 | 
			
		||||
                                mDeviceItemsForSharing,
 | 
			
		||||
                                item -> {
 | 
			
		||||
                                    AudioSharingUtils.addSourceToTargetSinks(
 | 
			
		||||
                                            mGroupedConnectedDevices
 | 
			
		||||
                                                    .getOrDefault(
 | 
			
		||||
                                                            item.getGroupId(), ImmutableList.of())
 | 
			
		||||
                                                    .stream()
 | 
			
		||||
                                                    .map(CachedBluetoothDevice::getDevice)
 | 
			
		||||
                                                    .collect(Collectors.toList()),
 | 
			
		||||
                                            mBtManager);
 | 
			
		||||
                                    mGroupedConnectedDevices.clear();
 | 
			
		||||
                                    mDeviceItemsForSharing.clear();
 | 
			
		||||
                                });
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,386 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.bluetooth.BluetoothCsipSetCoordinator;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.bluetooth.BluetoothStatusCodes;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.provider.Settings;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.widget.Toast;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LeAudioProfile;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfile;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.VolumeControlProfile;
 | 
			
		||||
import com.android.settingslib.flags.Flags;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
public class AudioSharingUtils {
 | 
			
		||||
    public static final String SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID =
 | 
			
		||||
            "bluetooth_le_broadcast_fallback_active_group_id";
 | 
			
		||||
    private static final String TAG = "AudioSharingUtils";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch {@link CachedBluetoothDevice}s connected to the broadcast assistant. The devices are
 | 
			
		||||
     * grouped by CSIP group id.
 | 
			
		||||
     *
 | 
			
		||||
     * @param localBtManager The BT manager to provide BT functions.
 | 
			
		||||
     * @return A map of connected devices grouped by CSIP group id.
 | 
			
		||||
     */
 | 
			
		||||
    public static Map<Integer, List<CachedBluetoothDevice>> fetchConnectedDevicesByGroupId(
 | 
			
		||||
            @Nullable LocalBluetoothManager localBtManager) {
 | 
			
		||||
        Map<Integer, List<CachedBluetoothDevice>> groupedDevices = new HashMap<>();
 | 
			
		||||
        if (localBtManager == null) {
 | 
			
		||||
            Log.d(TAG, "Skip fetchConnectedDevicesByGroupId due to bt manager is null");
 | 
			
		||||
            return groupedDevices;
 | 
			
		||||
        }
 | 
			
		||||
        LocalBluetoothLeBroadcastAssistant assistant =
 | 
			
		||||
                localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
        if (assistant == null) {
 | 
			
		||||
            Log.d(TAG, "Skip fetchConnectedDevicesByGroupId due to assistant profile is null");
 | 
			
		||||
            return groupedDevices;
 | 
			
		||||
        }
 | 
			
		||||
        List<BluetoothDevice> connectedDevices =
 | 
			
		||||
                assistant.getDevicesMatchingConnectionStates(
 | 
			
		||||
                        new int[] {BluetoothProfile.STATE_CONNECTED});
 | 
			
		||||
        CachedBluetoothDeviceManager cacheManager = localBtManager.getCachedDeviceManager();
 | 
			
		||||
        for (BluetoothDevice device : connectedDevices) {
 | 
			
		||||
            CachedBluetoothDevice cachedDevice = cacheManager.findDevice(device);
 | 
			
		||||
            if (cachedDevice == null) {
 | 
			
		||||
                Log.d(TAG, "Skip device due to not being cached: " + device.getAnonymizedAddress());
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            int groupId = getGroupId(cachedDevice);
 | 
			
		||||
            if (groupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "Skip device due to no valid group id: " + device.getAnonymizedAddress());
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            if (!groupedDevices.containsKey(groupId)) {
 | 
			
		||||
                groupedDevices.put(groupId, new ArrayList<>());
 | 
			
		||||
            }
 | 
			
		||||
            groupedDevices.get(groupId).add(cachedDevice);
 | 
			
		||||
        }
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "fetchConnectedDevicesByGroupId: " + groupedDevices);
 | 
			
		||||
        }
 | 
			
		||||
        return groupedDevices;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch a list of ordered connected lead {@link CachedBluetoothDevice}s eligible for audio
 | 
			
		||||
     * sharing. The active device is placed in the first place if it exists. The devices can be
 | 
			
		||||
     * filtered by whether it is already in the audio sharing session.
 | 
			
		||||
     *
 | 
			
		||||
     * @param localBtManager The BT manager to provide BT functions. *
 | 
			
		||||
     * @param groupedConnectedDevices devices connected to broadcast assistant grouped by CSIP group
 | 
			
		||||
     *     id.
 | 
			
		||||
     * @param filterByInSharing Whether to filter the device by if is already in the sharing
 | 
			
		||||
     *     session.
 | 
			
		||||
     * @return A list of ordered connected devices eligible for the audio sharing. The active device
 | 
			
		||||
     *     is placed in the first place if it exists.
 | 
			
		||||
     */
 | 
			
		||||
    public static List<CachedBluetoothDevice> buildOrderedConnectedLeadDevices(
 | 
			
		||||
            @Nullable LocalBluetoothManager localBtManager,
 | 
			
		||||
            Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices,
 | 
			
		||||
            boolean filterByInSharing) {
 | 
			
		||||
        List<CachedBluetoothDevice> orderedDevices = new ArrayList<>();
 | 
			
		||||
        for (List<CachedBluetoothDevice> devices : groupedConnectedDevices.values()) {
 | 
			
		||||
            @Nullable CachedBluetoothDevice leadDevice = getLeadDevice(devices);
 | 
			
		||||
            if (leadDevice == null) {
 | 
			
		||||
                Log.d(TAG, "Skip due to no lead device");
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            if (filterByInSharing
 | 
			
		||||
                    && !BluetoothUtils.hasConnectedBroadcastSource(leadDevice, localBtManager)) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "Filtered the device due to not in sharing session: "
 | 
			
		||||
                                + leadDevice.getDevice().getAnonymizedAddress());
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            orderedDevices.add(leadDevice);
 | 
			
		||||
        }
 | 
			
		||||
        orderedDevices.sort(
 | 
			
		||||
                (CachedBluetoothDevice d1, CachedBluetoothDevice d2) -> {
 | 
			
		||||
                    // Active above not inactive
 | 
			
		||||
                    int comparison =
 | 
			
		||||
                            (isActiveLeAudioDevice(d2) ? 1 : 0)
 | 
			
		||||
                                    - (isActiveLeAudioDevice(d1) ? 1 : 0);
 | 
			
		||||
                    if (comparison != 0) return comparison;
 | 
			
		||||
                    // Bonded above not bonded
 | 
			
		||||
                    comparison =
 | 
			
		||||
                            (d2.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0)
 | 
			
		||||
                                    - (d1.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
 | 
			
		||||
                    if (comparison != 0) return comparison;
 | 
			
		||||
                    // Bond timestamp available above unavailable
 | 
			
		||||
                    comparison =
 | 
			
		||||
                            (d2.getBondTimestamp() != null ? 1 : 0)
 | 
			
		||||
                                    - (d1.getBondTimestamp() != null ? 1 : 0);
 | 
			
		||||
                    if (comparison != 0) return comparison;
 | 
			
		||||
                    // Order by bond timestamp if it is available
 | 
			
		||||
                    // Otherwise order by device name
 | 
			
		||||
                    return d1.getBondTimestamp() != null
 | 
			
		||||
                            ? d1.getBondTimestamp().compareTo(d2.getBondTimestamp())
 | 
			
		||||
                            : d1.getName().compareTo(d2.getName());
 | 
			
		||||
                });
 | 
			
		||||
        return orderedDevices;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get the lead device from a list of devices with same group id.
 | 
			
		||||
     *
 | 
			
		||||
     * @param devices A list of devices with same group id.
 | 
			
		||||
     * @return The lead device
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    public static CachedBluetoothDevice getLeadDevice(
 | 
			
		||||
            @NonNull List<CachedBluetoothDevice> devices) {
 | 
			
		||||
        if (devices.isEmpty()) return null;
 | 
			
		||||
        for (CachedBluetoothDevice device : devices) {
 | 
			
		||||
            if (!device.getMemberDevice().isEmpty()) {
 | 
			
		||||
                return device;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        CachedBluetoothDevice leadDevice = devices.get(0);
 | 
			
		||||
        Log.d(
 | 
			
		||||
                TAG,
 | 
			
		||||
                "No lead device in the group, pick arbitrary device as the lead: "
 | 
			
		||||
                        + leadDevice.getDevice().getAnonymizedAddress());
 | 
			
		||||
        return leadDevice;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Fetch a list of ordered connected lead {@link AudioSharingDeviceItem}s eligible for audio
 | 
			
		||||
     * sharing. The active device is placed in the first place if it exists. The devices can be
 | 
			
		||||
     * filtered by whether it is already in the audio sharing session.
 | 
			
		||||
     *
 | 
			
		||||
     * @param localBtManager The BT manager to provide BT functions. *
 | 
			
		||||
     * @param groupedConnectedDevices devices connected to broadcast assistant grouped by CSIP group
 | 
			
		||||
     *     id.
 | 
			
		||||
     * @param filterByInSharing Whether to filter the device by if is already in the sharing
 | 
			
		||||
     *     session.
 | 
			
		||||
     * @return A list of ordered connected devices eligible for the audio sharing. The active device
 | 
			
		||||
     *     is placed in the first place if it exists.
 | 
			
		||||
     */
 | 
			
		||||
    @NonNull
 | 
			
		||||
    public static List<AudioSharingDeviceItem> buildOrderedConnectedLeadAudioSharingDeviceItem(
 | 
			
		||||
            @Nullable LocalBluetoothManager localBtManager,
 | 
			
		||||
            Map<Integer, List<CachedBluetoothDevice>> groupedConnectedDevices,
 | 
			
		||||
            boolean filterByInSharing) {
 | 
			
		||||
        return buildOrderedConnectedLeadDevices(
 | 
			
		||||
                        localBtManager, groupedConnectedDevices, filterByInSharing)
 | 
			
		||||
                .stream()
 | 
			
		||||
                .map(device -> buildAudioSharingDeviceItem(device))
 | 
			
		||||
                .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Build {@link AudioSharingDeviceItem} from {@link CachedBluetoothDevice}. */
 | 
			
		||||
    public static AudioSharingDeviceItem buildAudioSharingDeviceItem(
 | 
			
		||||
            CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        return new AudioSharingDeviceItem(
 | 
			
		||||
                cachedDevice.getName(),
 | 
			
		||||
                getGroupId(cachedDevice),
 | 
			
		||||
                isActiveLeAudioDevice(cachedDevice));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if {@link CachedBluetoothDevice} is an active le audio device.
 | 
			
		||||
     *
 | 
			
		||||
     * @param cachedDevice The cached bluetooth device to check.
 | 
			
		||||
     * @return Whether the device is an active le audio device.
 | 
			
		||||
     */
 | 
			
		||||
    public static boolean isActiveLeAudioDevice(CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        return BluetoothUtils.isActiveLeAudioDevice(cachedDevice);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Toast message on main thread. */
 | 
			
		||||
    public static void toastMessage(Context context, String message) {
 | 
			
		||||
        context.getMainExecutor()
 | 
			
		||||
                .execute(() -> Toast.makeText(context, message, Toast.LENGTH_LONG).show());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Returns if the le audio sharing is enabled. */
 | 
			
		||||
    public static boolean isFeatureEnabled() {
 | 
			
		||||
        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
 | 
			
		||||
        return Flags.enableLeAudioSharing()
 | 
			
		||||
                && adapter.isLeAudioBroadcastSourceSupported()
 | 
			
		||||
                        == BluetoothStatusCodes.FEATURE_SUPPORTED
 | 
			
		||||
                && adapter.isLeAudioBroadcastAssistantSupported()
 | 
			
		||||
                        == BluetoothStatusCodes.FEATURE_SUPPORTED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Add source to target sinks. */
 | 
			
		||||
    public static void addSourceToTargetSinks(
 | 
			
		||||
            List<BluetoothDevice> sinks, @Nullable LocalBluetoothManager localBtManager) {
 | 
			
		||||
        if (localBtManager == null) {
 | 
			
		||||
            Log.d(TAG, "skip addSourceToTargetDevices: LocalBluetoothManager is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (sinks.isEmpty()) {
 | 
			
		||||
            Log.d(TAG, "Skip addSourceToTargetDevices. No sinks.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        LocalBluetoothLeBroadcast broadcast =
 | 
			
		||||
                localBtManager.getProfileManager().getLeAudioBroadcastProfile();
 | 
			
		||||
        if (broadcast == null) {
 | 
			
		||||
            Log.d(TAG, "skip addSourceToTargetDevices. Broadcast profile is null.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        LocalBluetoothLeBroadcastAssistant assistant =
 | 
			
		||||
                localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
        if (assistant == null) {
 | 
			
		||||
            Log.d(TAG, "skip addSourceToTargetDevices. Assistant profile is null.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        BluetoothLeBroadcastMetadata broadcastMetadata =
 | 
			
		||||
                broadcast.getLatestBluetoothLeBroadcastMetadata();
 | 
			
		||||
        if (broadcastMetadata == null) {
 | 
			
		||||
            Log.d(TAG, "skip addSourceToTargetDevices: There is no broadcastMetadata.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        List<BluetoothDevice> connectedDevices =
 | 
			
		||||
                assistant.getDevicesMatchingConnectionStates(
 | 
			
		||||
                        new int[] {BluetoothProfile.STATE_CONNECTED});
 | 
			
		||||
        for (BluetoothDevice sink : sinks) {
 | 
			
		||||
            if (connectedDevices.contains(sink)) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "Add broadcast with broadcastId: "
 | 
			
		||||
                                + broadcastMetadata.getBroadcastId()
 | 
			
		||||
                                + " to the device: "
 | 
			
		||||
                                + sink.getAnonymizedAddress());
 | 
			
		||||
                assistant.addSource(sink, broadcastMetadata, /* isGroupOp= */ false);
 | 
			
		||||
            } else {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "Skip add broadcast with broadcastId: "
 | 
			
		||||
                                + broadcastMetadata.getBroadcastId()
 | 
			
		||||
                                + " to the not connected device: "
 | 
			
		||||
                                + sink.getAnonymizedAddress());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Returns if the broadcast is on-going. */
 | 
			
		||||
    public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
 | 
			
		||||
        if (manager == null) return false;
 | 
			
		||||
        LocalBluetoothLeBroadcast broadcast =
 | 
			
		||||
                manager.getProfileManager().getLeAudioBroadcastProfile();
 | 
			
		||||
        return broadcast != null && broadcast.isEnabled(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Stops the latest broadcast. */
 | 
			
		||||
    public static void stopBroadcasting(@Nullable LocalBluetoothManager manager) {
 | 
			
		||||
        if (manager == null) {
 | 
			
		||||
            Log.d(TAG, "Skip stop broadcasting due to bt manager is null");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        LocalBluetoothLeBroadcast broadcast =
 | 
			
		||||
                manager.getProfileManager().getLeAudioBroadcastProfile();
 | 
			
		||||
        if (broadcast == null) {
 | 
			
		||||
            Log.d(TAG, "Skip stop broadcasting due to broadcast profile is null");
 | 
			
		||||
        }
 | 
			
		||||
        broadcast.stopBroadcast(broadcast.getLatestBroadcastId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get CSIP group id for {@link CachedBluetoothDevice}.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>If CachedBluetoothDevice#getGroupId is invalid, fetch group id from
 | 
			
		||||
     * LeAudioProfile#getGroupId.
 | 
			
		||||
     */
 | 
			
		||||
    public static int getGroupId(CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        int groupId = cachedDevice.getGroupId();
 | 
			
		||||
        String anonymizedAddress = cachedDevice.getDevice().getAnonymizedAddress();
 | 
			
		||||
        if (groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
 | 
			
		||||
            Log.d(TAG, "getGroupId by CSIP profile for device: " + anonymizedAddress);
 | 
			
		||||
            return groupId;
 | 
			
		||||
        }
 | 
			
		||||
        for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
 | 
			
		||||
            if (profile instanceof LeAudioProfile) {
 | 
			
		||||
                Log.d(TAG, "getGroupId by LEA profile for device: " + anonymizedAddress);
 | 
			
		||||
                return ((LeAudioProfile) profile).getGroupId(cachedDevice.getDevice());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress);
 | 
			
		||||
        return BluetoothCsipSetCoordinator.GROUP_ID_INVALID;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Get the fallback active group id from SettingsProvider. */
 | 
			
		||||
    public static int getFallbackActiveGroupId(@NonNull Context context) {
 | 
			
		||||
        return Settings.Secure.getInt(
 | 
			
		||||
                context.getContentResolver(),
 | 
			
		||||
                SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID,
 | 
			
		||||
                BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Post the runnable to main thread. */
 | 
			
		||||
    public static void postOnMainThread(@NonNull Context context, @NonNull Runnable runnable) {
 | 
			
		||||
        context.getMainExecutor().execute(runnable);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */
 | 
			
		||||
    public static boolean isLeAudioSupported(CachedBluetoothDevice cachedDevice) {
 | 
			
		||||
        return cachedDevice.getProfiles().stream()
 | 
			
		||||
                .anyMatch(
 | 
			
		||||
                        profile ->
 | 
			
		||||
                                profile instanceof LeAudioProfile
 | 
			
		||||
                                        && profile.isEnabled(cachedDevice.getDevice()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Check if the LE Audio related profiles ready */
 | 
			
		||||
    public static boolean isAudioSharingProfileReady(
 | 
			
		||||
            @Nullable LocalBluetoothProfileManager profileManager) {
 | 
			
		||||
        if (profileManager == null) return false;
 | 
			
		||||
        LocalBluetoothLeBroadcast broadcast = profileManager.getLeAudioBroadcastProfile();
 | 
			
		||||
        if (broadcast == null || !broadcast.isProfileReady()) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        LocalBluetoothLeBroadcastAssistant assistant =
 | 
			
		||||
                profileManager.getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
        if (assistant == null || !assistant.isProfileReady()) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        VolumeControlProfile vc = profileManager.getVolumeControlProfile();
 | 
			
		||||
        if (vc == null || !vc.isProfileReady()) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,107 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.app.Dialog;
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
import androidx.fragment.app.FragmentManager;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
/** Provides a dialog to choose the active device for calls and alarms. */
 | 
			
		||||
public class CallsAndAlarmsDialogFragment extends InstrumentedDialogFragment {
 | 
			
		||||
    private static final String TAG = "CallsAndAlarmsDialog";
 | 
			
		||||
    private static final String BUNDLE_KEY_DEVICE_ITEMS = "bundle_key_device_items";
 | 
			
		||||
 | 
			
		||||
    // The host creates an instance of this dialog fragment must implement this interface to receive
 | 
			
		||||
    // event callbacks.
 | 
			
		||||
    public interface DialogEventListener {
 | 
			
		||||
        /**
 | 
			
		||||
         * Called when users click the device item to set active for calls and alarms in the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param item The device item clicked.
 | 
			
		||||
         */
 | 
			
		||||
        void onItemClick(AudioSharingDeviceItem item);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable private static DialogEventListener sListener;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        return SettingsEnums.DIALOG_AUDIO_SHARING_SWITCH_ACTIVE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Display the {@link CallsAndAlarmsDialogFragment} dialog.
 | 
			
		||||
     *
 | 
			
		||||
     * @param host The Fragment this dialog will be hosted.
 | 
			
		||||
     * @param deviceItems The connected device items in audio sharing session.
 | 
			
		||||
     * @param listener The callback to handle the user action on this dialog.
 | 
			
		||||
     */
 | 
			
		||||
    public static void show(
 | 
			
		||||
            @NonNull Fragment host,
 | 
			
		||||
            @NonNull List<AudioSharingDeviceItem> deviceItems,
 | 
			
		||||
            @NonNull DialogEventListener listener) {
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) return;
 | 
			
		||||
        final FragmentManager manager = host.getChildFragmentManager();
 | 
			
		||||
        sListener = listener;
 | 
			
		||||
        if (manager.findFragmentByTag(TAG) == null) {
 | 
			
		||||
            final Bundle bundle = new Bundle();
 | 
			
		||||
            bundle.putParcelableList(BUNDLE_KEY_DEVICE_ITEMS, deviceItems);
 | 
			
		||||
            final CallsAndAlarmsDialogFragment dialog = new CallsAndAlarmsDialogFragment();
 | 
			
		||||
            dialog.setArguments(bundle);
 | 
			
		||||
            dialog.show(manager, TAG);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Dialog onCreateDialog(Bundle savedInstanceState) {
 | 
			
		||||
        Bundle arguments = requireArguments();
 | 
			
		||||
        List<AudioSharingDeviceItem> deviceItems =
 | 
			
		||||
                arguments.getParcelable(BUNDLE_KEY_DEVICE_ITEMS, List.class);
 | 
			
		||||
        int checkedItem = -1;
 | 
			
		||||
        for (AudioSharingDeviceItem item : deviceItems) {
 | 
			
		||||
            int fallbackActiveGroupId = AudioSharingUtils.getFallbackActiveGroupId(getContext());
 | 
			
		||||
            if (item.getGroupId() == fallbackActiveGroupId) {
 | 
			
		||||
                checkedItem = deviceItems.indexOf(item);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        String[] choices =
 | 
			
		||||
                deviceItems.stream().map(AudioSharingDeviceItem::getName).toArray(String[]::new);
 | 
			
		||||
        AlertDialog.Builder builder =
 | 
			
		||||
                new AlertDialog.Builder(getActivity())
 | 
			
		||||
                        .setTitle(R.string.audio_sharing_call_audio_title)
 | 
			
		||||
                        .setSingleChoiceItems(
 | 
			
		||||
                                choices,
 | 
			
		||||
                                checkedItem,
 | 
			
		||||
                                (dialog, which) -> {
 | 
			
		||||
                                    if (sListener != null) {
 | 
			
		||||
                                        sListener.onItemClick(deviceItems.get(which));
 | 
			
		||||
                                    }
 | 
			
		||||
                                });
 | 
			
		||||
        return builder.create();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,361 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import static com.android.settings.connecteddevice.audiosharing.AudioSharingUtils.SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.bluetooth.BluetoothCsipSetCoordinator;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastAssistant;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.content.ContentResolver;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.database.ContentObserver;
 | 
			
		||||
import android.os.Handler;
 | 
			
		||||
import android.os.Looper;
 | 
			
		||||
import android.provider.Settings;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothEventManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
 | 
			
		||||
/** PreferenceController to control the dialog to choose the active device for calls and alarms */
 | 
			
		||||
public class CallsAndAlarmsPreferenceController extends AudioSharingBasePreferenceController
 | 
			
		||||
        implements BluetoothCallback {
 | 
			
		||||
    private static final String TAG = "CallsAndAlarmsPreferenceController";
 | 
			
		||||
    private static final String PREF_KEY = "calls_and_alarms";
 | 
			
		||||
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothProfileManager mProfileManager;
 | 
			
		||||
    @Nullable private final BluetoothEventManager mEventManager;
 | 
			
		||||
    @Nullable private final ContentResolver mContentResolver;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant;
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    private final ContentObserver mSettingsObserver;
 | 
			
		||||
    @Nullable private DashboardFragment mFragment;
 | 
			
		||||
    Map<Integer, List<CachedBluetoothDevice>> mGroupedConnectedDevices = new HashMap<>();
 | 
			
		||||
    private List<AudioSharingDeviceItem> mDeviceItemsInSharingSession = new ArrayList<>();
 | 
			
		||||
    private AtomicBoolean mCallbacksRegistered = new AtomicBoolean(false);
 | 
			
		||||
    private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
 | 
			
		||||
            new BluetoothLeBroadcastAssistant.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStarted(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStartFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStopped(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSearchStopFailed(int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAdded(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAddFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink,
 | 
			
		||||
                        @NonNull BluetoothLeBroadcastMetadata source,
 | 
			
		||||
                        int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceModified(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceModifyFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoved(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoveFailed(
 | 
			
		||||
                        @NonNull BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onReceiveStateChanged(
 | 
			
		||||
                        @NonNull BluetoothDevice sink,
 | 
			
		||||
                        int sourceId,
 | 
			
		||||
                        @NonNull BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
                    if (BluetoothUtils.isConnected(state)) {
 | 
			
		||||
                        Log.d(TAG, "onReceiveStateChanged: synced, updateSummary");
 | 
			
		||||
                        updateSummary();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    public CallsAndAlarmsPreferenceController(Context context) {
 | 
			
		||||
        super(context, PREF_KEY);
 | 
			
		||||
        mBtManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
 | 
			
		||||
        mEventManager = mBtManager == null ? null : mBtManager.getEventManager();
 | 
			
		||||
        mAssistant =
 | 
			
		||||
                mProfileManager == null
 | 
			
		||||
                        ? null
 | 
			
		||||
                        : mProfileManager.getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
        mContentResolver = context.getContentResolver();
 | 
			
		||||
        mSettingsObserver = new FallbackDeviceGroupIdSettingsObserver();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private class FallbackDeviceGroupIdSettingsObserver extends ContentObserver {
 | 
			
		||||
        FallbackDeviceGroupIdSettingsObserver() {
 | 
			
		||||
            super(new Handler(Looper.getMainLooper()));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Override
 | 
			
		||||
        public void onChange(boolean selfChange) {
 | 
			
		||||
            Log.d(TAG, "onChange, fallback device group id has been changed");
 | 
			
		||||
            var unused = ThreadUtils.postOnBackgroundThread(() -> updateSummary());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getPreferenceKey() {
 | 
			
		||||
        return PREF_KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(@NonNull PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        if (mPreference != null) {
 | 
			
		||||
            mPreference.setVisible(false);
 | 
			
		||||
            updateSummary();
 | 
			
		||||
            mPreference.setOnPreferenceClickListener(
 | 
			
		||||
                    preference -> {
 | 
			
		||||
                        if (mFragment == null) {
 | 
			
		||||
                            Log.w(TAG, "Dialog fail to show due to null host.");
 | 
			
		||||
                            return true;
 | 
			
		||||
                        }
 | 
			
		||||
                        updateDeviceItemsInSharingSession();
 | 
			
		||||
                        if (mDeviceItemsInSharingSession.size() >= 1) {
 | 
			
		||||
                            CallsAndAlarmsDialogFragment.show(
 | 
			
		||||
                                    mFragment,
 | 
			
		||||
                                    mDeviceItemsInSharingSession,
 | 
			
		||||
                                    (AudioSharingDeviceItem item) -> {
 | 
			
		||||
                                        if (!mGroupedConnectedDevices.containsKey(
 | 
			
		||||
                                                item.getGroupId())) {
 | 
			
		||||
                                            return;
 | 
			
		||||
                                        }
 | 
			
		||||
                                        List<CachedBluetoothDevice> devices =
 | 
			
		||||
                                                mGroupedConnectedDevices.get(item.getGroupId());
 | 
			
		||||
                                        @Nullable
 | 
			
		||||
                                        CachedBluetoothDevice lead =
 | 
			
		||||
                                                AudioSharingUtils.getLeadDevice(devices);
 | 
			
		||||
                                        if (lead != null) {
 | 
			
		||||
                                            Log.d(
 | 
			
		||||
                                                    TAG,
 | 
			
		||||
                                                    "Set fallback active device: "
 | 
			
		||||
                                                            + lead.getDevice()
 | 
			
		||||
                                                                    .getAnonymizedAddress());
 | 
			
		||||
                                            lead.setActive();
 | 
			
		||||
                                        } else {
 | 
			
		||||
                                            Log.w(
 | 
			
		||||
                                                    TAG,
 | 
			
		||||
                                                    "Fail to set fallback active device: no lead"
 | 
			
		||||
                                                            + " device");
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                        }
 | 
			
		||||
                        return true;
 | 
			
		||||
                    });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        super.onStart(owner);
 | 
			
		||||
        registerCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        super.onStop(owner);
 | 
			
		||||
        unregisterCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onProfileConnectionStateChanged(
 | 
			
		||||
            @NonNull CachedBluetoothDevice cachedDevice,
 | 
			
		||||
            @ConnectionState int state,
 | 
			
		||||
            int bluetoothProfile) {
 | 
			
		||||
        if (state == BluetoothAdapter.STATE_DISCONNECTED
 | 
			
		||||
                && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
 | 
			
		||||
            Log.d(TAG, "updatePreference, LE_AUDIO_BROADCAST_ASSISTANT is disconnected.");
 | 
			
		||||
            // The fallback active device could be updated if the previous fallback device is
 | 
			
		||||
            // disconnected.
 | 
			
		||||
            updateSummary();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize the controller.
 | 
			
		||||
     *
 | 
			
		||||
     * @param fragment The fragment to host the {@link CallsAndAlarmsDialogFragment} dialog.
 | 
			
		||||
     */
 | 
			
		||||
    public void init(DashboardFragment fragment) {
 | 
			
		||||
        this.mFragment = fragment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    ContentObserver getSettingsObserver() {
 | 
			
		||||
        return mSettingsObserver;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Test only: set callback registration status in tests. */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public void setCallbacksRegistered(boolean registered) {
 | 
			
		||||
        mCallbacksRegistered.set(registered);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void registerCallbacks() {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip registerCallbacks(). Feature is not available.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mEventManager == null || mContentResolver == null || mAssistant == null) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "Skip registerCallbacks(). Init is not ready: eventManager = "
 | 
			
		||||
                            + (mEventManager == null)
 | 
			
		||||
                            + ", contentResolver"
 | 
			
		||||
                            + (mContentResolver == null));
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!mCallbacksRegistered.get()) {
 | 
			
		||||
            Log.d(TAG, "registerCallbacks()");
 | 
			
		||||
            mEventManager.registerCallback(this);
 | 
			
		||||
            mContentResolver.registerContentObserver(
 | 
			
		||||
                    Settings.Secure.getUriFor(SETTINGS_KEY_FALLBACK_DEVICE_GROUP_ID),
 | 
			
		||||
                    false,
 | 
			
		||||
                    mSettingsObserver);
 | 
			
		||||
            mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
 | 
			
		||||
            mCallbacksRegistered.set(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void unregisterCallbacks() {
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.d(TAG, "Skip unregisterCallbacks(). Feature is not available.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mEventManager == null || mContentResolver == null || mAssistant == null) {
 | 
			
		||||
            Log.d(TAG, "Skip unregisterCallbacks(). Init is not ready.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mCallbacksRegistered.get()) {
 | 
			
		||||
            Log.d(TAG, "unregisterCallbacks()");
 | 
			
		||||
            mEventManager.unregisterCallback(this);
 | 
			
		||||
            mContentResolver.unregisterContentObserver(mSettingsObserver);
 | 
			
		||||
            mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
 | 
			
		||||
            mCallbacksRegistered.set(false);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update the preference summary: current headset for call audio.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>The summary should be updated when:
 | 
			
		||||
     *
 | 
			
		||||
     * <p>1. displayPreference.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>2. ContentObserver#onChange: the fallback device value in SettingsProvider is changed.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>3. onProfileConnectionStateChanged: the assistant profile of fallback device disconnected.
 | 
			
		||||
     * When the last headset in audio sharing disconnected, both Settings and bluetooth framework
 | 
			
		||||
     * won't set the SettingsProvider, so no ContentObserver#onChange.
 | 
			
		||||
     *
 | 
			
		||||
     * <p>4. onReceiveStateChanged: new headset join the audio sharing. If the headset has already
 | 
			
		||||
     * been set as fallback device in SettingsProvider by bluetooth framework when the broadcast is
 | 
			
		||||
     * started, Settings won't set the SettingsProvider again when the headset join the audio
 | 
			
		||||
     * sharing, so there won't be ContentObserver#onChange. We need listen to onReceiveStateChanged
 | 
			
		||||
     * to handle this scenario.
 | 
			
		||||
     */
 | 
			
		||||
    private void updateSummary() {
 | 
			
		||||
        updateDeviceItemsInSharingSession();
 | 
			
		||||
        int fallbackActiveGroupId = AudioSharingUtils.getFallbackActiveGroupId(mContext);
 | 
			
		||||
        if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
 | 
			
		||||
            for (AudioSharingDeviceItem item : mDeviceItemsInSharingSession) {
 | 
			
		||||
                if (item.getGroupId() == fallbackActiveGroupId) {
 | 
			
		||||
                    Log.d(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "updatePreference: set summary tp fallback group "
 | 
			
		||||
                                    + fallbackActiveGroupId);
 | 
			
		||||
                    AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                            mContext,
 | 
			
		||||
                            () -> {
 | 
			
		||||
                                if (mPreference != null) {
 | 
			
		||||
                                    mPreference.setSummary(
 | 
			
		||||
                                            mContext.getString(
 | 
			
		||||
                                                    R.string.audio_sharing_call_audio_description,
 | 
			
		||||
                                                    item.getName()));
 | 
			
		||||
                                }
 | 
			
		||||
                            });
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(TAG, "updatePreference: set empty summary");
 | 
			
		||||
        AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                mContext,
 | 
			
		||||
                () -> {
 | 
			
		||||
                    if (mPreference != null) {
 | 
			
		||||
                        mPreference.setSummary("");
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateDeviceItemsInSharingSession() {
 | 
			
		||||
        mGroupedConnectedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(mBtManager);
 | 
			
		||||
        mDeviceItemsInSharingSession =
 | 
			
		||||
                AudioSharingUtils.buildOrderedConnectedLeadAudioSharingDeviceItem(
 | 
			
		||||
                        mBtManager, mGroupedConnectedDevices, /* filterByInSharing= */ true);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,144 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.content.BroadcastReceiver;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.content.IntentFilter;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
 | 
			
		||||
public class StreamSettingsCategoryController extends BasePreferenceController
 | 
			
		||||
        implements DefaultLifecycleObserver, LocalBluetoothProfileManager.ServiceListener {
 | 
			
		||||
    private static final String TAG = "StreamSettingsCategoryController";
 | 
			
		||||
    private final BluetoothAdapter mBluetoothAdapter;
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mBtManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothProfileManager mProfileManager;
 | 
			
		||||
    @Nullable private Preference mPreference;
 | 
			
		||||
    @VisibleForTesting final IntentFilter mIntentFilter;
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    BroadcastReceiver mReceiver =
 | 
			
		||||
            new BroadcastReceiver() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onReceive(Context context, Intent intent) {
 | 
			
		||||
                    if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) return;
 | 
			
		||||
                    updateVisibility();
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    public StreamSettingsCategoryController(Context context, String key) {
 | 
			
		||||
        super(context, key);
 | 
			
		||||
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
 | 
			
		||||
        mBtManager = Utils.getLocalBtManager(context);
 | 
			
		||||
        mProfileManager = mBtManager == null ? null : mBtManager.getProfileManager();
 | 
			
		||||
        mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) return;
 | 
			
		||||
        mContext.registerReceiver(mReceiver, mIntentFilter, Context.RECEIVER_EXPORTED_UNAUDITED);
 | 
			
		||||
        if (!isProfileReady() && mProfileManager != null) {
 | 
			
		||||
            mProfileManager.addServiceListener(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (!isAvailable()) return;
 | 
			
		||||
        mContext.unregisterReceiver(mReceiver);
 | 
			
		||||
        if (mProfileManager != null) {
 | 
			
		||||
            mProfileManager.removeServiceListener(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        mPreference = screen.findPreference(getPreferenceKey());
 | 
			
		||||
        updateVisibility();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onServiceConnected() {
 | 
			
		||||
        if (isAvailable() && isProfileReady()) {
 | 
			
		||||
            updateVisibility();
 | 
			
		||||
            if (mProfileManager != null) {
 | 
			
		||||
                mProfileManager.removeServiceListener(this);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onServiceDisconnected() {
 | 
			
		||||
        // Do nothing
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateVisibility() {
 | 
			
		||||
        if (mPreference == null) {
 | 
			
		||||
            Log.w(TAG, "Skip updateVisibility, null preference");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!isAvailable()) {
 | 
			
		||||
            Log.w(TAG, "Skip updateVisibility, unavailable preference");
 | 
			
		||||
            AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                    mContext,
 | 
			
		||||
                    () -> { // Check nullability to pass NullAway check
 | 
			
		||||
                        if (mPreference != null) {
 | 
			
		||||
                            mPreference.setVisible(false);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        boolean visible = isBluetoothOn() && isProfileReady();
 | 
			
		||||
        AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                mContext,
 | 
			
		||||
                () -> { // Check nullability to pass NullAway check
 | 
			
		||||
                    if (mPreference != null) {
 | 
			
		||||
                        mPreference.setVisible(visible);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isBluetoothOn() {
 | 
			
		||||
        return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean isProfileReady() {
 | 
			
		||||
        return AudioSharingUtils.isAudioSharingProfileReady(mProfileManager);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
 | 
			
		||||
class AddSourceBadCodeState extends SyncedState {
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    static final int AUDIO_STREAM_ADD_SOURCE_BAD_CODE_STATE_SUMMARY =
 | 
			
		||||
            R.string.audio_streams_add_source_bad_code_state_summary;
 | 
			
		||||
 | 
			
		||||
    @Nullable private static AddSourceBadCodeState sInstance = null;
 | 
			
		||||
 | 
			
		||||
    AddSourceBadCodeState() {}
 | 
			
		||||
 | 
			
		||||
    static AddSourceBadCodeState getInstance() {
 | 
			
		||||
        if (sInstance == null) {
 | 
			
		||||
            sInstance = new AddSourceBadCodeState();
 | 
			
		||||
        }
 | 
			
		||||
        return sInstance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    int getSummary() {
 | 
			
		||||
        return AUDIO_STREAM_ADD_SOURCE_BAD_CODE_STATE_SUMMARY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
 | 
			
		||||
        return AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_BAD_CODE;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
 | 
			
		||||
class AddSourceFailedState extends SyncedState {
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    static final int AUDIO_STREAM_ADD_SOURCE_FAILED_STATE_SUMMARY =
 | 
			
		||||
            R.string.audio_streams_add_source_failed_state_summary;
 | 
			
		||||
 | 
			
		||||
    @Nullable private static AddSourceFailedState sInstance = null;
 | 
			
		||||
 | 
			
		||||
    AddSourceFailedState() {}
 | 
			
		||||
 | 
			
		||||
    static AddSourceFailedState getInstance() {
 | 
			
		||||
        if (sInstance == null) {
 | 
			
		||||
            sInstance = new AddSourceFailedState();
 | 
			
		||||
        }
 | 
			
		||||
        return sInstance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    int getSummary() {
 | 
			
		||||
        return AUDIO_STREAM_ADD_SOURCE_FAILED_STATE_SUMMARY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
 | 
			
		||||
        return AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,106 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.app.AlertDialog;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
class AddSourceWaitForResponseState extends AudioStreamStateHandler {
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    static final int AUDIO_STREAM_ADD_SOURCE_WAIT_FOR_RESPONSE_STATE_SUMMARY =
 | 
			
		||||
            R.string.audio_streams_add_source_wait_for_response_summary;
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting static final int ADD_SOURCE_WAIT_FOR_RESPONSE_TIMEOUT_MILLIS = 20000;
 | 
			
		||||
 | 
			
		||||
    @Nullable private static AddSourceWaitForResponseState sInstance = null;
 | 
			
		||||
 | 
			
		||||
    private AddSourceWaitForResponseState() {}
 | 
			
		||||
 | 
			
		||||
    static AddSourceWaitForResponseState getInstance() {
 | 
			
		||||
        if (sInstance == null) {
 | 
			
		||||
            sInstance = new AddSourceWaitForResponseState();
 | 
			
		||||
        }
 | 
			
		||||
        return sInstance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    void performAction(
 | 
			
		||||
            AudioStreamPreference preference,
 | 
			
		||||
            AudioStreamsProgressCategoryController controller,
 | 
			
		||||
            AudioStreamsHelper helper) {
 | 
			
		||||
        mHandler.removeCallbacksAndMessages(preference);
 | 
			
		||||
        var metadata = preference.getAudioStreamMetadata();
 | 
			
		||||
        if (metadata != null) {
 | 
			
		||||
            helper.addSource(metadata);
 | 
			
		||||
            // Cache the metadata that used for add source, if source is added successfully, we
 | 
			
		||||
            // will save it persistently.
 | 
			
		||||
            mAudioStreamsRepository.cacheMetadata(metadata);
 | 
			
		||||
 | 
			
		||||
            // It's possible that onSourceLost() is not notified even if the source is no longer
 | 
			
		||||
            // valid. When calling addSource() for a source that's already lost, no callback
 | 
			
		||||
            // will be sent back. So we remove the preference and pop up a dialog if it's state
 | 
			
		||||
            // has not been changed after waiting for a certain time.
 | 
			
		||||
            mHandler.postDelayed(
 | 
			
		||||
                    () -> {
 | 
			
		||||
                        if (preference.isShown()
 | 
			
		||||
                                && preference.getAudioStreamState() == getStateEnum()) {
 | 
			
		||||
                            controller.handleSourceFailedToConnect(
 | 
			
		||||
                                    preference.getAudioStreamBroadcastId());
 | 
			
		||||
                            ThreadUtils.postOnMainThread(
 | 
			
		||||
                                    () -> {
 | 
			
		||||
                                        if (controller.getFragment() != null) {
 | 
			
		||||
                                            AudioStreamsDialogFragment.show(
 | 
			
		||||
                                                    controller.getFragment(),
 | 
			
		||||
                                                    getBroadcastUnavailableNoRetryDialog(
 | 
			
		||||
                                                            preference.getContext(),
 | 
			
		||||
                                                            AudioStreamsHelper.getBroadcastName(
 | 
			
		||||
                                                                    metadata)));
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    preference,
 | 
			
		||||
                    ADD_SOURCE_WAIT_FOR_RESPONSE_TIMEOUT_MILLIS);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    int getSummary() {
 | 
			
		||||
        return AUDIO_STREAM_ADD_SOURCE_WAIT_FOR_RESPONSE_STATE_SUMMARY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
 | 
			
		||||
        return AudioStreamsProgressCategoryController.AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private AudioStreamsDialogFragment.DialogBuilder getBroadcastUnavailableNoRetryDialog(
 | 
			
		||||
            Context context, String broadcastName) {
 | 
			
		||||
        return new AudioStreamsDialogFragment.DialogBuilder(context)
 | 
			
		||||
                .setTitle(context.getString(R.string.audio_streams_dialog_stream_is_not_available))
 | 
			
		||||
                .setSubTitle1(broadcastName)
 | 
			
		||||
                .setSubTitle2(context.getString(R.string.audio_streams_is_not_playing))
 | 
			
		||||
                .setRightButtonText(context.getString(R.string.audio_streams_dialog_close))
 | 
			
		||||
                .setRightButtonOnClickListener(AlertDialog::dismiss);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,197 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastAssistant;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
import com.android.settingslib.widget.ActionButtonsPreference;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamButtonController extends BasePreferenceController
 | 
			
		||||
        implements DefaultLifecycleObserver {
 | 
			
		||||
    private static final String TAG = "AudioStreamButtonController";
 | 
			
		||||
    private static final String KEY = "audio_stream_button";
 | 
			
		||||
    private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
 | 
			
		||||
            new AudioStreamsBroadcastAssistantCallback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
                    super.onSourceRemoved(sink, sourceId, reason);
 | 
			
		||||
                    updateButton();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
                    super.onSourceRemoveFailed(sink, sourceId, reason);
 | 
			
		||||
                    updateButton();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onReceiveStateChanged(
 | 
			
		||||
                        BluetoothDevice sink,
 | 
			
		||||
                        int sourceId,
 | 
			
		||||
                        BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
                    super.onReceiveStateChanged(sink, sourceId, state);
 | 
			
		||||
                    if (AudioStreamsHelper.isConnected(state)) {
 | 
			
		||||
                        updateButton();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceAddFailed(
 | 
			
		||||
                        BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
 | 
			
		||||
                    super.onSourceAddFailed(sink, source, reason);
 | 
			
		||||
                    updateButton();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceLost(int broadcastId) {
 | 
			
		||||
                    super.onSourceLost(broadcastId);
 | 
			
		||||
                    updateButton();
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    private final AudioStreamsRepository mAudioStreamsRepository =
 | 
			
		||||
            AudioStreamsRepository.getInstance();
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    private final AudioStreamsHelper mAudioStreamsHelper;
 | 
			
		||||
    private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
 | 
			
		||||
    private @Nullable ActionButtonsPreference mPreference;
 | 
			
		||||
    private int mBroadcastId = -1;
 | 
			
		||||
 | 
			
		||||
    public AudioStreamButtonController(Context context, String preferenceKey) {
 | 
			
		||||
        super(context, preferenceKey);
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
        mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
 | 
			
		||||
        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (mLeBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (mLeBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        mPreference = screen.findPreference(getPreferenceKey());
 | 
			
		||||
        updateButton();
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateButton() {
 | 
			
		||||
        if (mPreference != null) {
 | 
			
		||||
            if (mAudioStreamsHelper.getAllConnectedSources().stream()
 | 
			
		||||
                    .map(BluetoothLeBroadcastReceiveState::getBroadcastId)
 | 
			
		||||
                    .anyMatch(connectedBroadcastId -> connectedBroadcastId == mBroadcastId)) {
 | 
			
		||||
                ThreadUtils.postOnMainThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            if (mPreference != null) {
 | 
			
		||||
                                mPreference.setButton1Enabled(true);
 | 
			
		||||
                                mPreference
 | 
			
		||||
                                        .setButton1Text(R.string.audio_streams_disconnect)
 | 
			
		||||
                                        .setButton1Icon(
 | 
			
		||||
                                                com.android.settings.R.drawable.ic_settings_close)
 | 
			
		||||
                                        .setButton1OnClickListener(
 | 
			
		||||
                                                unused -> {
 | 
			
		||||
                                                    if (mPreference != null) {
 | 
			
		||||
                                                        mPreference.setButton1Enabled(false);
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                    mAudioStreamsHelper.removeSource(mBroadcastId);
 | 
			
		||||
                                                });
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
            } else {
 | 
			
		||||
                View.OnClickListener clickToRejoin =
 | 
			
		||||
                        unused ->
 | 
			
		||||
                                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                                        () -> {
 | 
			
		||||
                                            var metadata =
 | 
			
		||||
                                                    mAudioStreamsRepository.getSavedMetadata(
 | 
			
		||||
                                                            mContext, mBroadcastId);
 | 
			
		||||
                                            if (metadata != null) {
 | 
			
		||||
                                                mAudioStreamsHelper.addSource(metadata);
 | 
			
		||||
                                                ThreadUtils.postOnMainThread(
 | 
			
		||||
                                                        () -> {
 | 
			
		||||
                                                            if (mPreference != null) {
 | 
			
		||||
                                                                mPreference.setButton1Enabled(
 | 
			
		||||
                                                                        false);
 | 
			
		||||
                                                            }
 | 
			
		||||
                                                        });
 | 
			
		||||
                                            }
 | 
			
		||||
                                        });
 | 
			
		||||
                ThreadUtils.postOnMainThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            if (mPreference != null) {
 | 
			
		||||
                                mPreference.setButton1Enabled(true);
 | 
			
		||||
                                mPreference
 | 
			
		||||
                                        .setButton1Text(R.string.audio_streams_connect)
 | 
			
		||||
                                        .setButton1Icon(com.android.settings.R.drawable.ic_add_24dp)
 | 
			
		||||
                                        .setButton1OnClickListener(clickToRejoin);
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Log.w(TAG, "updateButton(): preference is null!");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AVAILABLE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getPreferenceKey() {
 | 
			
		||||
        return KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Initialize with broadcast id */
 | 
			
		||||
    void init(int broadcastId) {
 | 
			
		||||
        mBroadcastId = broadcastId;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,195 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.app.Dialog;
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.provider.Settings;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeFragment;
 | 
			
		||||
import com.android.settings.core.SubSettingLauncher;
 | 
			
		||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
 | 
			
		||||
import com.google.common.base.Strings;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamConfirmDialog extends InstrumentedDialogFragment {
 | 
			
		||||
    public static final String KEY_BROADCAST_METADATA = "key_broadcast_metadata";
 | 
			
		||||
    private static final String TAG = "AudioStreamConfirmDialog";
 | 
			
		||||
    private static final int DEFAULT_DEVICE_NAME = R.string.audio_streams_dialog_default_device;
 | 
			
		||||
    @Nullable private LocalBluetoothManager mLocalBluetoothManager;
 | 
			
		||||
    @Nullable private LocalBluetoothProfileManager mProfileManager;
 | 
			
		||||
    @Nullable private Activity mActivity;
 | 
			
		||||
    @Nullable private String mBroadcastMetadataStr;
 | 
			
		||||
    @Nullable private BluetoothLeBroadcastMetadata mBroadcastMetadata;
 | 
			
		||||
    private boolean mIsRequestValid = false;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        setShowsDialog(true);
 | 
			
		||||
        mActivity = getActivity();
 | 
			
		||||
        if (mActivity == null) {
 | 
			
		||||
            Log.w(TAG, "onCreate() mActivity is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mLocalBluetoothManager = Utils.getLocalBluetoothManager(mActivity);
 | 
			
		||||
        mProfileManager =
 | 
			
		||||
                mLocalBluetoothManager == null ? null : mLocalBluetoothManager.getProfileManager();
 | 
			
		||||
        mBroadcastMetadataStr =
 | 
			
		||||
                mActivity.getIntent().getStringExtra(QrCodeScanModeFragment.KEY_BROADCAST_METADATA);
 | 
			
		||||
        if (Strings.isNullOrEmpty(mBroadcastMetadataStr)) {
 | 
			
		||||
            Log.w(TAG, "onCreate() mBroadcastMetadataStr is null or empty!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mBroadcastMetadata =
 | 
			
		||||
                BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
 | 
			
		||||
                        mBroadcastMetadataStr);
 | 
			
		||||
        if (mBroadcastMetadata == null) {
 | 
			
		||||
            Log.w(TAG, "onCreate() mBroadcastMetadata is null!");
 | 
			
		||||
        } else {
 | 
			
		||||
            mIsRequestValid = true;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Dialog onCreateDialog(Bundle savedInstanceState) {
 | 
			
		||||
        if (AudioSharingUtils.isAudioSharingProfileReady(mProfileManager)) {
 | 
			
		||||
            CachedBluetoothDevice connectedLeDevice =
 | 
			
		||||
                    AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
 | 
			
		||||
                                    mLocalBluetoothManager)
 | 
			
		||||
                            .orElse(null);
 | 
			
		||||
            if (connectedLeDevice == null) {
 | 
			
		||||
                return getNoLeDeviceDialog();
 | 
			
		||||
            }
 | 
			
		||||
            String deviceName = connectedLeDevice.getName();
 | 
			
		||||
            return mIsRequestValid ? getConfirmDialog(deviceName) : getErrorDialog(deviceName);
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(TAG, "onCreateDialog() : profile not ready!");
 | 
			
		||||
        String defaultDeviceName =
 | 
			
		||||
                mActivity != null ? mActivity.getString(DEFAULT_DEVICE_NAME) : "";
 | 
			
		||||
        return mIsRequestValid
 | 
			
		||||
                ? getConfirmDialog(defaultDeviceName)
 | 
			
		||||
                : getErrorDialog(defaultDeviceName);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        // TODO(chelseahao): update metrics id
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Dialog getConfirmDialog(String name) {
 | 
			
		||||
        return new AudioStreamsDialogFragment.DialogBuilder(getActivity())
 | 
			
		||||
                .setTitle(getString(R.string.audio_streams_dialog_listen_to_audio_stream))
 | 
			
		||||
                .setSubTitle1(
 | 
			
		||||
                        mBroadcastMetadata != null
 | 
			
		||||
                                ? AudioStreamsHelper.getBroadcastName(mBroadcastMetadata)
 | 
			
		||||
                                : "")
 | 
			
		||||
                .setSubTitle2(getString(R.string.audio_streams_dialog_control_volume, name))
 | 
			
		||||
                .setLeftButtonText(getString(com.android.settings.R.string.cancel))
 | 
			
		||||
                .setLeftButtonOnClickListener(
 | 
			
		||||
                        unused -> {
 | 
			
		||||
                            dismiss();
 | 
			
		||||
                            if (mActivity != null) {
 | 
			
		||||
                                mActivity.finish();
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                .setRightButtonText(getString(R.string.audio_streams_dialog_listen))
 | 
			
		||||
                .setRightButtonOnClickListener(
 | 
			
		||||
                        unused -> {
 | 
			
		||||
                            launchAudioStreamsActivity();
 | 
			
		||||
                            dismiss();
 | 
			
		||||
                            if (mActivity != null) {
 | 
			
		||||
                                mActivity.finish();
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Dialog getErrorDialog(String name) {
 | 
			
		||||
        return new AudioStreamsDialogFragment.DialogBuilder(getActivity())
 | 
			
		||||
                .setTitle(getString(R.string.audio_streams_dialog_cannot_listen))
 | 
			
		||||
                .setSubTitle2(getString(R.string.audio_streams_dialog_cannot_play, name))
 | 
			
		||||
                .setRightButtonText(getString(R.string.audio_streams_dialog_close))
 | 
			
		||||
                .setRightButtonOnClickListener(
 | 
			
		||||
                        unused -> {
 | 
			
		||||
                            dismiss();
 | 
			
		||||
                            if (mActivity != null) {
 | 
			
		||||
                                mActivity.finish();
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Dialog getNoLeDeviceDialog() {
 | 
			
		||||
        return new AudioStreamsDialogFragment.DialogBuilder(getActivity())
 | 
			
		||||
                .setTitle(getString(R.string.audio_streams_dialog_no_le_device_title))
 | 
			
		||||
                .setSubTitle2(getString(R.string.audio_streams_dialog_no_le_device_subtitle))
 | 
			
		||||
                .setLeftButtonText(getString(R.string.audio_streams_dialog_close))
 | 
			
		||||
                .setLeftButtonOnClickListener(
 | 
			
		||||
                        unused -> {
 | 
			
		||||
                            dismiss();
 | 
			
		||||
                            if (mActivity != null) {
 | 
			
		||||
                                mActivity.finish();
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                .setRightButtonText(getString(R.string.audio_streams_dialog_no_le_device_button))
 | 
			
		||||
                .setRightButtonOnClickListener(
 | 
			
		||||
                        dialog -> {
 | 
			
		||||
                            if (mActivity != null) {
 | 
			
		||||
                                mActivity.startActivity(
 | 
			
		||||
                                        new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
 | 
			
		||||
                                                .setPackage(mActivity.getPackageName()));
 | 
			
		||||
                            }
 | 
			
		||||
                            dismiss();
 | 
			
		||||
                            if (mActivity != null) {
 | 
			
		||||
                                mActivity.finish();
 | 
			
		||||
                            }
 | 
			
		||||
                        })
 | 
			
		||||
                .build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void launchAudioStreamsActivity() {
 | 
			
		||||
        Bundle bundle = new Bundle();
 | 
			
		||||
        bundle.putString(KEY_BROADCAST_METADATA, mBroadcastMetadataStr);
 | 
			
		||||
        if (mActivity != null) {
 | 
			
		||||
            new SubSettingLauncher(getActivity())
 | 
			
		||||
                    .setTitleText(getString(R.string.audio_streams_activity_title))
 | 
			
		||||
                    .setDestination(AudioStreamsDashboardFragment.class.getName())
 | 
			
		||||
                    .setArguments(bundle)
 | 
			
		||||
                    .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
 | 
			
		||||
                    .launch();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.SettingsActivity;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamConfirmDialogActivity extends SettingsActivity {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedState) {
 | 
			
		||||
        super.onCreate(savedState);
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            finish();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected boolean isValidFragment(String fragmentName) {
 | 
			
		||||
        return AudioStreamConfirmDialog.class.getName().equals(fragmentName);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,59 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamDetailsFragment extends DashboardFragment {
 | 
			
		||||
    static final String BROADCAST_NAME_ARG = "broadcast_name";
 | 
			
		||||
    static final String BROADCAST_ID_ARG = "broadcast_id";
 | 
			
		||||
    private static final String TAG = "AudioStreamDetailsFragment";
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onAttach(Context context) {
 | 
			
		||||
        super.onAttach(context);
 | 
			
		||||
        Bundle arguments = getArguments();
 | 
			
		||||
        if (arguments != null) {
 | 
			
		||||
            use(AudioStreamHeaderController.class)
 | 
			
		||||
                    .init(
 | 
			
		||||
                            this,
 | 
			
		||||
                            arguments.getString(BROADCAST_NAME_ARG),
 | 
			
		||||
                            arguments.getInt(BROADCAST_ID_ARG));
 | 
			
		||||
            use(AudioStreamButtonController.class).init(arguments.getInt(BROADCAST_ID_ARG));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        // TODO(chelseahao): update metrics id
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected int getPreferenceScreenResId() {
 | 
			
		||||
        return R.xml.bluetooth_le_audio_stream_details_fragment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String getLogTag() {
 | 
			
		||||
        return TAG;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,182 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastAssistant;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settings.widget.EntityHeaderController;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
import com.android.settingslib.widget.LayoutPreference;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamHeaderController extends BasePreferenceController
 | 
			
		||||
        implements DefaultLifecycleObserver {
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    static final int AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY =
 | 
			
		||||
            R.string.audio_streams_listening_now;
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting static final String AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY = "";
 | 
			
		||||
    private static final String TAG = "AudioStreamHeaderController";
 | 
			
		||||
    private static final String KEY = "audio_stream_header";
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    private final AudioStreamsHelper mAudioStreamsHelper;
 | 
			
		||||
    @Nullable private final LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
 | 
			
		||||
    private final BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback =
 | 
			
		||||
            new AudioStreamsBroadcastAssistantCallback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
                    super.onSourceRemoved(sink, sourceId, reason);
 | 
			
		||||
                    updateSummary();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceLost(int broadcastId) {
 | 
			
		||||
                    super.onSourceLost(broadcastId);
 | 
			
		||||
                    updateSummary();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onReceiveStateChanged(
 | 
			
		||||
                        BluetoothDevice sink,
 | 
			
		||||
                        int sourceId,
 | 
			
		||||
                        BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
                    super.onReceiveStateChanged(sink, sourceId, state);
 | 
			
		||||
                    if (AudioStreamsHelper.isConnected(state)) {
 | 
			
		||||
                        updateSummary();
 | 
			
		||||
                        mAudioStreamsHelper.startMediaService(
 | 
			
		||||
                                mContext, mBroadcastId, mBroadcastName);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    private @Nullable EntityHeaderController mHeaderController;
 | 
			
		||||
    private @Nullable DashboardFragment mFragment;
 | 
			
		||||
    private String mBroadcastName = "";
 | 
			
		||||
    private int mBroadcastId = -1;
 | 
			
		||||
 | 
			
		||||
    public AudioStreamHeaderController(Context context, String preferenceKey) {
 | 
			
		||||
        super(context, preferenceKey);
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
        mAudioStreamsHelper = new AudioStreamsHelper(Utils.getLocalBtManager(context));
 | 
			
		||||
        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (mLeBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "onStart(): LeBroadcastAssistant is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (mLeBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "onStop(): LeBroadcastAssistant is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        LayoutPreference headerPreference = screen.findPreference(KEY);
 | 
			
		||||
        if (headerPreference != null && mFragment != null) {
 | 
			
		||||
            mHeaderController =
 | 
			
		||||
                    EntityHeaderController.newInstance(
 | 
			
		||||
                            mFragment.getActivity(),
 | 
			
		||||
                            mFragment,
 | 
			
		||||
                            headerPreference.findViewById(com.android.settings.R.id.entity_header));
 | 
			
		||||
            if (mBroadcastName != null) {
 | 
			
		||||
                mHeaderController.setLabel(mBroadcastName);
 | 
			
		||||
            }
 | 
			
		||||
            mHeaderController.setIcon(
 | 
			
		||||
                    screen.getContext()
 | 
			
		||||
                            .getDrawable(
 | 
			
		||||
                                    com.android.settingslib.R.drawable.ic_bt_le_audio_sharing));
 | 
			
		||||
            screen.addPreference(headerPreference);
 | 
			
		||||
            updateSummary();
 | 
			
		||||
        }
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateSummary() {
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            var latestSummary =
 | 
			
		||||
                                    mAudioStreamsHelper.getAllConnectedSources().stream()
 | 
			
		||||
                                                    .map(
 | 
			
		||||
                                                            BluetoothLeBroadcastReceiveState
 | 
			
		||||
                                                                    ::getBroadcastId)
 | 
			
		||||
                                                    .anyMatch(
 | 
			
		||||
                                                            connectedBroadcastId ->
 | 
			
		||||
                                                                    connectedBroadcastId
 | 
			
		||||
                                                                            == mBroadcastId)
 | 
			
		||||
                                            ? mContext.getString(
 | 
			
		||||
                                                    AUDIO_STREAM_HEADER_LISTENING_NOW_SUMMARY)
 | 
			
		||||
                                            : AUDIO_STREAM_HEADER_NOT_LISTENING_SUMMARY;
 | 
			
		||||
                            ThreadUtils.postOnMainThread(
 | 
			
		||||
                                    () -> {
 | 
			
		||||
                                        if (mHeaderController != null) {
 | 
			
		||||
                                            mHeaderController.setSummary(latestSummary);
 | 
			
		||||
                                            mHeaderController.done(true);
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AVAILABLE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getPreferenceKey() {
 | 
			
		||||
        return KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Initialize with {@link AudioStreamDetailsFragment} and broadcast name and id */
 | 
			
		||||
    void init(
 | 
			
		||||
            AudioStreamDetailsFragment audioStreamDetailsFragment,
 | 
			
		||||
            String broadcastName,
 | 
			
		||||
            int broadcastId) {
 | 
			
		||||
        mFragment = audioStreamDetailsFragment;
 | 
			
		||||
        mBroadcastName = broadcastName;
 | 
			
		||||
        mBroadcastId = broadcastId;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,378 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.app.Notification;
 | 
			
		||||
import android.app.NotificationChannel;
 | 
			
		||||
import android.app.NotificationManager;
 | 
			
		||||
import android.app.Service;
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.bluetooth.BluetoothVolumeControl;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.media.MediaMetadata;
 | 
			
		||||
import android.media.session.MediaSession;
 | 
			
		||||
import android.media.session.PlaybackState;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.os.IBinder;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.IntRange;
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.VolumeControlProfile;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.concurrent.ExecutorService;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamMediaService extends Service {
 | 
			
		||||
    static final String BROADCAST_ID = "audio_stream_media_service_broadcast_id";
 | 
			
		||||
    static final String BROADCAST_TITLE = "audio_stream_media_service_broadcast_title";
 | 
			
		||||
    static final String DEVICES = "audio_stream_media_service_devices";
 | 
			
		||||
    private static final String TAG = "AudioStreamMediaService";
 | 
			
		||||
    private static final int NOTIFICATION_ID = 1;
 | 
			
		||||
    private static final int BROADCAST_CONTENT_TEXT = R.string.audio_streams_listening_now;
 | 
			
		||||
    private static final String LEAVE_BROADCAST_ACTION = "leave_broadcast_action";
 | 
			
		||||
    private static final String LEAVE_BROADCAST_TEXT = "Leave Broadcast";
 | 
			
		||||
    private static final String CHANNEL_ID = "bluetooth_notification_channel";
 | 
			
		||||
    private static final int STATIC_PLAYBACK_DURATION = 100;
 | 
			
		||||
    private static final int STATIC_PLAYBACK_POSITION = 30;
 | 
			
		||||
    private static final int ZERO_PLAYBACK_SPEED = 0;
 | 
			
		||||
    private final AudioStreamsBroadcastAssistantCallback mBroadcastAssistantCallback =
 | 
			
		||||
            new AudioStreamsBroadcastAssistantCallback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceLost(int broadcastId) {
 | 
			
		||||
                    super.onSourceLost(broadcastId);
 | 
			
		||||
                    if (broadcastId == mBroadcastId) {
 | 
			
		||||
                        stopSelf();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
                    super.onSourceRemoved(sink, sourceId, reason);
 | 
			
		||||
                    if (mAudioStreamsHelper != null
 | 
			
		||||
                            && mAudioStreamsHelper.getAllConnectedSources().stream()
 | 
			
		||||
                                    .map(BluetoothLeBroadcastReceiveState::getBroadcastId)
 | 
			
		||||
                                    .noneMatch(id -> id == mBroadcastId)) {
 | 
			
		||||
                        stopSelf();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    private final BluetoothCallback mBluetoothCallback =
 | 
			
		||||
            new BluetoothCallback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onBluetoothStateChanged(int bluetoothState) {
 | 
			
		||||
                    if (BluetoothAdapter.STATE_OFF == bluetoothState) {
 | 
			
		||||
                        stopSelf();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onProfileConnectionStateChanged(
 | 
			
		||||
                        @NonNull CachedBluetoothDevice cachedDevice,
 | 
			
		||||
                        @ConnectionState int state,
 | 
			
		||||
                        int bluetoothProfile) {
 | 
			
		||||
                    if (state == BluetoothAdapter.STATE_DISCONNECTED
 | 
			
		||||
                            && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
 | 
			
		||||
                            && mDevices != null) {
 | 
			
		||||
                        mDevices.remove(cachedDevice.getDevice());
 | 
			
		||||
                        cachedDevice
 | 
			
		||||
                                .getMemberDevice()
 | 
			
		||||
                                .forEach(
 | 
			
		||||
                                        m -> {
 | 
			
		||||
                                            // Check nullability to pass NullAway check
 | 
			
		||||
                                            if (mDevices != null) {
 | 
			
		||||
                                                mDevices.remove(m.getDevice());
 | 
			
		||||
                                            }
 | 
			
		||||
                                        });
 | 
			
		||||
                    }
 | 
			
		||||
                    if (mDevices == null || mDevices.isEmpty()) {
 | 
			
		||||
                        stopSelf();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    private final BluetoothVolumeControl.Callback mVolumeControlCallback =
 | 
			
		||||
            new BluetoothVolumeControl.Callback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onDeviceVolumeChanged(
 | 
			
		||||
                        @NonNull BluetoothDevice device,
 | 
			
		||||
                        @IntRange(from = -255, to = 255) int volume) {
 | 
			
		||||
                    if (mDevices == null || mDevices.isEmpty()) {
 | 
			
		||||
                        Log.w(TAG, "active device or device has source is null!");
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    if (mDevices.contains(device)) {
 | 
			
		||||
                        Log.d(
 | 
			
		||||
                                TAG,
 | 
			
		||||
                                "onDeviceVolumeChanged() bluetoothDevice : "
 | 
			
		||||
                                        + device
 | 
			
		||||
                                        + " volume: "
 | 
			
		||||
                                        + volume);
 | 
			
		||||
                        if (volume == 0) {
 | 
			
		||||
                            mIsMuted = true;
 | 
			
		||||
                        } else {
 | 
			
		||||
                            mIsMuted = false;
 | 
			
		||||
                            mLatestPositiveVolume = volume;
 | 
			
		||||
                        }
 | 
			
		||||
                        if (mLocalSession != null) {
 | 
			
		||||
                            mLocalSession.setPlaybackState(getPlaybackState());
 | 
			
		||||
                            if (mNotificationManager != null) {
 | 
			
		||||
                                mNotificationManager.notify(NOTIFICATION_ID, buildNotification());
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    private final PlaybackState.Builder mPlayStatePlayingBuilder =
 | 
			
		||||
            new PlaybackState.Builder()
 | 
			
		||||
                    .setActions(PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SEEK_TO)
 | 
			
		||||
                    .setState(
 | 
			
		||||
                            PlaybackState.STATE_PLAYING,
 | 
			
		||||
                            STATIC_PLAYBACK_POSITION,
 | 
			
		||||
                            ZERO_PLAYBACK_SPEED)
 | 
			
		||||
                    .addCustomAction(
 | 
			
		||||
                            LEAVE_BROADCAST_ACTION,
 | 
			
		||||
                            LEAVE_BROADCAST_TEXT,
 | 
			
		||||
                            com.android.settings.R.drawable.ic_clear);
 | 
			
		||||
    private final PlaybackState.Builder mPlayStatePausingBuilder =
 | 
			
		||||
            new PlaybackState.Builder()
 | 
			
		||||
                    .setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_SEEK_TO)
 | 
			
		||||
                    .setState(
 | 
			
		||||
                            PlaybackState.STATE_PAUSED,
 | 
			
		||||
                            STATIC_PLAYBACK_POSITION,
 | 
			
		||||
                            ZERO_PLAYBACK_SPEED)
 | 
			
		||||
                    .addCustomAction(
 | 
			
		||||
                            LEAVE_BROADCAST_ACTION,
 | 
			
		||||
                            LEAVE_BROADCAST_TEXT,
 | 
			
		||||
                            com.android.settings.R.drawable.ic_clear);
 | 
			
		||||
 | 
			
		||||
    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
    private int mBroadcastId;
 | 
			
		||||
    @Nullable private ArrayList<BluetoothDevice> mDevices;
 | 
			
		||||
    @Nullable private LocalBluetoothManager mLocalBtManager;
 | 
			
		||||
    @Nullable private AudioStreamsHelper mAudioStreamsHelper;
 | 
			
		||||
    @Nullable private LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
 | 
			
		||||
    @Nullable private VolumeControlProfile mVolumeControl;
 | 
			
		||||
    @Nullable private NotificationManager mNotificationManager;
 | 
			
		||||
 | 
			
		||||
    // Set 25 as default as the volume range from `VolumeControlProfile` is from 0 to 255.
 | 
			
		||||
    // If the initial volume from `onDeviceVolumeChanged` is larger than zero (not muted), we will
 | 
			
		||||
    // override this value. Otherwise, we raise the volume to 25 when the play button is clicked.
 | 
			
		||||
    private int mLatestPositiveVolume = 25;
 | 
			
		||||
    private boolean mIsMuted = false;
 | 
			
		||||
    @Nullable private MediaSession mLocalSession;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onCreate() {
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        super.onCreate();
 | 
			
		||||
        mLocalBtManager = Utils.getLocalBtManager(this);
 | 
			
		||||
        if (mLocalBtManager == null) {
 | 
			
		||||
            Log.w(TAG, "onCreate() : mLocalBtManager is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mAudioStreamsHelper = new AudioStreamsHelper(mLocalBtManager);
 | 
			
		||||
        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
 | 
			
		||||
        if (mLeBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "onCreate() : mLeBroadcastAssistant is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mNotificationManager = getSystemService(NotificationManager.class);
 | 
			
		||||
        if (mNotificationManager == null) {
 | 
			
		||||
            Log.w(TAG, "onCreate() : notificationManager is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (mNotificationManager.getNotificationChannel(CHANNEL_ID) == null) {
 | 
			
		||||
            NotificationChannel notificationChannel =
 | 
			
		||||
                    new NotificationChannel(
 | 
			
		||||
                            CHANNEL_ID,
 | 
			
		||||
                            this.getString(com.android.settings.R.string.bluetooth),
 | 
			
		||||
                            NotificationManager.IMPORTANCE_HIGH);
 | 
			
		||||
            mNotificationManager.createNotificationChannel(notificationChannel);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback);
 | 
			
		||||
 | 
			
		||||
        mVolumeControl = mLocalBtManager.getProfileManager().getVolumeControlProfile();
 | 
			
		||||
        if (mVolumeControl != null) {
 | 
			
		||||
            mVolumeControl.registerCallback(mExecutor, mVolumeControlCallback);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onDestroy() {
 | 
			
		||||
        super.onDestroy();
 | 
			
		||||
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mLocalBtManager != null) {
 | 
			
		||||
            mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback);
 | 
			
		||||
        }
 | 
			
		||||
        if (mLeBroadcastAssistant != null) {
 | 
			
		||||
            mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
 | 
			
		||||
        }
 | 
			
		||||
        if (mVolumeControl != null) {
 | 
			
		||||
            mVolumeControl.unregisterCallback(mVolumeControlCallback);
 | 
			
		||||
        }
 | 
			
		||||
        if (mLocalSession != null) {
 | 
			
		||||
            mLocalSession.release();
 | 
			
		||||
            mLocalSession = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
 | 
			
		||||
        Log.d(TAG, "onStartCommand()");
 | 
			
		||||
 | 
			
		||||
        mBroadcastId = intent != null ? intent.getIntExtra(BROADCAST_ID, -1) : -1;
 | 
			
		||||
        if (mBroadcastId == -1) {
 | 
			
		||||
            Log.w(TAG, "Invalid broadcast ID. Service will not start.");
 | 
			
		||||
            stopSelf();
 | 
			
		||||
            return START_NOT_STICKY;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (intent != null) {
 | 
			
		||||
            mDevices = intent.getParcelableArrayListExtra(DEVICES, BluetoothDevice.class);
 | 
			
		||||
        }
 | 
			
		||||
        if (mDevices == null || mDevices.isEmpty()) {
 | 
			
		||||
            Log.w(TAG, "No device. Service will not start.");
 | 
			
		||||
            stopSelf();
 | 
			
		||||
            return START_NOT_STICKY;
 | 
			
		||||
        }
 | 
			
		||||
        if (intent != null) {
 | 
			
		||||
            createLocalMediaSession(intent.getStringExtra(BROADCAST_TITLE));
 | 
			
		||||
            startForeground(NOTIFICATION_ID, buildNotification());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return START_NOT_STICKY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void createLocalMediaSession(String title) {
 | 
			
		||||
        mLocalSession = new MediaSession(this, TAG);
 | 
			
		||||
        mLocalSession.setMetadata(
 | 
			
		||||
                new MediaMetadata.Builder()
 | 
			
		||||
                        .putString(MediaMetadata.METADATA_KEY_TITLE, title)
 | 
			
		||||
                        .putLong(MediaMetadata.METADATA_KEY_DURATION, STATIC_PLAYBACK_DURATION)
 | 
			
		||||
                        .build());
 | 
			
		||||
        mLocalSession.setActive(true);
 | 
			
		||||
        mLocalSession.setPlaybackState(getPlaybackState());
 | 
			
		||||
        mLocalSession.setCallback(
 | 
			
		||||
                new MediaSession.Callback() {
 | 
			
		||||
                    public void onSeekTo(long pos) {
 | 
			
		||||
                        Log.d(TAG, "onSeekTo: " + pos);
 | 
			
		||||
                        if (mLocalSession != null) {
 | 
			
		||||
                            mLocalSession.setPlaybackState(getPlaybackState());
 | 
			
		||||
                            if (mNotificationManager != null) {
 | 
			
		||||
                                mNotificationManager.notify(NOTIFICATION_ID, buildNotification());
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onPause() {
 | 
			
		||||
                        if (mDevices == null || mDevices.isEmpty()) {
 | 
			
		||||
                            Log.w(TAG, "active device or device has source is null!");
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                        Log.d(
 | 
			
		||||
                                TAG,
 | 
			
		||||
                                "onPause() setting volume for device : "
 | 
			
		||||
                                        + mDevices.get(0)
 | 
			
		||||
                                        + " volume: "
 | 
			
		||||
                                        + 0);
 | 
			
		||||
                        if (mVolumeControl != null) {
 | 
			
		||||
                            mVolumeControl.setDeviceVolume(mDevices.get(0), 0, true);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onPlay() {
 | 
			
		||||
                        if (mDevices == null || mDevices.isEmpty()) {
 | 
			
		||||
                            Log.w(TAG, "active device or device has source is null!");
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                        Log.d(
 | 
			
		||||
                                TAG,
 | 
			
		||||
                                "onPlay() setting volume for device : "
 | 
			
		||||
                                        + mDevices.get(0)
 | 
			
		||||
                                        + " volume: "
 | 
			
		||||
                                        + mLatestPositiveVolume);
 | 
			
		||||
                        if (mVolumeControl != null) {
 | 
			
		||||
                            mVolumeControl.setDeviceVolume(
 | 
			
		||||
                                    mDevices.get(0), mLatestPositiveVolume, true);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void onCustomAction(@NonNull String action, Bundle extras) {
 | 
			
		||||
                        Log.d(TAG, "onCustomAction: " + action);
 | 
			
		||||
                        if (action.equals(LEAVE_BROADCAST_ACTION) && mAudioStreamsHelper != null) {
 | 
			
		||||
                            mAudioStreamsHelper.removeSource(mBroadcastId);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private PlaybackState getPlaybackState() {
 | 
			
		||||
        return mIsMuted ? mPlayStatePausingBuilder.build() : mPlayStatePlayingBuilder.build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Notification buildNotification() {
 | 
			
		||||
        Notification.Builder notificationBuilder =
 | 
			
		||||
                new Notification.Builder(this, CHANNEL_ID)
 | 
			
		||||
                        .setSmallIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing)
 | 
			
		||||
                        .setStyle(
 | 
			
		||||
                                new Notification.MediaStyle()
 | 
			
		||||
                                        .setMediaSession(
 | 
			
		||||
                                                mLocalSession != null
 | 
			
		||||
                                                        ? mLocalSession.getSessionToken()
 | 
			
		||||
                                                        : null))
 | 
			
		||||
                        .setContentText(this.getString(BROADCAST_CONTENT_TEXT))
 | 
			
		||||
                        .setSilent(true);
 | 
			
		||||
        return notificationBuilder.build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    @Override
 | 
			
		||||
    public IBinder onBind(Intent intent) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,201 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.AttributeSet;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.preference.PreferenceViewHolder;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settingslib.widget.TwoTargetPreference;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Custom preference class for managing audio stream preferences with an optional lock icon. Extends
 | 
			
		||||
 * {@link TwoTargetPreference}.
 | 
			
		||||
 */
 | 
			
		||||
class AudioStreamPreference extends TwoTargetPreference {
 | 
			
		||||
    private boolean mIsConnected = false;
 | 
			
		||||
    private boolean mIsEncrypted = true;
 | 
			
		||||
    @Nullable private AudioStream mAudioStream;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Update preference UI based on connection status
 | 
			
		||||
     *
 | 
			
		||||
     * @param isConnected Is this stream connected
 | 
			
		||||
     * @param summary Summary text
 | 
			
		||||
     * @param onPreferenceClickListener Click listener for the preference
 | 
			
		||||
     */
 | 
			
		||||
    void setIsConnected(
 | 
			
		||||
            boolean isConnected,
 | 
			
		||||
            String summary,
 | 
			
		||||
            @Nullable OnPreferenceClickListener onPreferenceClickListener) {
 | 
			
		||||
        if (mIsConnected == isConnected
 | 
			
		||||
                && getSummary() == summary
 | 
			
		||||
                && getOnPreferenceClickListener() == onPreferenceClickListener) {
 | 
			
		||||
            // Nothing to update.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mIsConnected = isConnected;
 | 
			
		||||
        setSummary(summary);
 | 
			
		||||
        setOnPreferenceClickListener(onPreferenceClickListener);
 | 
			
		||||
        notifyChanged();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private AudioStreamPreference(Context context, @Nullable AttributeSet attrs) {
 | 
			
		||||
        super(context, attrs);
 | 
			
		||||
        setIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setAudioStreamState(AudioStreamsProgressCategoryController.AudioStreamState state) {
 | 
			
		||||
        if (mAudioStream != null) {
 | 
			
		||||
            mAudioStream.setState(state);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setAudioStreamMetadata(BluetoothLeBroadcastMetadata metadata) {
 | 
			
		||||
        if (mAudioStream != null) {
 | 
			
		||||
            mAudioStream.setMetadata(metadata);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    int getAudioStreamBroadcastId() {
 | 
			
		||||
        return mAudioStream != null ? mAudioStream.getBroadcastId() : -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    String getAudioStreamBroadcastName() {
 | 
			
		||||
        return mAudioStream != null ? mAudioStream.getBroadcastName() : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    int getAudioStreamRssi() {
 | 
			
		||||
        return mAudioStream != null ? mAudioStream.getRssi() : -1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    BluetoothLeBroadcastMetadata getAudioStreamMetadata() {
 | 
			
		||||
        return mAudioStream != null ? mAudioStream.getMetadata() : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    AudioStreamsProgressCategoryController.AudioStreamState getAudioStreamState() {
 | 
			
		||||
        return mAudioStream != null
 | 
			
		||||
                ? mAudioStream.getState()
 | 
			
		||||
                : AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected boolean shouldHideSecondTarget() {
 | 
			
		||||
        return mIsConnected || !mIsEncrypted;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected int getSecondTargetResId() {
 | 
			
		||||
        return R.layout.preference_widget_lock;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onBindViewHolder(PreferenceViewHolder holder) {
 | 
			
		||||
        super.onBindViewHolder(holder);
 | 
			
		||||
        View divider =
 | 
			
		||||
                holder.findViewById(
 | 
			
		||||
                        com.android.settingslib.widget.preference.twotarget.R.id
 | 
			
		||||
                                .two_target_divider);
 | 
			
		||||
        if (divider != null) {
 | 
			
		||||
            divider.setVisibility(View.GONE);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static AudioStreamPreference fromMetadata(
 | 
			
		||||
            Context context, BluetoothLeBroadcastMetadata source) {
 | 
			
		||||
        AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
 | 
			
		||||
        preference.setIsEncrypted(source.isEncrypted());
 | 
			
		||||
        preference.setTitle(AudioStreamsHelper.getBroadcastName(source));
 | 
			
		||||
        preference.setAudioStream(new AudioStream(source));
 | 
			
		||||
        return preference;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static AudioStreamPreference fromReceiveState(
 | 
			
		||||
            Context context, BluetoothLeBroadcastReceiveState receiveState) {
 | 
			
		||||
        AudioStreamPreference preference = new AudioStreamPreference(context, /* attrs= */ null);
 | 
			
		||||
        preference.setTitle(AudioStreamsHelper.getBroadcastName(receiveState));
 | 
			
		||||
        preference.setAudioStream(new AudioStream(receiveState));
 | 
			
		||||
        return preference;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void setAudioStream(AudioStream audioStream) {
 | 
			
		||||
        mAudioStream = audioStream;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void setIsEncrypted(boolean isEncrypted) {
 | 
			
		||||
        mIsEncrypted = isEncrypted;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static final class AudioStream {
 | 
			
		||||
        private static final int UNAVAILABLE = -1;
 | 
			
		||||
        @Nullable private BluetoothLeBroadcastMetadata mMetadata;
 | 
			
		||||
        @Nullable private BluetoothLeBroadcastReceiveState mReceiveState;
 | 
			
		||||
        private AudioStreamsProgressCategoryController.AudioStreamState mState =
 | 
			
		||||
                AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
 | 
			
		||||
 | 
			
		||||
        private AudioStream(BluetoothLeBroadcastMetadata metadata) {
 | 
			
		||||
            mMetadata = metadata;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private AudioStream(BluetoothLeBroadcastReceiveState receiveState) {
 | 
			
		||||
            mReceiveState = receiveState;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private int getBroadcastId() {
 | 
			
		||||
            return mMetadata != null
 | 
			
		||||
                    ? mMetadata.getBroadcastId()
 | 
			
		||||
                    : mReceiveState != null ? mReceiveState.getBroadcastId() : UNAVAILABLE;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private @Nullable String getBroadcastName() {
 | 
			
		||||
            return mMetadata != null
 | 
			
		||||
                    ? AudioStreamsHelper.getBroadcastName(mMetadata)
 | 
			
		||||
                    : mReceiveState != null
 | 
			
		||||
                            ? AudioStreamsHelper.getBroadcastName(mReceiveState)
 | 
			
		||||
                            : null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private int getRssi() {
 | 
			
		||||
            return mMetadata != null ? mMetadata.getRssi() : Integer.MAX_VALUE;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private AudioStreamsProgressCategoryController.AudioStreamState getState() {
 | 
			
		||||
            return mState;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @Nullable
 | 
			
		||||
        private BluetoothLeBroadcastMetadata getMetadata() {
 | 
			
		||||
            return mMetadata;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void setState(AudioStreamsProgressCategoryController.AudioStreamState state) {
 | 
			
		||||
            mState = state;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void setMetadata(BluetoothLeBroadcastMetadata metadata) {
 | 
			
		||||
            mMetadata = metadata;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,111 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.os.Handler;
 | 
			
		||||
import android.os.Looper;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.StringRes;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
class AudioStreamStateHandler {
 | 
			
		||||
    private static final String TAG = "AudioStreamStateHandler";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
    @VisibleForTesting static final int EMPTY_STRING_RES = 0;
 | 
			
		||||
 | 
			
		||||
    final AudioStreamsRepository mAudioStreamsRepository = AudioStreamsRepository.getInstance();
 | 
			
		||||
    final Handler mHandler = new Handler(Looper.getMainLooper());
 | 
			
		||||
 | 
			
		||||
    AudioStreamStateHandler() {}
 | 
			
		||||
 | 
			
		||||
    void handleStateChange(
 | 
			
		||||
            AudioStreamPreference preference,
 | 
			
		||||
            AudioStreamsProgressCategoryController controller,
 | 
			
		||||
            AudioStreamsHelper helper) {
 | 
			
		||||
        var newState = getStateEnum();
 | 
			
		||||
        if (preference.getAudioStreamState() == newState) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "moveToState() : moving preference : ["
 | 
			
		||||
                            + preference.getAudioStreamBroadcastId()
 | 
			
		||||
                            + ", "
 | 
			
		||||
                            + preference.getAudioStreamBroadcastName()
 | 
			
		||||
                            + "] from state : "
 | 
			
		||||
                            + preference.getAudioStreamState()
 | 
			
		||||
                            + " to state : "
 | 
			
		||||
                            + newState);
 | 
			
		||||
        }
 | 
			
		||||
        preference.setAudioStreamState(newState);
 | 
			
		||||
 | 
			
		||||
        performAction(preference, controller, helper);
 | 
			
		||||
 | 
			
		||||
        // Update UI
 | 
			
		||||
        ThreadUtils.postOnMainThread(
 | 
			
		||||
                () ->
 | 
			
		||||
                        preference.setIsConnected(
 | 
			
		||||
                                newState
 | 
			
		||||
                                        == AudioStreamsProgressCategoryController.AudioStreamState
 | 
			
		||||
                                                .SOURCE_ADDED,
 | 
			
		||||
                                getSummary() != EMPTY_STRING_RES
 | 
			
		||||
                                        ? preference.getContext().getString(getSummary())
 | 
			
		||||
                                        : "",
 | 
			
		||||
                                getOnClickListener(controller)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Perform action related to the audio stream state (e.g, addSource) This method is intended to
 | 
			
		||||
     * be optionally overridden by subclasses to provide custom behavior based on the audio stream
 | 
			
		||||
     * state change.
 | 
			
		||||
     */
 | 
			
		||||
    void performAction(
 | 
			
		||||
            AudioStreamPreference preference,
 | 
			
		||||
            AudioStreamsProgressCategoryController controller,
 | 
			
		||||
            AudioStreamsHelper helper) {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The preference summary for the audio stream state (e.g, Scanning...) This method is intended
 | 
			
		||||
     * to be optionally overridden.
 | 
			
		||||
     */
 | 
			
		||||
    @StringRes
 | 
			
		||||
    int getSummary() {
 | 
			
		||||
        return EMPTY_STRING_RES;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * The preference on click event for the audio stream state (e.g, open up a dialog) This method
 | 
			
		||||
     * is intended to be optionally overridden.
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    Preference.OnPreferenceClickListener getOnClickListener(
 | 
			
		||||
            AudioStreamsProgressCategoryController controller) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Subclasses should always override. */
 | 
			
		||||
    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
 | 
			
		||||
        return AudioStreamsProgressCategoryController.AudioStreamState.UNKNOWN;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,70 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamsActiveDeviceController extends BasePreferenceController
 | 
			
		||||
        implements AudioStreamsActiveDeviceSummaryUpdater.OnSummaryChangeListener,
 | 
			
		||||
                DefaultLifecycleObserver {
 | 
			
		||||
 | 
			
		||||
    public static final String KEY = "audio_streams_active_device";
 | 
			
		||||
    private final AudioStreamsActiveDeviceSummaryUpdater mSummaryHelper;
 | 
			
		||||
    @Nullable private Preference mPreference;
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsActiveDeviceController(Context context, String preferenceKey) {
 | 
			
		||||
        super(context, preferenceKey);
 | 
			
		||||
        mSummaryHelper = new AudioStreamsActiveDeviceSummaryUpdater(mContext, this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        mPreference = screen.findPreference(KEY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AVAILABLE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSummaryChanged(String summary) {
 | 
			
		||||
        if (mPreference != null) {
 | 
			
		||||
            mPreference.setSummary(summary);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onResume(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        mSummaryHelper.register(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        mSummaryHelper.register(false);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,106 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.text.TextUtils;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamsActiveDeviceSummaryUpdater implements BluetoothCallback {
 | 
			
		||||
    private static final String TAG = "AudioStreamsActiveDeviceSummaryUpdater";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
    private final LocalBluetoothManager mBluetoothManager;
 | 
			
		||||
    private Context mContext;
 | 
			
		||||
    @Nullable private String mSummary;
 | 
			
		||||
    private OnSummaryChangeListener mListener;
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsActiveDeviceSummaryUpdater(
 | 
			
		||||
            Context context, OnSummaryChangeListener listener) {
 | 
			
		||||
        mContext = context;
 | 
			
		||||
        mBluetoothManager = Utils.getLocalBluetoothManager(context);
 | 
			
		||||
        mListener = listener;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onActiveDeviceChanged(
 | 
			
		||||
            @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "onActiveDeviceChanged() with activeDevice : "
 | 
			
		||||
                            + (activeDevice == null ? "null" : activeDevice.getAddress())
 | 
			
		||||
                            + " on profile : "
 | 
			
		||||
                            + bluetoothProfile);
 | 
			
		||||
        }
 | 
			
		||||
        if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
 | 
			
		||||
            notifyChangeIfNeeded();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void register(boolean register) {
 | 
			
		||||
        if (register) {
 | 
			
		||||
            notifyChangeIfNeeded();
 | 
			
		||||
            mBluetoothManager.getEventManager().registerCallback(this);
 | 
			
		||||
        } else {
 | 
			
		||||
            mBluetoothManager.getEventManager().unregisterCallback(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void notifyChangeIfNeeded() {
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            String summary = getSummary();
 | 
			
		||||
                            if (!TextUtils.equals(mSummary, summary)) {
 | 
			
		||||
                                mSummary = summary;
 | 
			
		||||
                                ThreadUtils.postOnMainThread(
 | 
			
		||||
                                        () -> mListener.onSummaryChanged(summary));
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private String getSummary() {
 | 
			
		||||
        var connectedSink =
 | 
			
		||||
                AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
 | 
			
		||||
                        mBluetoothManager);
 | 
			
		||||
        if (connectedSink.isEmpty()) {
 | 
			
		||||
            return mContext.getString(R.string.audio_streams_dialog_no_le_device_title);
 | 
			
		||||
        }
 | 
			
		||||
        return connectedSink.get().getName();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Interface definition for a callback to be invoked when the summary has been changed. */
 | 
			
		||||
    interface OnSummaryChangeListener {
 | 
			
		||||
        /**
 | 
			
		||||
         * Called when summary has changed.
 | 
			
		||||
         *
 | 
			
		||||
         * @param summary The new summary.
 | 
			
		||||
         */
 | 
			
		||||
        void onSummaryChanged(String summary);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,132 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastAssistant;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamsBroadcastAssistantCallback
 | 
			
		||||
        implements BluetoothLeBroadcastAssistant.Callback {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioStreamsBroadcastAssistantCallback";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onReceiveStateChanged(
 | 
			
		||||
            BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "onReceiveStateChanged() sink : "
 | 
			
		||||
                            + sink.getAddress()
 | 
			
		||||
                            + " sourceId: "
 | 
			
		||||
                            + sourceId
 | 
			
		||||
                            + " state: "
 | 
			
		||||
                            + state);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSearchStartFailed(int reason) {
 | 
			
		||||
        Log.w(TAG, "onSearchStartFailed() reason : " + reason);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSearchStarted(int reason) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "onSearchStarted() reason : " + reason);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSearchStopFailed(int reason) {
 | 
			
		||||
        Log.w(TAG, "onSearchStopFailed() reason : " + reason);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSearchStopped(int reason) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "onSearchStopped() reason : " + reason);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceAddFailed(
 | 
			
		||||
            BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "onSourceAddFailed() sink : "
 | 
			
		||||
                            + sink.getAddress()
 | 
			
		||||
                            + " source: "
 | 
			
		||||
                            + source
 | 
			
		||||
                            + " reason: "
 | 
			
		||||
                            + reason);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceAdded(BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "onSourceAdded() sink : "
 | 
			
		||||
                            + sink.getAddress()
 | 
			
		||||
                            + " sourceId: "
 | 
			
		||||
                            + sourceId
 | 
			
		||||
                            + " reason: "
 | 
			
		||||
                            + reason);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceFound(BluetoothLeBroadcastMetadata source) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "onSourceFound() broadcastId : " + source.getBroadcastId());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceLost(int broadcastId) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "onSourceLost() broadcastId : " + broadcastId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceModified(BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceModifyFailed(BluetoothDevice sink, int sourceId, int reason) {}
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
        Log.w(TAG, "onSourceRemoveFailed() sourceId : " + sourceId + " reason : " + reason);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "onSourceRemoved() sourceId : " + sourceId + " reason : " + reason);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,134 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingBasePreferenceController;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.flags.Flags;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamsCategoryController extends AudioSharingBasePreferenceController {
 | 
			
		||||
    private static final String TAG = "AudioStreamsCategoryController";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
    private final LocalBluetoothManager mLocalBtManager;
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    private final BluetoothCallback mBluetoothCallback =
 | 
			
		||||
            new BluetoothCallback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onActiveDeviceChanged(
 | 
			
		||||
                        @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
 | 
			
		||||
                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
 | 
			
		||||
                        updateVisibility();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsCategoryController(Context context, String key) {
 | 
			
		||||
        super(context, key);
 | 
			
		||||
        mLocalBtManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        super.onStart(owner);
 | 
			
		||||
        if (mLocalBtManager != null) {
 | 
			
		||||
            mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        super.onStop(owner);
 | 
			
		||||
        if (mLocalBtManager != null) {
 | 
			
		||||
            mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return Flags.enableLeAudioQrCodePrivateBroadcastSharing()
 | 
			
		||||
                ? AVAILABLE
 | 
			
		||||
                : UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void updateVisibility() {
 | 
			
		||||
        if (mPreference == null) return;
 | 
			
		||||
        mExecutor.execute(
 | 
			
		||||
                () -> {
 | 
			
		||||
                    if (!isAvailable()) {
 | 
			
		||||
                        Log.d(TAG, "skip updateVisibility, unavailable preference");
 | 
			
		||||
                        AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                                mContext,
 | 
			
		||||
                                () -> { // Check nullability to pass NullAway check
 | 
			
		||||
                                    if (mPreference != null) {
 | 
			
		||||
                                        mPreference.setVisible(false);
 | 
			
		||||
                                    }
 | 
			
		||||
                                });
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    boolean hasConnectedLe =
 | 
			
		||||
                            AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
 | 
			
		||||
                                            mLocalBtManager)
 | 
			
		||||
                                    .isPresent();
 | 
			
		||||
                    boolean isProfileReady =
 | 
			
		||||
                            AudioSharingUtils.isAudioSharingProfileReady(
 | 
			
		||||
                                    mLocalBtManager.getProfileManager());
 | 
			
		||||
                    boolean isBroadcasting = isBroadcasting();
 | 
			
		||||
                    boolean isBluetoothOn = isBluetoothStateOn();
 | 
			
		||||
                    if (DEBUG) {
 | 
			
		||||
                        Log.d(
 | 
			
		||||
                                TAG,
 | 
			
		||||
                                "updateVisibility() isBroadcasting : "
 | 
			
		||||
                                        + isBroadcasting
 | 
			
		||||
                                        + " hasConnectedLe : "
 | 
			
		||||
                                        + hasConnectedLe
 | 
			
		||||
                                        + " is BT on : "
 | 
			
		||||
                                        + isBluetoothOn
 | 
			
		||||
                                        + " is profile ready : "
 | 
			
		||||
                                        + isProfileReady);
 | 
			
		||||
                    }
 | 
			
		||||
                    AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                            mContext,
 | 
			
		||||
                            () -> { // Check nullability to pass NullAway check
 | 
			
		||||
                                if (mPreference != null) {
 | 
			
		||||
                                    mPreference.setVisible(
 | 
			
		||||
                                            isProfileReady
 | 
			
		||||
                                                    && isBluetoothOn
 | 
			
		||||
                                                    && hasConnectedLe
 | 
			
		||||
                                                    && !isBroadcasting);
 | 
			
		||||
                                }
 | 
			
		||||
                            });
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,138 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeFragment;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
 | 
			
		||||
import com.google.common.base.Strings;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamsDashboardFragment extends DashboardFragment {
 | 
			
		||||
    private static final String TAG = "AudioStreamsDashboardFrag";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
    private AudioStreamsProgressCategoryController mAudioStreamsProgressCategoryController;
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsDashboardFragment() {
 | 
			
		||||
        super();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        // TODO: update category id.
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected String getLogTag() {
 | 
			
		||||
        return TAG;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getHelpResource() {
 | 
			
		||||
        return R.string.help_url_audio_sharing;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected int getPreferenceScreenResId() {
 | 
			
		||||
        return R.xml.bluetooth_le_audio_streams;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onAttach(Context context) {
 | 
			
		||||
        super.onAttach(context);
 | 
			
		||||
        use(AudioStreamsScanQrCodeController.class).setFragment(this);
 | 
			
		||||
        mAudioStreamsProgressCategoryController = use(AudioStreamsProgressCategoryController.class);
 | 
			
		||||
        mAudioStreamsProgressCategoryController.setFragment(this);
 | 
			
		||||
 | 
			
		||||
        if (getArguments() != null) {
 | 
			
		||||
            String broadcastMetadataStr =
 | 
			
		||||
                    getArguments().getString(AudioStreamConfirmDialog.KEY_BROADCAST_METADATA);
 | 
			
		||||
            if (!Strings.isNullOrEmpty(broadcastMetadataStr)) {
 | 
			
		||||
                BluetoothLeBroadcastMetadata broadcastMetadata =
 | 
			
		||||
                        BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
 | 
			
		||||
                                broadcastMetadataStr);
 | 
			
		||||
                if (broadcastMetadata == null) {
 | 
			
		||||
                    Log.w(TAG, "onAttach() broadcastMetadata is null!");
 | 
			
		||||
                } else {
 | 
			
		||||
                    mAudioStreamsProgressCategoryController.setSourceFromQrCode(broadcastMetadata);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onActivityCreated(Bundle savedInstanceState) {
 | 
			
		||||
        super.onActivityCreated(savedInstanceState);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
 | 
			
		||||
        super.onActivityResult(requestCode, resultCode, data);
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "onActivityResult() requestCode : "
 | 
			
		||||
                            + requestCode
 | 
			
		||||
                            + " resultCode : "
 | 
			
		||||
                            + resultCode);
 | 
			
		||||
        }
 | 
			
		||||
        if (requestCode == REQUEST_SCAN_BT_BROADCAST_QR_CODE) {
 | 
			
		||||
            if (resultCode == Activity.RESULT_OK) {
 | 
			
		||||
                String broadcastMetadata =
 | 
			
		||||
                        data != null
 | 
			
		||||
                                ? data.getStringExtra(QrCodeScanModeFragment.KEY_BROADCAST_METADATA)
 | 
			
		||||
                                : "";
 | 
			
		||||
                BluetoothLeBroadcastMetadata source =
 | 
			
		||||
                        BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
 | 
			
		||||
                                broadcastMetadata);
 | 
			
		||||
                if (source == null) {
 | 
			
		||||
                    Log.w(TAG, "onActivityResult() source is null!");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                if (DEBUG) {
 | 
			
		||||
                    Log.d(TAG, "onActivityResult() broadcastId : " + source.getBroadcastId());
 | 
			
		||||
                }
 | 
			
		||||
                if (mAudioStreamsProgressCategoryController == null) {
 | 
			
		||||
                    Log.w(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "onActivityResult() AudioStreamsProgressCategoryController is null!");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                mAudioStreamsProgressCategoryController.setSourceFromQrCode(source);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,249 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.app.AlertDialog;
 | 
			
		||||
import android.app.Dialog;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.widget.Button;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.fragment.app.DialogFragment;
 | 
			
		||||
import androidx.fragment.app.Fragment;
 | 
			
		||||
import androidx.fragment.app.FragmentManager;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
 | 
			
		||||
 | 
			
		||||
import com.google.common.base.Strings;
 | 
			
		||||
 | 
			
		||||
import java.util.function.Consumer;
 | 
			
		||||
 | 
			
		||||
/** A dialog fragment for constructing and showing audio stream dialogs. */
 | 
			
		||||
public class AudioStreamsDialogFragment extends InstrumentedDialogFragment {
 | 
			
		||||
    private static final String TAG = "AudioStreamsDialogFragment";
 | 
			
		||||
    private final DialogBuilder mDialogBuilder;
 | 
			
		||||
 | 
			
		||||
    AudioStreamsDialogFragment(DialogBuilder dialogBuilder) {
 | 
			
		||||
        mDialogBuilder = dialogBuilder;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        // TODO(chelseahao): update metrics id
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Dialog onCreateDialog(Bundle savedInstanceState) {
 | 
			
		||||
        return mDialogBuilder.build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Displays the audio stream dialog on the specified host fragment.
 | 
			
		||||
     *
 | 
			
		||||
     * @param host The fragment to host the dialog.
 | 
			
		||||
     * @param dialogBuilder The builder for constructing the dialog.
 | 
			
		||||
     */
 | 
			
		||||
    public static void show(Fragment host, DialogBuilder dialogBuilder) {
 | 
			
		||||
        if (!host.isAdded()) {
 | 
			
		||||
            Log.w(TAG, "The host fragment is not added to the activity!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        FragmentManager manager = host.getChildFragmentManager();
 | 
			
		||||
        (new AudioStreamsDialogFragment(dialogBuilder)).show(manager, TAG);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static void dismissAll(Fragment host) {
 | 
			
		||||
        if (!host.isAdded()) {
 | 
			
		||||
            Log.w(TAG, "The host fragment is not added to the activity!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        FragmentManager manager = host.getChildFragmentManager();
 | 
			
		||||
        Fragment dialog = manager.findFragmentByTag(TAG);
 | 
			
		||||
        if (dialog != null
 | 
			
		||||
                && ((DialogFragment) dialog).getDialog() != null
 | 
			
		||||
                && ((DialogFragment) dialog).getDialog().isShowing()) {
 | 
			
		||||
            ((DialogFragment) dialog).dismiss();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void show(@NonNull FragmentManager manager, @Nullable String tag) {
 | 
			
		||||
        Fragment dialog = manager.findFragmentByTag(TAG);
 | 
			
		||||
        if (dialog != null
 | 
			
		||||
                && ((DialogFragment) dialog).getDialog() != null
 | 
			
		||||
                && ((DialogFragment) dialog).getDialog().isShowing()) {
 | 
			
		||||
            Log.w(TAG, "Dialog already showing, ignore");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        super.show(manager, tag);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** A builder class for constructing the audio stream dialog. */
 | 
			
		||||
    public static class DialogBuilder {
 | 
			
		||||
        private final Context mContext;
 | 
			
		||||
        private final AlertDialog.Builder mBuilder;
 | 
			
		||||
        @Nullable private String mTitle;
 | 
			
		||||
        @Nullable private String mSubTitle1;
 | 
			
		||||
        @Nullable private String mSubTitle2;
 | 
			
		||||
        @Nullable private String mLeftButtonText;
 | 
			
		||||
        @Nullable private String mRightButtonText;
 | 
			
		||||
        @Nullable private Consumer<AlertDialog> mLeftButtonOnClickListener;
 | 
			
		||||
        @Nullable private Consumer<AlertDialog> mRightButtonOnClickListener;
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Constructs a new instance of DialogBuilder.
 | 
			
		||||
         *
 | 
			
		||||
         * @param context The context used for building the dialog.
 | 
			
		||||
         */
 | 
			
		||||
        public DialogBuilder(Context context) {
 | 
			
		||||
            mContext = context;
 | 
			
		||||
            mBuilder = new AlertDialog.Builder(context);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the title of the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param title The title text.
 | 
			
		||||
         * @return This DialogBuilder instance.
 | 
			
		||||
         */
 | 
			
		||||
        public DialogBuilder setTitle(String title) {
 | 
			
		||||
            mTitle = title;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the first subtitle of the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param subTitle1 The text of the first subtitle.
 | 
			
		||||
         * @return This DialogBuilder instance.
 | 
			
		||||
         */
 | 
			
		||||
        public DialogBuilder setSubTitle1(String subTitle1) {
 | 
			
		||||
            mSubTitle1 = subTitle1;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the second subtitle of the dialog.
 | 
			
		||||
         *
 | 
			
		||||
         * @param subTitle2 The text of the second subtitle.
 | 
			
		||||
         * @return This DialogBuilder instance.
 | 
			
		||||
         */
 | 
			
		||||
        public DialogBuilder setSubTitle2(String subTitle2) {
 | 
			
		||||
            mSubTitle2 = subTitle2;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the text of the left button.
 | 
			
		||||
         *
 | 
			
		||||
         * @param text The text of the left button.
 | 
			
		||||
         * @return This DialogBuilder instance.
 | 
			
		||||
         */
 | 
			
		||||
        public DialogBuilder setLeftButtonText(String text) {
 | 
			
		||||
            mLeftButtonText = text;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the click listener of the left button.
 | 
			
		||||
         *
 | 
			
		||||
         * @param listener The click listener for the left button.
 | 
			
		||||
         * @return This DialogBuilder instance.
 | 
			
		||||
         */
 | 
			
		||||
        public DialogBuilder setLeftButtonOnClickListener(Consumer<AlertDialog> listener) {
 | 
			
		||||
            mLeftButtonOnClickListener = listener;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the text of the right button.
 | 
			
		||||
         *
 | 
			
		||||
         * @param text The text of the right button.
 | 
			
		||||
         * @return This DialogBuilder instance.
 | 
			
		||||
         */
 | 
			
		||||
        public DialogBuilder setRightButtonText(String text) {
 | 
			
		||||
            mRightButtonText = text;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Sets the click listener of the right button.
 | 
			
		||||
         *
 | 
			
		||||
         * @param listener The click listener for the right button.
 | 
			
		||||
         * @return This DialogBuilder instance.
 | 
			
		||||
         */
 | 
			
		||||
        public DialogBuilder setRightButtonOnClickListener(Consumer<AlertDialog> listener) {
 | 
			
		||||
            mRightButtonOnClickListener = listener;
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        AlertDialog build() {
 | 
			
		||||
            View rootView =
 | 
			
		||||
                    LayoutInflater.from(mContext)
 | 
			
		||||
                            .inflate(R.xml.bluetooth_audio_streams_dialog, /* parent= */ null);
 | 
			
		||||
 | 
			
		||||
            AlertDialog dialog = mBuilder.setView(rootView).setCancelable(false).create();
 | 
			
		||||
            dialog.setCanceledOnTouchOutside(false);
 | 
			
		||||
 | 
			
		||||
            TextView title = rootView.requireViewById(R.id.dialog_title);
 | 
			
		||||
            title.setText(mTitle);
 | 
			
		||||
 | 
			
		||||
            if (!Strings.isNullOrEmpty(mSubTitle1)) {
 | 
			
		||||
                TextView subTitle1 = rootView.requireViewById(R.id.dialog_subtitle);
 | 
			
		||||
                subTitle1.setText(mSubTitle1);
 | 
			
		||||
                subTitle1.setVisibility(View.VISIBLE);
 | 
			
		||||
            }
 | 
			
		||||
            if (!Strings.isNullOrEmpty(mSubTitle2)) {
 | 
			
		||||
                TextView subTitle2 = rootView.requireViewById(R.id.dialog_subtitle_2);
 | 
			
		||||
                subTitle2.setText(mSubTitle2);
 | 
			
		||||
                subTitle2.setVisibility(View.VISIBLE);
 | 
			
		||||
            }
 | 
			
		||||
            if (!Strings.isNullOrEmpty(mLeftButtonText)) {
 | 
			
		||||
                Button leftButton = rootView.requireViewById(R.id.left_button);
 | 
			
		||||
                leftButton.setText(mLeftButtonText);
 | 
			
		||||
                leftButton.setVisibility(View.VISIBLE);
 | 
			
		||||
                leftButton.setOnClickListener(
 | 
			
		||||
                        unused -> {
 | 
			
		||||
                            if (mLeftButtonOnClickListener != null) {
 | 
			
		||||
                                mLeftButtonOnClickListener.accept(dialog);
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
            }
 | 
			
		||||
            if (!Strings.isNullOrEmpty(mRightButtonText)) {
 | 
			
		||||
                Button rightButton = rootView.requireViewById(R.id.right_button);
 | 
			
		||||
                rightButton.setText(mRightButtonText);
 | 
			
		||||
                rightButton.setVisibility(View.VISIBLE);
 | 
			
		||||
                rightButton.setOnClickListener(
 | 
			
		||||
                        unused -> {
 | 
			
		||||
                            if (mRightButtonOnClickListener != null) {
 | 
			
		||||
                                mRightButtonOnClickListener.accept(dialog);
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return dialog;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,355 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_ID;
 | 
			
		||||
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.BROADCAST_TITLE;
 | 
			
		||||
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamMediaService.DEVICES;
 | 
			
		||||
 | 
			
		||||
import static java.util.Collections.emptyList;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeAudioContentMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
import com.google.common.base.Strings;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A helper class that adds, removes and retrieves LE broadcast sources for all active sink devices.
 | 
			
		||||
 */
 | 
			
		||||
public class AudioStreamsHelper {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioStreamsHelper";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
 | 
			
		||||
    private final @Nullable LocalBluetoothManager mBluetoothManager;
 | 
			
		||||
    private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
 | 
			
		||||
 | 
			
		||||
    AudioStreamsHelper(@Nullable LocalBluetoothManager bluetoothManager) {
 | 
			
		||||
        mBluetoothManager = bluetoothManager;
 | 
			
		||||
        mLeBroadcastAssistant = getLeBroadcastAssistant(mBluetoothManager);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adds the specified LE broadcast source to all active sinks.
 | 
			
		||||
     *
 | 
			
		||||
     * @param source The LE broadcast metadata representing the audio source.
 | 
			
		||||
     */
 | 
			
		||||
    void addSource(BluetoothLeBroadcastMetadata source) {
 | 
			
		||||
        if (mLeBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "addSource(): LeBroadcastAssistant is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            for (var sink :
 | 
			
		||||
                                    getConnectedBluetoothDevices(
 | 
			
		||||
                                            mBluetoothManager, /* inSharingOnly= */ false)) {
 | 
			
		||||
                                if (DEBUG) {
 | 
			
		||||
                                    Log.d(
 | 
			
		||||
                                            TAG,
 | 
			
		||||
                                            "addSource(): join broadcast broadcastId"
 | 
			
		||||
                                                    + " : "
 | 
			
		||||
                                                    + source.getBroadcastId()
 | 
			
		||||
                                                    + " sink : "
 | 
			
		||||
                                                    + sink.getAddress());
 | 
			
		||||
                                }
 | 
			
		||||
                                mLeBroadcastAssistant.addSource(sink, source, false);
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Removes sources from LE broadcasts associated for all active sinks based on broadcast Id. */
 | 
			
		||||
    void removeSource(int broadcastId) {
 | 
			
		||||
        if (mLeBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "removeSource(): LeBroadcastAssistant is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            for (var sink :
 | 
			
		||||
                                    getConnectedBluetoothDevices(
 | 
			
		||||
                                            mBluetoothManager, /* inSharingOnly= */ true)) {
 | 
			
		||||
                                if (DEBUG) {
 | 
			
		||||
                                    Log.d(
 | 
			
		||||
                                            TAG,
 | 
			
		||||
                                            "removeSource(): remove all sources with broadcast id :"
 | 
			
		||||
                                                    + broadcastId
 | 
			
		||||
                                                    + " from sink : "
 | 
			
		||||
                                                    + sink.getAddress());
 | 
			
		||||
                                }
 | 
			
		||||
                                mLeBroadcastAssistant.getAllSources(sink).stream()
 | 
			
		||||
                                        .filter(state -> state.getBroadcastId() == broadcastId)
 | 
			
		||||
                                        .forEach(
 | 
			
		||||
                                                state ->
 | 
			
		||||
                                                        mLeBroadcastAssistant.removeSource(
 | 
			
		||||
                                                                sink, state.getSourceId()));
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Retrieves a list of all LE broadcast receive states from active sinks. */
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    public List<BluetoothLeBroadcastReceiveState> getAllConnectedSources() {
 | 
			
		||||
        if (mLeBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "getAllSources(): LeBroadcastAssistant is null!");
 | 
			
		||||
            return emptyList();
 | 
			
		||||
        }
 | 
			
		||||
        return getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true).stream()
 | 
			
		||||
                .flatMap(sink -> mLeBroadcastAssistant.getAllSources(sink).stream())
 | 
			
		||||
                .filter(AudioStreamsHelper::isConnected)
 | 
			
		||||
                .toList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant() {
 | 
			
		||||
        return mLeBroadcastAssistant;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Checks the connectivity status based on the provided broadcast receive state. */
 | 
			
		||||
    public static boolean isConnected(BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
        return state.getBisSyncState().stream().anyMatch(bitmap -> bitmap != 0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static boolean isBadCode(BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
        return state.getPaSyncState() == BluetoothLeBroadcastReceiveState.PA_SYNC_STATE_SYNCHRONIZED
 | 
			
		||||
                && state.getBigEncryptionState()
 | 
			
		||||
                        == BluetoothLeBroadcastReceiveState.BIG_ENCRYPTION_STATE_BAD_CODE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns a {@code CachedBluetoothDevice} that is either connected to a broadcast source or is
 | 
			
		||||
     * a connected LE device.
 | 
			
		||||
     */
 | 
			
		||||
    public static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharingOrLeConnected(
 | 
			
		||||
            @androidx.annotation.Nullable LocalBluetoothManager manager) {
 | 
			
		||||
        if (manager == null) {
 | 
			
		||||
            Log.w(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "getCachedBluetoothDeviceInSharingOrLeConnected(): LocalBluetoothManager is"
 | 
			
		||||
                            + " null!");
 | 
			
		||||
            return Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
        var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager);
 | 
			
		||||
        var leadDevices =
 | 
			
		||||
                AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
 | 
			
		||||
        if (leadDevices.isEmpty()) {
 | 
			
		||||
            Log.w(TAG, "getCachedBluetoothDeviceInSharingOrLeConnected(): No lead device!");
 | 
			
		||||
            return Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
        var deviceHasSource =
 | 
			
		||||
                leadDevices.stream()
 | 
			
		||||
                        .filter(device -> hasConnectedBroadcastSource(device, manager))
 | 
			
		||||
                        .findFirst();
 | 
			
		||||
        if (deviceHasSource.isPresent()) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "getCachedBluetoothDeviceInSharingOrLeConnected(): Device has connected source"
 | 
			
		||||
                            + " found: "
 | 
			
		||||
                            + deviceHasSource.get().getAddress());
 | 
			
		||||
            return deviceHasSource;
 | 
			
		||||
        }
 | 
			
		||||
        Log.d(
 | 
			
		||||
                TAG,
 | 
			
		||||
                "getCachedBluetoothDeviceInSharingOrLeConnected(): Device connected found: "
 | 
			
		||||
                        + leadDevices.get(0).getAddress());
 | 
			
		||||
        return Optional.of(leadDevices.get(0));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** Returns a {@code CachedBluetoothDevice} that has a connected broadcast source. */
 | 
			
		||||
    static Optional<CachedBluetoothDevice> getCachedBluetoothDeviceInSharing(
 | 
			
		||||
            @androidx.annotation.Nullable LocalBluetoothManager manager) {
 | 
			
		||||
        if (manager == null) {
 | 
			
		||||
            Log.w(TAG, "getCachedBluetoothDeviceInSharing(): LocalBluetoothManager is null!");
 | 
			
		||||
            return Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
        var groupedDevices = AudioSharingUtils.fetchConnectedDevicesByGroupId(manager);
 | 
			
		||||
        var leadDevices =
 | 
			
		||||
                AudioSharingUtils.buildOrderedConnectedLeadDevices(manager, groupedDevices, false);
 | 
			
		||||
        if (leadDevices.isEmpty()) {
 | 
			
		||||
            Log.w(TAG, "getCachedBluetoothDeviceInSharing(): No lead device!");
 | 
			
		||||
            return Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
        return leadDevices.stream()
 | 
			
		||||
                .filter(device -> hasConnectedBroadcastSource(device, manager))
 | 
			
		||||
                .findFirst();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if {@link CachedBluetoothDevice} has connected to a broadcast source.
 | 
			
		||||
     *
 | 
			
		||||
     * @param cachedDevice The cached bluetooth device to check.
 | 
			
		||||
     * @param localBtManager The BT manager to provide BT functions.
 | 
			
		||||
     * @return Whether the device has connected to a broadcast source.
 | 
			
		||||
     */
 | 
			
		||||
    private static boolean hasConnectedBroadcastSource(
 | 
			
		||||
            CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) {
 | 
			
		||||
        if (localBtManager == null) {
 | 
			
		||||
            Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        LocalBluetoothLeBroadcastAssistant assistant =
 | 
			
		||||
                localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
        if (assistant == null) {
 | 
			
		||||
            Log.d(TAG, "Skip check hasConnectedBroadcastSource due to assistant profile is null");
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        List<BluetoothLeBroadcastReceiveState> sourceList =
 | 
			
		||||
                assistant.getAllSources(cachedDevice.getDevice());
 | 
			
		||||
        if (!sourceList.isEmpty()
 | 
			
		||||
                && sourceList.stream().anyMatch(AudioStreamsHelper::isConnected)) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "Lead device has connected broadcast source, device = "
 | 
			
		||||
                            + cachedDevice.getDevice().getAnonymizedAddress());
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        // Return true if member device is in broadcast.
 | 
			
		||||
        for (CachedBluetoothDevice device : cachedDevice.getMemberDevice()) {
 | 
			
		||||
            List<BluetoothLeBroadcastReceiveState> list =
 | 
			
		||||
                    assistant.getAllSources(device.getDevice());
 | 
			
		||||
            if (!list.isEmpty() && list.stream().anyMatch(AudioStreamsHelper::isConnected)) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "Member device has connected broadcast source, device = "
 | 
			
		||||
                                + device.getDevice().getAnonymizedAddress());
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Retrieves a list of connected Bluetooth devices that belongs to one {@link
 | 
			
		||||
     * CachedBluetoothDevice} that's either connected to a broadcast source or is a connected LE
 | 
			
		||||
     * audio device.
 | 
			
		||||
     */
 | 
			
		||||
    static List<BluetoothDevice> getConnectedBluetoothDevices(
 | 
			
		||||
            @Nullable LocalBluetoothManager manager, boolean inSharingOnly) {
 | 
			
		||||
        if (manager == null) {
 | 
			
		||||
            Log.w(TAG, "getConnectedBluetoothDevices(): LocalBluetoothManager is null!");
 | 
			
		||||
            return emptyList();
 | 
			
		||||
        }
 | 
			
		||||
        var leBroadcastAssistant = getLeBroadcastAssistant(manager);
 | 
			
		||||
        if (leBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "getConnectedBluetoothDevices(): LeBroadcastAssistant is null!");
 | 
			
		||||
            return emptyList();
 | 
			
		||||
        }
 | 
			
		||||
        List<BluetoothDevice> connectedDevices =
 | 
			
		||||
                leBroadcastAssistant.getDevicesMatchingConnectionStates(
 | 
			
		||||
                        new int[] {BluetoothProfile.STATE_CONNECTED});
 | 
			
		||||
        Optional<CachedBluetoothDevice> cachedBluetoothDevice =
 | 
			
		||||
                inSharingOnly
 | 
			
		||||
                        ? getCachedBluetoothDeviceInSharing(manager)
 | 
			
		||||
                        : getCachedBluetoothDeviceInSharingOrLeConnected(manager);
 | 
			
		||||
        List<BluetoothDevice> bluetoothDevices =
 | 
			
		||||
                cachedBluetoothDevice
 | 
			
		||||
                        .map(
 | 
			
		||||
                                c ->
 | 
			
		||||
                                        Stream.concat(
 | 
			
		||||
                                                        Stream.of(c.getDevice()),
 | 
			
		||||
                                                        c.getMemberDevice().stream()
 | 
			
		||||
                                                                .map(
 | 
			
		||||
                                                                        CachedBluetoothDevice
 | 
			
		||||
                                                                                ::getDevice))
 | 
			
		||||
                                                .filter(connectedDevices::contains)
 | 
			
		||||
                                                .toList())
 | 
			
		||||
                        .orElse(emptyList());
 | 
			
		||||
        Log.d(TAG, "getConnectedBluetoothDevices() devices: " + bluetoothDevices);
 | 
			
		||||
        return bluetoothDevices;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static @Nullable LocalBluetoothLeBroadcastAssistant getLeBroadcastAssistant(
 | 
			
		||||
            @Nullable LocalBluetoothManager manager) {
 | 
			
		||||
        if (manager == null) {
 | 
			
		||||
            Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothManager is null!");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        LocalBluetoothProfileManager profileManager = manager.getProfileManager();
 | 
			
		||||
        if (profileManager == null) {
 | 
			
		||||
            Log.w(TAG, "getLeBroadcastAssistant(): LocalBluetoothProfileManager is null!");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return profileManager.getLeAudioBroadcastAssistantProfile();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static String getBroadcastName(BluetoothLeBroadcastMetadata source) {
 | 
			
		||||
        // TODO(b/331547596): prioritize broadcastName
 | 
			
		||||
        Optional<String> optionalProgramInfo =
 | 
			
		||||
                source.getSubgroups().stream()
 | 
			
		||||
                        .map(subgroup -> subgroup.getContentMetadata().getProgramInfo())
 | 
			
		||||
                        .filter(programInfo -> !Strings.isNullOrEmpty(programInfo))
 | 
			
		||||
                        .findFirst();
 | 
			
		||||
 | 
			
		||||
        return optionalProgramInfo.orElseGet(
 | 
			
		||||
                () -> {
 | 
			
		||||
                    String broadcastName = source.getBroadcastName();
 | 
			
		||||
                    if (broadcastName != null && !broadcastName.isEmpty()) {
 | 
			
		||||
                        return broadcastName;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return "Broadcast Id: " + source.getBroadcastId();
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static String getBroadcastName(BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
        return state.getSubgroupMetadata().stream()
 | 
			
		||||
                .map(BluetoothLeAudioContentMetadata::getProgramInfo)
 | 
			
		||||
                .filter(i -> !Strings.isNullOrEmpty(i))
 | 
			
		||||
                .findFirst()
 | 
			
		||||
                .orElse("Broadcast Id: " + state.getBroadcastId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void startMediaService(Context context, int audioStreamBroadcastId, String title) {
 | 
			
		||||
        List<BluetoothDevice> devices =
 | 
			
		||||
                getConnectedBluetoothDevices(mBluetoothManager, /* inSharingOnly= */ true);
 | 
			
		||||
        if (devices.isEmpty()) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        var intent = new Intent(context, AudioStreamMediaService.class);
 | 
			
		||||
        intent.putExtra(BROADCAST_ID, audioStreamBroadcastId);
 | 
			
		||||
        intent.putExtra(BROADCAST_TITLE, title);
 | 
			
		||||
        intent.putParcelableArrayListExtra(DEVICES, new ArrayList<>(devices));
 | 
			
		||||
        context.startService(intent);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,122 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamsProgressCategoryCallback extends AudioStreamsBroadcastAssistantCallback {
 | 
			
		||||
    private static final String TAG = "AudioStreamsProgressCategoryCallback";
 | 
			
		||||
 | 
			
		||||
    private final AudioStreamsProgressCategoryController mCategoryController;
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsProgressCategoryCallback(
 | 
			
		||||
            AudioStreamsProgressCategoryController audioStreamsProgressCategoryController) {
 | 
			
		||||
        mCategoryController = audioStreamsProgressCategoryController;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onReceiveStateChanged(
 | 
			
		||||
            BluetoothDevice sink, int sourceId, BluetoothLeBroadcastReceiveState state) {
 | 
			
		||||
        super.onReceiveStateChanged(sink, sourceId, state);
 | 
			
		||||
 | 
			
		||||
        if (AudioStreamsHelper.isConnected(state)) {
 | 
			
		||||
            mCategoryController.handleSourceConnected(state);
 | 
			
		||||
        } else if (AudioStreamsHelper.isBadCode(state)) {
 | 
			
		||||
            mCategoryController.handleSourceConnectBadCode(state);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSearchStartFailed(int reason) {
 | 
			
		||||
        super.onSearchStartFailed(reason);
 | 
			
		||||
        mCategoryController.showToast(
 | 
			
		||||
                String.format(Locale.US, "Failed to start scanning, reason %d", reason));
 | 
			
		||||
        mCategoryController.setScanning(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSearchStarted(int reason) {
 | 
			
		||||
        super.onSearchStarted(reason);
 | 
			
		||||
        if (mCategoryController == null) {
 | 
			
		||||
            Log.w(TAG, "onSearchStarted() : mCategoryController is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mCategoryController.setScanning(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSearchStopFailed(int reason) {
 | 
			
		||||
        super.onSearchStopFailed(reason);
 | 
			
		||||
        mCategoryController.showToast(
 | 
			
		||||
                String.format(Locale.US, "Failed to stop scanning, reason %d", reason));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSearchStopped(int reason) {
 | 
			
		||||
        super.onSearchStopped(reason);
 | 
			
		||||
        if (mCategoryController == null) {
 | 
			
		||||
            Log.w(TAG, "onSearchStopped() : mCategoryController is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mCategoryController.setScanning(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceAddFailed(
 | 
			
		||||
            BluetoothDevice sink, BluetoothLeBroadcastMetadata source, int reason) {
 | 
			
		||||
        super.onSourceAddFailed(sink, source, reason);
 | 
			
		||||
        mCategoryController.handleSourceFailedToConnect(source.getBroadcastId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceFound(BluetoothLeBroadcastMetadata source) {
 | 
			
		||||
        super.onSourceFound(source);
 | 
			
		||||
        if (mCategoryController == null) {
 | 
			
		||||
            Log.w(TAG, "onSourceFound() : mCategoryController is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mCategoryController.handleSourceFound(source);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceLost(int broadcastId) {
 | 
			
		||||
        super.onSourceLost(broadcastId);
 | 
			
		||||
        mCategoryController.handleSourceLost(broadcastId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceRemoveFailed(BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
        super.onSourceRemoveFailed(sink, sourceId, reason);
 | 
			
		||||
        mCategoryController.showToast(
 | 
			
		||||
                String.format(
 | 
			
		||||
                        Locale.US,
 | 
			
		||||
                        "Failed to remove source %d for sink %s",
 | 
			
		||||
                        sourceId,
 | 
			
		||||
                        sink.getAddress()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSourceRemoved(BluetoothDevice sink, int sourceId, int reason) {
 | 
			
		||||
        super.onSourceRemoved(sink, sourceId, reason);
 | 
			
		||||
        mCategoryController.handleSourceRemoved();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,575 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import static java.util.Collections.emptyList;
 | 
			
		||||
 | 
			
		||||
import android.app.AlertDialog;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.provider.Settings;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
import java.util.Comparator;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
import java.util.concurrent.Executors;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamsProgressCategoryController extends BasePreferenceController
 | 
			
		||||
        implements DefaultLifecycleObserver {
 | 
			
		||||
    private static final String TAG = "AudioStreamsProgressCategoryController";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
    private static final int UNSET_BROADCAST_ID = -1;
 | 
			
		||||
    private final BluetoothCallback mBluetoothCallback =
 | 
			
		||||
            new BluetoothCallback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onActiveDeviceChanged(
 | 
			
		||||
                        @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
 | 
			
		||||
                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
 | 
			
		||||
                        mExecutor.execute(() -> init());
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    private final Comparator<AudioStreamPreference> mComparator =
 | 
			
		||||
            Comparator.<AudioStreamPreference, Boolean>comparing(
 | 
			
		||||
                            p ->
 | 
			
		||||
                                    p.getAudioStreamState()
 | 
			
		||||
                                            == AudioStreamsProgressCategoryController
 | 
			
		||||
                                                    .AudioStreamState.SOURCE_ADDED)
 | 
			
		||||
                    .thenComparingInt(AudioStreamPreference::getAudioStreamRssi)
 | 
			
		||||
                    .reversed();
 | 
			
		||||
 | 
			
		||||
    public enum AudioStreamState {
 | 
			
		||||
        UNKNOWN,
 | 
			
		||||
        // When mSourceFromQrCode is present and this source has not been synced.
 | 
			
		||||
        WAIT_FOR_SYNC,
 | 
			
		||||
        // When source has been synced but not added to any sink.
 | 
			
		||||
        SYNCED,
 | 
			
		||||
        // When addSource is called for this source and waiting for response.
 | 
			
		||||
        ADD_SOURCE_WAIT_FOR_RESPONSE,
 | 
			
		||||
        // When addSource result in a bad code response.
 | 
			
		||||
        ADD_SOURCE_BAD_CODE,
 | 
			
		||||
        // When addSource result in other bad state.
 | 
			
		||||
        ADD_SOURCE_FAILED,
 | 
			
		||||
        // Source is added to active sink.
 | 
			
		||||
        SOURCE_ADDED,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private final Executor mExecutor;
 | 
			
		||||
    private final AudioStreamsProgressCategoryCallback mBroadcastAssistantCallback;
 | 
			
		||||
    private final AudioStreamsHelper mAudioStreamsHelper;
 | 
			
		||||
    private final MediaControlHelper mMediaControlHelper;
 | 
			
		||||
    private final @Nullable LocalBluetoothLeBroadcastAssistant mLeBroadcastAssistant;
 | 
			
		||||
    private final @Nullable LocalBluetoothManager mBluetoothManager;
 | 
			
		||||
    private final ConcurrentHashMap<Integer, AudioStreamPreference> mBroadcastIdToPreferenceMap =
 | 
			
		||||
            new ConcurrentHashMap<>();
 | 
			
		||||
    private @Nullable BluetoothLeBroadcastMetadata mSourceFromQrCode;
 | 
			
		||||
    @Nullable private AudioStreamsProgressCategoryPreference mCategoryPreference;
 | 
			
		||||
    @Nullable private AudioStreamsDashboardFragment mFragment;
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsProgressCategoryController(Context context, String preferenceKey) {
 | 
			
		||||
        super(context, preferenceKey);
 | 
			
		||||
        mExecutor = Executors.newSingleThreadExecutor();
 | 
			
		||||
        mBluetoothManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
        mAudioStreamsHelper = new AudioStreamsHelper(mBluetoothManager);
 | 
			
		||||
        mMediaControlHelper = new MediaControlHelper(mContext, mBluetoothManager);
 | 
			
		||||
        mLeBroadcastAssistant = mAudioStreamsHelper.getLeBroadcastAssistant();
 | 
			
		||||
        mBroadcastAssistantCallback = new AudioStreamsProgressCategoryCallback(this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AVAILABLE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        mCategoryPreference = screen.findPreference(getPreferenceKey());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (mBluetoothManager != null) {
 | 
			
		||||
            mBluetoothManager.getEventManager().registerCallback(mBluetoothCallback);
 | 
			
		||||
        }
 | 
			
		||||
        mExecutor.execute(this::init);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (mBluetoothManager != null) {
 | 
			
		||||
            mBluetoothManager.getEventManager().unregisterCallback(mBluetoothCallback);
 | 
			
		||||
        }
 | 
			
		||||
        mExecutor.execute(this::stopScanning);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setFragment(AudioStreamsDashboardFragment fragment) {
 | 
			
		||||
        mFragment = fragment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    AudioStreamsDashboardFragment getFragment() {
 | 
			
		||||
        return mFragment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setSourceFromQrCode(BluetoothLeBroadcastMetadata source) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "setSourceFromQrCode(): broadcastId " + source.getBroadcastId());
 | 
			
		||||
        }
 | 
			
		||||
        mSourceFromQrCode = source;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void setScanning(boolean isScanning) {
 | 
			
		||||
        ThreadUtils.postOnMainThread(
 | 
			
		||||
                () -> {
 | 
			
		||||
                    if (mCategoryPreference != null) mCategoryPreference.setProgress(isScanning);
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find preference by scanned source and decide next state.
 | 
			
		||||
    // Expect one of the following:
 | 
			
		||||
    // 1) No preference existed, create new preference with state SYNCED
 | 
			
		||||
    // 2) WAIT_FOR_SYNC, move to ADD_SOURCE_WAIT_FOR_RESPONSE
 | 
			
		||||
    // 3) SOURCE_ADDED, leave as-is
 | 
			
		||||
    void handleSourceFound(BluetoothLeBroadcastMetadata source) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "handleSourceFound()");
 | 
			
		||||
        }
 | 
			
		||||
        var broadcastIdFound = source.getBroadcastId();
 | 
			
		||||
 | 
			
		||||
        if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
 | 
			
		||||
            // mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the
 | 
			
		||||
            // scanned metadata.
 | 
			
		||||
            if (DEBUG) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "handleSourceFound() : processing mSourceFromQrCode with broadcastId"
 | 
			
		||||
                                + " unset");
 | 
			
		||||
            }
 | 
			
		||||
            boolean updated =
 | 
			
		||||
                    maybeUpdateId(
 | 
			
		||||
                            AudioStreamsHelper.getBroadcastName(source), source.getBroadcastId());
 | 
			
		||||
            if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) {
 | 
			
		||||
                var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID);
 | 
			
		||||
                mBroadcastIdToPreferenceMap.put(source.getBroadcastId(), preference);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mBroadcastIdToPreferenceMap.compute(
 | 
			
		||||
                broadcastIdFound,
 | 
			
		||||
                (k, existingPreference) -> {
 | 
			
		||||
                    if (existingPreference == null) {
 | 
			
		||||
                        return addNewPreference(source, AudioStreamState.SYNCED);
 | 
			
		||||
                    }
 | 
			
		||||
                    var fromState = existingPreference.getAudioStreamState();
 | 
			
		||||
                    if (fromState == AudioStreamState.WAIT_FOR_SYNC && mSourceFromQrCode != null) {
 | 
			
		||||
                        // A preference with source founded is existed from a QR code scan. As the
 | 
			
		||||
                        // source is now synced, we update the preference with source from scanning
 | 
			
		||||
                        // as it includes complete broadcast info.
 | 
			
		||||
                        existingPreference.setAudioStreamMetadata(
 | 
			
		||||
                                new BluetoothLeBroadcastMetadata.Builder(source)
 | 
			
		||||
                                        .setBroadcastCode(mSourceFromQrCode.getBroadcastCode())
 | 
			
		||||
                                        .build());
 | 
			
		||||
                        moveToState(
 | 
			
		||||
                                existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // A preference with source founded existed either because it's already
 | 
			
		||||
                        // connected (SOURCE_ADDED). Any other reason is unexpected. We update the
 | 
			
		||||
                        // preference with this source and won't change it's state.
 | 
			
		||||
                        existingPreference.setAudioStreamMetadata(source);
 | 
			
		||||
                        if (fromState != AudioStreamState.SOURCE_ADDED) {
 | 
			
		||||
                            Log.w(
 | 
			
		||||
                                    TAG,
 | 
			
		||||
                                    "handleSourceFound(): unexpected state : "
 | 
			
		||||
                                            + fromState
 | 
			
		||||
                                            + " for broadcastId : "
 | 
			
		||||
                                            + broadcastIdFound);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    return existingPreference;
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean maybeUpdateId(String targetBroadcastName, int broadcastIdToSet) {
 | 
			
		||||
        if (mSourceFromQrCode == null) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (targetBroadcastName.equals(AudioStreamsHelper.getBroadcastName(mSourceFromQrCode))) {
 | 
			
		||||
            if (DEBUG) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "maybeUpdateId() : updating unset broadcastId for metadataFromQrCode with"
 | 
			
		||||
                                + " broadcastName: "
 | 
			
		||||
                                + AudioStreamsHelper.getBroadcastName(mSourceFromQrCode)
 | 
			
		||||
                                + " to broadcast Id: "
 | 
			
		||||
                                + broadcastIdToSet);
 | 
			
		||||
            }
 | 
			
		||||
            mSourceFromQrCode =
 | 
			
		||||
                    new BluetoothLeBroadcastMetadata.Builder(mSourceFromQrCode)
 | 
			
		||||
                            .setBroadcastId(broadcastIdToSet)
 | 
			
		||||
                            .build();
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find preference by mSourceFromQrCode and decide next state.
 | 
			
		||||
    // Expect no preference existed, create new preference with state WAIT_FOR_SYNC
 | 
			
		||||
    private void handleSourceFromQrCodeIfExists() {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "handleSourceFromQrCodeIfExists()");
 | 
			
		||||
        }
 | 
			
		||||
        if (mSourceFromQrCode == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mBroadcastIdToPreferenceMap.compute(
 | 
			
		||||
                mSourceFromQrCode.getBroadcastId(),
 | 
			
		||||
                (k, existingPreference) -> {
 | 
			
		||||
                    if (existingPreference == null) {
 | 
			
		||||
                        // No existing preference for this source from the QR code scan, add one and
 | 
			
		||||
                        // set initial state to WAIT_FOR_SYNC.
 | 
			
		||||
                        // Check nullability to bypass NullAway check.
 | 
			
		||||
                        if (mSourceFromQrCode != null) {
 | 
			
		||||
                            return addNewPreference(
 | 
			
		||||
                                    mSourceFromQrCode, AudioStreamState.WAIT_FOR_SYNC);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    Log.w(
 | 
			
		||||
                            TAG,
 | 
			
		||||
                            "handleSourceFromQrCodeIfExists(): unexpected state : "
 | 
			
		||||
                                    + existingPreference.getAudioStreamState()
 | 
			
		||||
                                    + " for broadcastId : "
 | 
			
		||||
                                    + (mSourceFromQrCode == null
 | 
			
		||||
                                            ? "null"
 | 
			
		||||
                                            : mSourceFromQrCode.getBroadcastId()));
 | 
			
		||||
                    return existingPreference;
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void handleSourceLost(int broadcastId) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "handleSourceLost()");
 | 
			
		||||
        }
 | 
			
		||||
        if (mAudioStreamsHelper.getAllConnectedSources().stream()
 | 
			
		||||
                .anyMatch(connected -> connected.getBroadcastId() == broadcastId)) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "handleSourceLost() : keep this preference as the source is still connected.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        var toRemove = mBroadcastIdToPreferenceMap.remove(broadcastId);
 | 
			
		||||
        if (toRemove != null) {
 | 
			
		||||
            ThreadUtils.postOnMainThread(
 | 
			
		||||
                    () -> {
 | 
			
		||||
                        if (mCategoryPreference != null) {
 | 
			
		||||
                            mCategoryPreference.removePreference(toRemove);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void handleSourceRemoved() {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "handleSourceRemoved()");
 | 
			
		||||
        }
 | 
			
		||||
        for (var entry : mBroadcastIdToPreferenceMap.entrySet()) {
 | 
			
		||||
            var preference = entry.getValue();
 | 
			
		||||
 | 
			
		||||
            // Look for preference has SOURCE_ADDED state, re-check if they are still connected. If
 | 
			
		||||
            // not, means the source is removed from the sink, we move back the preference to SYNCED
 | 
			
		||||
            // state.
 | 
			
		||||
            if (preference.getAudioStreamState() == AudioStreamState.SOURCE_ADDED
 | 
			
		||||
                    && mAudioStreamsHelper.getAllConnectedSources().stream()
 | 
			
		||||
                            .noneMatch(
 | 
			
		||||
                                    connected ->
 | 
			
		||||
                                            connected.getBroadcastId()
 | 
			
		||||
                                                    == preference.getAudioStreamBroadcastId())) {
 | 
			
		||||
 | 
			
		||||
                ThreadUtils.postOnMainThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            var metadata = preference.getAudioStreamMetadata();
 | 
			
		||||
 | 
			
		||||
                            if (metadata != null) {
 | 
			
		||||
                                moveToState(preference, AudioStreamState.SYNCED);
 | 
			
		||||
                            } else {
 | 
			
		||||
                                handleSourceLost(preference.getAudioStreamBroadcastId());
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find preference by receiveState and decide next state.
 | 
			
		||||
    // Expect one of the following:
 | 
			
		||||
    // 1) No preference existed, create new preference with state SOURCE_ADDED
 | 
			
		||||
    // 2) Any other state, move to SOURCE_ADDED
 | 
			
		||||
    void handleSourceConnected(BluetoothLeBroadcastReceiveState receiveState) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "handleSourceConnected()");
 | 
			
		||||
        }
 | 
			
		||||
        if (!AudioStreamsHelper.isConnected(receiveState)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        var broadcastIdConnected = receiveState.getBroadcastId();
 | 
			
		||||
        if (mSourceFromQrCode != null && mSourceFromQrCode.getBroadcastId() == UNSET_BROADCAST_ID) {
 | 
			
		||||
            // mSourceFromQrCode could have no broadcast Id, we fill in the broadcast Id from the
 | 
			
		||||
            // connected source receiveState.
 | 
			
		||||
            if (DEBUG) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "handleSourceConnected() : processing mSourceFromQrCode with broadcastId"
 | 
			
		||||
                                + " unset");
 | 
			
		||||
            }
 | 
			
		||||
            boolean updated =
 | 
			
		||||
                    maybeUpdateId(
 | 
			
		||||
                            AudioStreamsHelper.getBroadcastName(receiveState),
 | 
			
		||||
                            receiveState.getBroadcastId());
 | 
			
		||||
            if (updated && mBroadcastIdToPreferenceMap.containsKey(UNSET_BROADCAST_ID)) {
 | 
			
		||||
                var preference = mBroadcastIdToPreferenceMap.remove(UNSET_BROADCAST_ID);
 | 
			
		||||
                mBroadcastIdToPreferenceMap.put(receiveState.getBroadcastId(), preference);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mBroadcastIdToPreferenceMap.compute(
 | 
			
		||||
                broadcastIdConnected,
 | 
			
		||||
                (k, existingPreference) -> {
 | 
			
		||||
                    if (existingPreference == null) {
 | 
			
		||||
                        // No existing preference for this source even if it's already connected,
 | 
			
		||||
                        // add one and set initial state to SOURCE_ADDED. This could happen because
 | 
			
		||||
                        // we retrieves the connected source during onStart() from
 | 
			
		||||
                        // AudioStreamsHelper#getAllConnectedSources() even before the source is
 | 
			
		||||
                        // founded by scanning.
 | 
			
		||||
                        return addNewPreference(receiveState, AudioStreamState.SOURCE_ADDED);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (existingPreference.getAudioStreamState() == AudioStreamState.WAIT_FOR_SYNC
 | 
			
		||||
                            && existingPreference.getAudioStreamBroadcastId() == UNSET_BROADCAST_ID
 | 
			
		||||
                            && mSourceFromQrCode != null) {
 | 
			
		||||
                        existingPreference.setAudioStreamMetadata(mSourceFromQrCode);
 | 
			
		||||
                    }
 | 
			
		||||
                    moveToState(existingPreference, AudioStreamState.SOURCE_ADDED);
 | 
			
		||||
                    return existingPreference;
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find preference by receiveState and decide next state.
 | 
			
		||||
    // Expect one preference existed, move to ADD_SOURCE_BAD_CODE
 | 
			
		||||
    void handleSourceConnectBadCode(BluetoothLeBroadcastReceiveState receiveState) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "handleSourceConnectBadCode()");
 | 
			
		||||
        }
 | 
			
		||||
        if (!AudioStreamsHelper.isBadCode(receiveState)) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mBroadcastIdToPreferenceMap.computeIfPresent(
 | 
			
		||||
                receiveState.getBroadcastId(),
 | 
			
		||||
                (k, existingPreference) -> {
 | 
			
		||||
                    moveToState(existingPreference, AudioStreamState.ADD_SOURCE_BAD_CODE);
 | 
			
		||||
                    return existingPreference;
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find preference by broadcastId and decide next state.
 | 
			
		||||
    // Expect one preference existed, move to ADD_SOURCE_FAILED
 | 
			
		||||
    void handleSourceFailedToConnect(int broadcastId) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "handleSourceFailedToConnect()");
 | 
			
		||||
        }
 | 
			
		||||
        mBroadcastIdToPreferenceMap.computeIfPresent(
 | 
			
		||||
                broadcastId,
 | 
			
		||||
                (k, existingPreference) -> {
 | 
			
		||||
                    moveToState(existingPreference, AudioStreamState.ADD_SOURCE_FAILED);
 | 
			
		||||
                    return existingPreference;
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find preference by metadata and decide next state.
 | 
			
		||||
    // Expect one preference existed, move to ADD_SOURCE_WAIT_FOR_RESPONSE
 | 
			
		||||
    void handleSourceAddRequest(
 | 
			
		||||
            AudioStreamPreference preference, BluetoothLeBroadcastMetadata metadata) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "handleSourceAddRequest()");
 | 
			
		||||
        }
 | 
			
		||||
        mBroadcastIdToPreferenceMap.computeIfPresent(
 | 
			
		||||
                metadata.getBroadcastId(),
 | 
			
		||||
                (k, existingPreference) -> {
 | 
			
		||||
                    if (!existingPreference.equals(preference)) {
 | 
			
		||||
                        Log.w(TAG, "handleSourceAddedRequest(): existing preference not match");
 | 
			
		||||
                    }
 | 
			
		||||
                    existingPreference.setAudioStreamMetadata(metadata);
 | 
			
		||||
                    moveToState(existingPreference, AudioStreamState.ADD_SOURCE_WAIT_FOR_RESPONSE);
 | 
			
		||||
                    return existingPreference;
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void showToast(String msg) {
 | 
			
		||||
        AudioSharingUtils.toastMessage(mContext, msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void init() {
 | 
			
		||||
        mBroadcastIdToPreferenceMap.clear();
 | 
			
		||||
        boolean hasConnected =
 | 
			
		||||
                AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(mBluetoothManager)
 | 
			
		||||
                        .isPresent();
 | 
			
		||||
        AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                mContext,
 | 
			
		||||
                () -> {
 | 
			
		||||
                    if (mCategoryPreference != null) {
 | 
			
		||||
                        mCategoryPreference.removeAudioStreamPreferences();
 | 
			
		||||
                        mCategoryPreference.setVisible(hasConnected);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        if (hasConnected) {
 | 
			
		||||
            startScanning();
 | 
			
		||||
            AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                    mContext,
 | 
			
		||||
                    () -> {
 | 
			
		||||
                        if (mFragment != null) {
 | 
			
		||||
                            AudioStreamsDialogFragment.dismissAll(mFragment);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
        } else {
 | 
			
		||||
            stopScanning();
 | 
			
		||||
            AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                    mContext,
 | 
			
		||||
                    () -> {
 | 
			
		||||
                        if (mFragment != null) {
 | 
			
		||||
                            AudioStreamsDialogFragment.show(mFragment, getNoLeDeviceDialog());
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void startScanning() {
 | 
			
		||||
        if (mLeBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "startScanning(): LeBroadcastAssistant is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mLeBroadcastAssistant.isSearchInProgress()) {
 | 
			
		||||
            Log.w(TAG, "startScanning(): scanning still in progress, stop scanning first.");
 | 
			
		||||
            stopScanning();
 | 
			
		||||
        }
 | 
			
		||||
        mLeBroadcastAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
 | 
			
		||||
        mExecutor.execute(
 | 
			
		||||
                () -> {
 | 
			
		||||
                    // Handle QR code scan, display currently connected streams then start scanning
 | 
			
		||||
                    // sequentially
 | 
			
		||||
                    handleSourceFromQrCodeIfExists();
 | 
			
		||||
                    mAudioStreamsHelper
 | 
			
		||||
                            .getAllConnectedSources()
 | 
			
		||||
                            .forEach(this::handleSourceConnected);
 | 
			
		||||
                    mLeBroadcastAssistant.startSearchingForSources(emptyList());
 | 
			
		||||
                    mMediaControlHelper.start();
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void stopScanning() {
 | 
			
		||||
        if (mLeBroadcastAssistant == null) {
 | 
			
		||||
            Log.w(TAG, "stopScanning(): LeBroadcastAssistant is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (mLeBroadcastAssistant.isSearchInProgress()) {
 | 
			
		||||
            if (DEBUG) {
 | 
			
		||||
                Log.d(TAG, "stopScanning()");
 | 
			
		||||
            }
 | 
			
		||||
            mLeBroadcastAssistant.stopSearchingForSources();
 | 
			
		||||
            mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
 | 
			
		||||
        }
 | 
			
		||||
        mMediaControlHelper.stop();
 | 
			
		||||
        mSourceFromQrCode = null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private AudioStreamPreference addNewPreference(
 | 
			
		||||
            BluetoothLeBroadcastReceiveState receiveState, AudioStreamState state) {
 | 
			
		||||
        var preference = AudioStreamPreference.fromReceiveState(mContext, receiveState);
 | 
			
		||||
        moveToState(preference, state);
 | 
			
		||||
        return preference;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private AudioStreamPreference addNewPreference(
 | 
			
		||||
            BluetoothLeBroadcastMetadata metadata, AudioStreamState state) {
 | 
			
		||||
        var preference = AudioStreamPreference.fromMetadata(mContext, metadata);
 | 
			
		||||
        moveToState(preference, state);
 | 
			
		||||
        return preference;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void moveToState(AudioStreamPreference preference, AudioStreamState state) {
 | 
			
		||||
        AudioStreamStateHandler stateHandler = switch (state) {
 | 
			
		||||
            case SYNCED -> SyncedState.getInstance();
 | 
			
		||||
            case WAIT_FOR_SYNC -> WaitForSyncState.getInstance();
 | 
			
		||||
            case ADD_SOURCE_WAIT_FOR_RESPONSE ->
 | 
			
		||||
                    AddSourceWaitForResponseState.getInstance();
 | 
			
		||||
            case ADD_SOURCE_BAD_CODE -> AddSourceBadCodeState.getInstance();
 | 
			
		||||
            case ADD_SOURCE_FAILED -> AddSourceFailedState.getInstance();
 | 
			
		||||
            case SOURCE_ADDED -> SourceAddedState.getInstance();
 | 
			
		||||
            default -> throw new IllegalArgumentException("Unsupported state: " + state);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        stateHandler.handleStateChange(preference, this, mAudioStreamsHelper);
 | 
			
		||||
 | 
			
		||||
        // Update UI with the updated preference
 | 
			
		||||
        AudioSharingUtils.postOnMainThread(
 | 
			
		||||
                mContext,
 | 
			
		||||
                () -> {
 | 
			
		||||
                    if (mCategoryPreference != null) {
 | 
			
		||||
                        mCategoryPreference.addAudioStreamPreference(preference, mComparator);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private AudioStreamsDialogFragment.DialogBuilder getNoLeDeviceDialog() {
 | 
			
		||||
        return new AudioStreamsDialogFragment.DialogBuilder(mContext)
 | 
			
		||||
                .setTitle(mContext.getString(R.string.audio_streams_dialog_no_le_device_title))
 | 
			
		||||
                .setSubTitle2(
 | 
			
		||||
                        mContext.getString(R.string.audio_streams_dialog_no_le_device_subtitle))
 | 
			
		||||
                .setLeftButtonText(mContext.getString(R.string.audio_streams_dialog_close))
 | 
			
		||||
                .setLeftButtonOnClickListener(AlertDialog::dismiss)
 | 
			
		||||
                .setRightButtonText(
 | 
			
		||||
                        mContext.getString(R.string.audio_streams_dialog_no_le_device_button))
 | 
			
		||||
                .setRightButtonOnClickListener(
 | 
			
		||||
                        dialog -> {
 | 
			
		||||
                            mContext.startActivity(
 | 
			
		||||
                                    new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
 | 
			
		||||
                                            .setPackage(mContext.getPackageName()));
 | 
			
		||||
                            dialog.dismiss();
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,89 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.util.AttributeSet;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.ProgressCategory;
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Comparator;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamsProgressCategoryPreference extends ProgressCategory {
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsProgressCategoryPreference(Context context) {
 | 
			
		||||
        super(context);
 | 
			
		||||
        init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsProgressCategoryPreference(Context context, AttributeSet attrs) {
 | 
			
		||||
        super(context, attrs);
 | 
			
		||||
        init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsProgressCategoryPreference(
 | 
			
		||||
            Context context, AttributeSet attrs, int defStyleAttr) {
 | 
			
		||||
        super(context, attrs, defStyleAttr);
 | 
			
		||||
        init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsProgressCategoryPreference(
 | 
			
		||||
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
 | 
			
		||||
        super(context, attrs, defStyleAttr, defStyleRes);
 | 
			
		||||
        init();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void addAudioStreamPreference(
 | 
			
		||||
            @NonNull AudioStreamPreference preference,
 | 
			
		||||
            Comparator<AudioStreamPreference> comparator) {
 | 
			
		||||
        super.addPreference(preference);
 | 
			
		||||
 | 
			
		||||
        List<AudioStreamPreference> preferences = getAllAudioStreamPreferences();
 | 
			
		||||
        preferences.sort(comparator);
 | 
			
		||||
        for (int i = 0; i < preferences.size(); i++) {
 | 
			
		||||
            // setOrder to i + 1, since the order 0 preference should always be the
 | 
			
		||||
            // "audio_streams_scan_qr_code"
 | 
			
		||||
            preferences.get(i).setOrder(i + 1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void removeAudioStreamPreferences() {
 | 
			
		||||
        List<AudioStreamPreference> streams = getAllAudioStreamPreferences();
 | 
			
		||||
        for (var toRemove : streams) {
 | 
			
		||||
            removePreference(toRemove);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private List<AudioStreamPreference> getAllAudioStreamPreferences() {
 | 
			
		||||
        List<AudioStreamPreference> streams = new ArrayList<>();
 | 
			
		||||
        for (int i = 0; i < getPreferenceCount(); i++) {
 | 
			
		||||
            if (getPreference(i) instanceof AudioStreamPreference) {
 | 
			
		||||
                streams.add((AudioStreamPreference) getPreference(i));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return streams;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void init() {
 | 
			
		||||
        setEmptyTextRes(R.string.audio_streams_empty);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,134 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.graphics.Bitmap;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
import android.widget.ImageView;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.core.InstrumentedFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.qrcode.QrCodeGenerator;
 | 
			
		||||
 | 
			
		||||
import com.google.zxing.WriterException;
 | 
			
		||||
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamsQrCodeFragment extends InstrumentedFragment {
 | 
			
		||||
    private static final String TAG = "AudioStreamsQrCodeFragment";
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        // TODO(chelseahao): update metrics id
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final View onCreateView(
 | 
			
		||||
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 | 
			
		||||
        View view = inflater.inflate(R.xml.bluetooth_audio_streams_qr_code, container, false);
 | 
			
		||||
 | 
			
		||||
        BluetoothLeBroadcastMetadata broadcastMetadata = getBroadcastMetadata();
 | 
			
		||||
 | 
			
		||||
        if (broadcastMetadata != null) {
 | 
			
		||||
            Optional<Bitmap> bm = getQrCodeBitmap(broadcastMetadata);
 | 
			
		||||
            if (bm.isEmpty()) {
 | 
			
		||||
                return view;
 | 
			
		||||
            }
 | 
			
		||||
            ((ImageView) view.requireViewById(R.id.qrcode_view)).setImageBitmap(bm.get());
 | 
			
		||||
            if (broadcastMetadata.getBroadcastCode() != null) {
 | 
			
		||||
                String password =
 | 
			
		||||
                        new String(broadcastMetadata.getBroadcastCode(), StandardCharsets.UTF_8);
 | 
			
		||||
                String passwordText =
 | 
			
		||||
                        getContext()
 | 
			
		||||
                                .getString(R.string.audio_streams_qr_code_page_password, password);
 | 
			
		||||
                ((TextView) view.requireViewById(R.id.password)).setText(passwordText);
 | 
			
		||||
            }
 | 
			
		||||
            TextView summaryView = view.requireViewById(android.R.id.summary);
 | 
			
		||||
            String summary =
 | 
			
		||||
                    view.getContext()
 | 
			
		||||
                            .getString(
 | 
			
		||||
                                    R.string.audio_streams_qr_code_page_description,
 | 
			
		||||
                                    broadcastMetadata.getBroadcastName());
 | 
			
		||||
            summaryView.setText(summary);
 | 
			
		||||
        }
 | 
			
		||||
        return view;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Optional<Bitmap> getQrCodeBitmap(@Nullable BluetoothLeBroadcastMetadata metadata) {
 | 
			
		||||
        if (metadata == null) {
 | 
			
		||||
            Log.d(TAG, "onCreateView: broadcastMetadata is empty!");
 | 
			
		||||
            return Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
        String metadataStr = BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(metadata);
 | 
			
		||||
        if (metadataStr.isEmpty()) {
 | 
			
		||||
            Log.d(TAG, "onCreateView: metadataStr is empty!");
 | 
			
		||||
            return Optional.empty();
 | 
			
		||||
        }
 | 
			
		||||
        Log.i(TAG, "onCreateView: metadataStr : " + metadataStr);
 | 
			
		||||
        try {
 | 
			
		||||
            int qrcodeSize =
 | 
			
		||||
                    getContext()
 | 
			
		||||
                            .getResources()
 | 
			
		||||
                            .getDimensionPixelSize(R.dimen.audio_streams_qrcode_size);
 | 
			
		||||
            Bitmap bitmap = QrCodeGenerator.encodeQrCode(metadataStr, qrcodeSize);
 | 
			
		||||
            return Optional.of(bitmap);
 | 
			
		||||
        } catch (WriterException e) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "onCreateView: broadcastMetadata "
 | 
			
		||||
                            + metadata
 | 
			
		||||
                            + " qrCode generation exception "
 | 
			
		||||
                            + e);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Optional.empty();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Nullable
 | 
			
		||||
    private BluetoothLeBroadcastMetadata getBroadcastMetadata() {
 | 
			
		||||
        LocalBluetoothLeBroadcast localBluetoothLeBroadcast =
 | 
			
		||||
                Utils.getLocalBtManager(getActivity())
 | 
			
		||||
                        .getProfileManager()
 | 
			
		||||
                        .getLeAudioBroadcastProfile();
 | 
			
		||||
        if (localBluetoothLeBroadcast == null) {
 | 
			
		||||
            Log.d(TAG, "getBroadcastMetadataQrCode: localBluetoothLeBroadcast is null!");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        BluetoothLeBroadcastMetadata metadata =
 | 
			
		||||
                localBluetoothLeBroadcast.getLatestBluetoothLeBroadcastMetadata();
 | 
			
		||||
        if (metadata == null) {
 | 
			
		||||
            Log.d(TAG, "getBroadcastMetadataQrCode: metadata is null!");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return metadata;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,159 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.SharedPreferences;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExt;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
 | 
			
		||||
import javax.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
/** Manages the caching and storage of Bluetooth audio stream metadata. */
 | 
			
		||||
public class AudioStreamsRepository {
 | 
			
		||||
 | 
			
		||||
    private static final String TAG = "AudioStreamsRepository";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
 | 
			
		||||
    private static final String PREF_KEY = "bluetooth_audio_stream_pref";
 | 
			
		||||
    private static final String METADATA_KEY = "bluetooth_audio_stream_metadata";
 | 
			
		||||
 | 
			
		||||
    @Nullable private static AudioStreamsRepository sInstance = null;
 | 
			
		||||
 | 
			
		||||
    private AudioStreamsRepository() {}
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the single instance of AudioStreamsRepository.
 | 
			
		||||
     *
 | 
			
		||||
     * @return The AudioStreamsRepository instance.
 | 
			
		||||
     */
 | 
			
		||||
    public static synchronized AudioStreamsRepository getInstance() {
 | 
			
		||||
        if (sInstance == null) {
 | 
			
		||||
            sInstance = new AudioStreamsRepository();
 | 
			
		||||
        }
 | 
			
		||||
        return sInstance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private final ConcurrentHashMap<Integer, BluetoothLeBroadcastMetadata>
 | 
			
		||||
            mBroadcastIdToMetadataCacheMap = new ConcurrentHashMap<>();
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Caches BluetoothLeBroadcastMetadata in a local cache.
 | 
			
		||||
     *
 | 
			
		||||
     * @param metadata The BluetoothLeBroadcastMetadata to be cached.
 | 
			
		||||
     */
 | 
			
		||||
    void cacheMetadata(BluetoothLeBroadcastMetadata metadata) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "cacheMetadata(): broadcastId "
 | 
			
		||||
                            + metadata.getBroadcastId()
 | 
			
		||||
                            + " saved in local cache.");
 | 
			
		||||
        }
 | 
			
		||||
        mBroadcastIdToMetadataCacheMap.put(metadata.getBroadcastId(), metadata);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets cached BluetoothLeBroadcastMetadata by broadcastId.
 | 
			
		||||
     *
 | 
			
		||||
     * @param broadcastId The broadcastId to look up in the cache.
 | 
			
		||||
     * @return The cached BluetoothLeBroadcastMetadata or null if not found.
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    BluetoothLeBroadcastMetadata getCachedMetadata(int broadcastId) {
 | 
			
		||||
        var metadata = mBroadcastIdToMetadataCacheMap.get(broadcastId);
 | 
			
		||||
        if (metadata == null) {
 | 
			
		||||
            Log.w(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "getCachedMetadata(): broadcastId not found in"
 | 
			
		||||
                            + " mBroadcastIdToMetadataCacheMap.");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        return metadata;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves metadata to SharedPreferences asynchronously.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The context.
 | 
			
		||||
     * @param metadata The BluetoothLeBroadcastMetadata to be saved.
 | 
			
		||||
     */
 | 
			
		||||
    void saveMetadata(Context context, BluetoothLeBroadcastMetadata metadata) {
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            SharedPreferences sharedPref =
 | 
			
		||||
                                    context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
 | 
			
		||||
                            if (sharedPref != null) {
 | 
			
		||||
                                SharedPreferences.Editor editor = sharedPref.edit();
 | 
			
		||||
                                editor.putString(
 | 
			
		||||
                                        METADATA_KEY,
 | 
			
		||||
                                        BluetoothLeBroadcastMetadataExt.INSTANCE.toQrCodeString(
 | 
			
		||||
                                                metadata));
 | 
			
		||||
                                editor.apply();
 | 
			
		||||
                                if (DEBUG) {
 | 
			
		||||
                                    Log.d(
 | 
			
		||||
                                            TAG,
 | 
			
		||||
                                            "saveMetadata(): broadcastId "
 | 
			
		||||
                                                    + metadata.getBroadcastId()
 | 
			
		||||
                                                    + " metadata saved in storage.");
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets saved metadata from SharedPreferences.
 | 
			
		||||
     *
 | 
			
		||||
     * @param context The context.
 | 
			
		||||
     * @param broadcastId The broadcastId to retrieve metadata for.
 | 
			
		||||
     * @return The saved BluetoothLeBroadcastMetadata or null if not found.
 | 
			
		||||
     */
 | 
			
		||||
    @Nullable
 | 
			
		||||
    BluetoothLeBroadcastMetadata getSavedMetadata(Context context, int broadcastId) {
 | 
			
		||||
        SharedPreferences sharedPref = context.getSharedPreferences(PREF_KEY, Context.MODE_PRIVATE);
 | 
			
		||||
        if (sharedPref != null) {
 | 
			
		||||
            String savedMetadataStr = sharedPref.getString(METADATA_KEY, null);
 | 
			
		||||
            if (savedMetadataStr == null) {
 | 
			
		||||
                Log.w(TAG, "getSavedMetadata(): savedMetadataStr is null");
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
            var savedMetadata =
 | 
			
		||||
                    BluetoothLeBroadcastMetadataExt.INSTANCE.convertToBroadcastMetadata(
 | 
			
		||||
                            savedMetadataStr);
 | 
			
		||||
            if (savedMetadata == null || savedMetadata.getBroadcastId() != broadcastId) {
 | 
			
		||||
                Log.w(TAG, "getSavedMetadata(): savedMetadata doesn't match broadcast Id.");
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
            if (DEBUG) {
 | 
			
		||||
                Log.d(
 | 
			
		||||
                        TAG,
 | 
			
		||||
                        "getSavedMetadata(): broadcastId "
 | 
			
		||||
                                + savedMetadata.getBroadcastId()
 | 
			
		||||
                                + " metadata found in storage.");
 | 
			
		||||
            }
 | 
			
		||||
            return savedMetadata;
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,140 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.lifecycle.DefaultLifecycleObserver;
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity;
 | 
			
		||||
import com.android.settings.core.BasePreferenceController;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
public class AudioStreamsScanQrCodeController extends BasePreferenceController
 | 
			
		||||
        implements DefaultLifecycleObserver {
 | 
			
		||||
    static final int REQUEST_SCAN_BT_BROADCAST_QR_CODE = 0;
 | 
			
		||||
    private static final String TAG = "AudioStreamsProgressCategoryController";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
    private static final String KEY = "audio_streams_scan_qr_code";
 | 
			
		||||
    private final BluetoothCallback mBluetoothCallback =
 | 
			
		||||
            new BluetoothCallback() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void onActiveDeviceChanged(
 | 
			
		||||
                        @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) {
 | 
			
		||||
                    if (bluetoothProfile == BluetoothProfile.LE_AUDIO) {
 | 
			
		||||
                        updateVisibility();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mLocalBtManager;
 | 
			
		||||
    @Nullable private AudioStreamsDashboardFragment mFragment;
 | 
			
		||||
    @Nullable private Preference mPreference;
 | 
			
		||||
 | 
			
		||||
    public AudioStreamsScanQrCodeController(Context context, String preferenceKey) {
 | 
			
		||||
        super(context, preferenceKey);
 | 
			
		||||
        mLocalBtManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setFragment(AudioStreamsDashboardFragment fragment) {
 | 
			
		||||
        mFragment = fragment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStart(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (mLocalBtManager != null) {
 | 
			
		||||
            mLocalBtManager.getEventManager().registerCallback(mBluetoothCallback);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onStop(@NonNull LifecycleOwner owner) {
 | 
			
		||||
        if (mLocalBtManager != null) {
 | 
			
		||||
            mLocalBtManager.getEventManager().unregisterCallback(mBluetoothCallback);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getAvailabilityStatus() {
 | 
			
		||||
        return AVAILABLE;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String getPreferenceKey() {
 | 
			
		||||
        return KEY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void displayPreference(PreferenceScreen screen) {
 | 
			
		||||
        super.displayPreference(screen);
 | 
			
		||||
        mPreference = screen.findPreference(getPreferenceKey());
 | 
			
		||||
        if (mPreference == null) {
 | 
			
		||||
            Log.w(TAG, "displayPreference() mPreference is null!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        mPreference.setOnPreferenceClickListener(
 | 
			
		||||
                preference -> {
 | 
			
		||||
                    if (mFragment == null) {
 | 
			
		||||
                        Log.w(TAG, "displayPreference() mFragment is null!");
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    if (preference.getKey().equals(KEY)) {
 | 
			
		||||
                        Intent intent = new Intent(mContext, QrCodeScanModeActivity.class);
 | 
			
		||||
                        intent.setAction(
 | 
			
		||||
                                BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER);
 | 
			
		||||
                        mFragment.startActivityForResult(intent, REQUEST_SCAN_BT_BROADCAST_QR_CODE);
 | 
			
		||||
                        if (DEBUG) {
 | 
			
		||||
                            Log.w(TAG, "displayPreference() sent intent : " + intent);
 | 
			
		||||
                        }
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
                    return false;
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void updateVisibility() {
 | 
			
		||||
        var unused =
 | 
			
		||||
                ThreadUtils.postOnBackgroundThread(
 | 
			
		||||
                        () -> {
 | 
			
		||||
                            boolean hasConnectedLe =
 | 
			
		||||
                                    AudioStreamsHelper
 | 
			
		||||
                                            .getCachedBluetoothDeviceInSharingOrLeConnected(
 | 
			
		||||
                                                    mLocalBtManager)
 | 
			
		||||
                                            .isPresent();
 | 
			
		||||
                            ThreadUtils.postOnMainThread(
 | 
			
		||||
                                    () -> {
 | 
			
		||||
                                        if (mPreference != null) {
 | 
			
		||||
                                            mPreference.setVisible(hasConnectedLe);
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,133 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.media.session.MediaController;
 | 
			
		||||
import android.media.session.MediaSessionManager;
 | 
			
		||||
import android.media.session.PlaybackState;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.util.Pair;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.media.BluetoothMediaDevice;
 | 
			
		||||
import com.android.settingslib.media.LocalMediaManager;
 | 
			
		||||
import com.android.settingslib.media.MediaDevice;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
 | 
			
		||||
class MediaControlHelper {
 | 
			
		||||
    private static final String TAG = "MediaControlHelper";
 | 
			
		||||
    private final Context mContext;
 | 
			
		||||
    private final MediaSessionManager mMediaSessionManager;
 | 
			
		||||
    @Nullable private final LocalBluetoothManager mLocalBluetoothManager;
 | 
			
		||||
    private final List<Pair<LocalMediaManager, LocalMediaManager.DeviceCallback>>
 | 
			
		||||
            mLocalMediaManagers = new ArrayList<>();
 | 
			
		||||
 | 
			
		||||
    MediaControlHelper(Context context, @Nullable LocalBluetoothManager localBluetoothManager) {
 | 
			
		||||
        mContext = context;
 | 
			
		||||
        mMediaSessionManager = context.getSystemService(MediaSessionManager.class);
 | 
			
		||||
        mLocalBluetoothManager = localBluetoothManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void start() {
 | 
			
		||||
        if (mLocalBluetoothManager == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        var currentLeDevice =
 | 
			
		||||
                AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
 | 
			
		||||
                        mLocalBluetoothManager);
 | 
			
		||||
        if (currentLeDevice.isEmpty()) {
 | 
			
		||||
            Log.d(TAG, "start() : current LE device is empty!");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) {
 | 
			
		||||
            String packageName = controller.getPackageName();
 | 
			
		||||
 | 
			
		||||
            // We won't stop media created from settings.
 | 
			
		||||
            if (Objects.equals(packageName, mContext.getPackageName())) {
 | 
			
		||||
                Log.d(TAG, "start() : skip package: " + packageName);
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Start scanning and listen to device list update, stop this media if device matched.
 | 
			
		||||
            var localMediaManager = new LocalMediaManager(mContext, packageName);
 | 
			
		||||
            var deviceCallback =
 | 
			
		||||
                    new LocalMediaManager.DeviceCallback() {
 | 
			
		||||
                        public void onDeviceListUpdate(List<MediaDevice> devices) {
 | 
			
		||||
                            if (shouldStopMedia(
 | 
			
		||||
                                    controller,
 | 
			
		||||
                                    currentLeDevice.get(),
 | 
			
		||||
                                    localMediaManager.getCurrentConnectedDevice())) {
 | 
			
		||||
                                Log.d(
 | 
			
		||||
                                        TAG,
 | 
			
		||||
                                        "start() : Stopping media player for package: "
 | 
			
		||||
                                                + controller.getPackageName());
 | 
			
		||||
                                var controls = controller.getTransportControls();
 | 
			
		||||
                                if (controls != null) {
 | 
			
		||||
                                    controls.stop();
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    };
 | 
			
		||||
            localMediaManager.registerCallback(deviceCallback);
 | 
			
		||||
            localMediaManager.startScan();
 | 
			
		||||
            mLocalMediaManagers.add(new Pair<>(localMediaManager, deviceCallback));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void stop() {
 | 
			
		||||
        mLocalMediaManagers.forEach(
 | 
			
		||||
                m -> {
 | 
			
		||||
                    m.first.stopScan();
 | 
			
		||||
                    m.first.unregisterCallback(m.second);
 | 
			
		||||
                });
 | 
			
		||||
        mLocalMediaManagers.clear();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean shouldStopMedia(
 | 
			
		||||
            MediaController controller,
 | 
			
		||||
            CachedBluetoothDevice currentLeDevice,
 | 
			
		||||
            MediaDevice currentMediaDevice) {
 | 
			
		||||
        // We won't stop media if it's already stopped.
 | 
			
		||||
        if (controller.getPlaybackState() != null
 | 
			
		||||
                && controller.getPlaybackState().getState() == PlaybackState.STATE_STOPPED) {
 | 
			
		||||
            Log.d(TAG, "shouldStopMedia() : skip already stopped: " + controller.getPackageName());
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var deviceForMedia =
 | 
			
		||||
                currentMediaDevice instanceof BluetoothMediaDevice
 | 
			
		||||
                        ? (BluetoothMediaDevice) currentMediaDevice
 | 
			
		||||
                        : null;
 | 
			
		||||
        return deviceForMedia != null
 | 
			
		||||
                && hasOverlap(deviceForMedia.getCachedDevice(), currentLeDevice);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static boolean hasOverlap(
 | 
			
		||||
            CachedBluetoothDevice device1, CachedBluetoothDevice device2) {
 | 
			
		||||
        return device1.equals(device2)
 | 
			
		||||
                || device1.getMemberDevice().contains(device2)
 | 
			
		||||
                || device2.getMemberDevice().contains(device1);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,94 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.core.SubSettingLauncher;
 | 
			
		||||
 | 
			
		||||
class SourceAddedState extends AudioStreamStateHandler {
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    static final int AUDIO_STREAM_SOURCE_ADDED_STATE_SUMMARY = R.string.audio_streams_listening_now;
 | 
			
		||||
 | 
			
		||||
    @Nullable private static SourceAddedState sInstance = null;
 | 
			
		||||
 | 
			
		||||
    private SourceAddedState() {}
 | 
			
		||||
 | 
			
		||||
    static SourceAddedState getInstance() {
 | 
			
		||||
        if (sInstance == null) {
 | 
			
		||||
            sInstance = new SourceAddedState();
 | 
			
		||||
        }
 | 
			
		||||
        return sInstance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    void performAction(
 | 
			
		||||
            AudioStreamPreference preference,
 | 
			
		||||
            AudioStreamsProgressCategoryController controller,
 | 
			
		||||
            AudioStreamsHelper helper) {
 | 
			
		||||
        var context = preference.getContext();
 | 
			
		||||
        // Saved connected metadata for user to re-join this broadcast later.
 | 
			
		||||
        var cached =
 | 
			
		||||
                mAudioStreamsRepository.getCachedMetadata(preference.getAudioStreamBroadcastId());
 | 
			
		||||
        if (cached != null) {
 | 
			
		||||
            mAudioStreamsRepository.saveMetadata(context, cached);
 | 
			
		||||
        }
 | 
			
		||||
        helper.startMediaService(
 | 
			
		||||
                context,
 | 
			
		||||
                preference.getAudioStreamBroadcastId(),
 | 
			
		||||
                String.valueOf(preference.getTitle()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    int getSummary() {
 | 
			
		||||
        return AUDIO_STREAM_SOURCE_ADDED_STATE_SUMMARY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    Preference.OnPreferenceClickListener getOnClickListener(
 | 
			
		||||
            AudioStreamsProgressCategoryController controller) {
 | 
			
		||||
        return preference -> {
 | 
			
		||||
            var p = (AudioStreamPreference) preference;
 | 
			
		||||
            Bundle broadcast = new Bundle();
 | 
			
		||||
            broadcast.putString(
 | 
			
		||||
                    AudioStreamDetailsFragment.BROADCAST_NAME_ARG, (String) p.getTitle());
 | 
			
		||||
            broadcast.putInt(
 | 
			
		||||
                    AudioStreamDetailsFragment.BROADCAST_ID_ARG, p.getAudioStreamBroadcastId());
 | 
			
		||||
 | 
			
		||||
            new SubSettingLauncher(p.getContext())
 | 
			
		||||
                    .setTitleText(
 | 
			
		||||
                            p.getContext().getString(R.string.audio_streams_detail_page_title))
 | 
			
		||||
                    .setDestination(AudioStreamDetailsFragment.class.getName())
 | 
			
		||||
                    // TODO(chelseahao): Add logging enum
 | 
			
		||||
                    .setSourceMetricsCategory(SettingsEnums.PAGE_UNKNOWN)
 | 
			
		||||
                    .setArguments(broadcast)
 | 
			
		||||
                    .launch();
 | 
			
		||||
            return true;
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
 | 
			
		||||
        return AudioStreamsProgressCategoryController.AudioStreamState.SOURCE_ADDED;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,114 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import android.app.AlertDialog;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastMetadata;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.widget.EditText;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
 | 
			
		||||
class SyncedState extends AudioStreamStateHandler {
 | 
			
		||||
    private static final String TAG = "SyncedState";
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
    @Nullable private static SyncedState sInstance = null;
 | 
			
		||||
 | 
			
		||||
    SyncedState() {}
 | 
			
		||||
 | 
			
		||||
    static SyncedState getInstance() {
 | 
			
		||||
        if (sInstance == null) {
 | 
			
		||||
            sInstance = new SyncedState();
 | 
			
		||||
        }
 | 
			
		||||
        return sInstance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    Preference.OnPreferenceClickListener getOnClickListener(
 | 
			
		||||
            AudioStreamsProgressCategoryController controller) {
 | 
			
		||||
        return p -> addSourceOrShowDialog(p, controller);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
 | 
			
		||||
        return AudioStreamsProgressCategoryController.AudioStreamState.SYNCED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private boolean addSourceOrShowDialog(
 | 
			
		||||
            Preference preference, AudioStreamsProgressCategoryController controller) {
 | 
			
		||||
        var p = (AudioStreamPreference) preference;
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(
 | 
			
		||||
                    TAG,
 | 
			
		||||
                    "preferenceClicked(): attempt to join broadcast id : "
 | 
			
		||||
                            + p.getAudioStreamBroadcastId());
 | 
			
		||||
        }
 | 
			
		||||
        var source = p.getAudioStreamMetadata();
 | 
			
		||||
        if (source != null) {
 | 
			
		||||
            if (source.isEncrypted()) {
 | 
			
		||||
                ThreadUtils.postOnMainThread(() -> launchPasswordDialog(source, p, controller));
 | 
			
		||||
            } else {
 | 
			
		||||
                controller.handleSourceAddRequest(p, source);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void launchPasswordDialog(
 | 
			
		||||
            BluetoothLeBroadcastMetadata source,
 | 
			
		||||
            AudioStreamPreference preference,
 | 
			
		||||
            AudioStreamsProgressCategoryController controller) {
 | 
			
		||||
        View layout =
 | 
			
		||||
                LayoutInflater.from(preference.getContext())
 | 
			
		||||
                        .inflate(R.layout.bluetooth_find_broadcast_password_dialog, null);
 | 
			
		||||
        ((TextView) layout.requireViewById(R.id.broadcast_name_text))
 | 
			
		||||
                .setText(preference.getTitle());
 | 
			
		||||
        AlertDialog alertDialog =
 | 
			
		||||
                new AlertDialog.Builder(preference.getContext())
 | 
			
		||||
                        .setTitle(R.string.find_broadcast_password_dialog_title)
 | 
			
		||||
                        .setView(layout)
 | 
			
		||||
                        .setNeutralButton(android.R.string.cancel, null)
 | 
			
		||||
                        .setPositiveButton(
 | 
			
		||||
                                R.string.bluetooth_connect_access_dialog_positive,
 | 
			
		||||
                                (dialog, which) -> {
 | 
			
		||||
                                    var code =
 | 
			
		||||
                                            ((EditText)
 | 
			
		||||
                                                            layout.requireViewById(
 | 
			
		||||
                                                                    R.id.broadcast_edit_text))
 | 
			
		||||
                                                    .getText()
 | 
			
		||||
                                                    .toString();
 | 
			
		||||
                                    var metadata =
 | 
			
		||||
                                            new BluetoothLeBroadcastMetadata.Builder(source)
 | 
			
		||||
                                                    .setBroadcastCode(
 | 
			
		||||
                                                            code.getBytes(StandardCharsets.UTF_8))
 | 
			
		||||
                                                    .build();
 | 
			
		||||
                                    controller.handleSourceAddRequest(preference, metadata);
 | 
			
		||||
                                })
 | 
			
		||||
                        .create();
 | 
			
		||||
        alertDialog.show();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,117 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing.audiostreams;
 | 
			
		||||
 | 
			
		||||
import static com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsScanQrCodeController.REQUEST_SCAN_BT_BROADCAST_QR_CODE;
 | 
			
		||||
 | 
			
		||||
import android.app.AlertDialog;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.VisibleForTesting;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.audiostreams.qrcode.QrCodeScanModeActivity;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
 | 
			
		||||
import com.android.settingslib.utils.ThreadUtils;
 | 
			
		||||
 | 
			
		||||
class WaitForSyncState extends AudioStreamStateHandler {
 | 
			
		||||
    @VisibleForTesting
 | 
			
		||||
    static final int AUDIO_STREAM_WAIT_FOR_SYNC_STATE_SUMMARY =
 | 
			
		||||
            R.string.audio_streams_wait_for_sync_state_summary;
 | 
			
		||||
 | 
			
		||||
    @VisibleForTesting static final int WAIT_FOR_SYNC_TIMEOUT_MILLIS = 15000;
 | 
			
		||||
 | 
			
		||||
    @Nullable private static WaitForSyncState sInstance = null;
 | 
			
		||||
 | 
			
		||||
    private WaitForSyncState() {}
 | 
			
		||||
 | 
			
		||||
    static WaitForSyncState getInstance() {
 | 
			
		||||
        if (sInstance == null) {
 | 
			
		||||
            sInstance = new WaitForSyncState();
 | 
			
		||||
        }
 | 
			
		||||
        return sInstance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    void performAction(
 | 
			
		||||
            AudioStreamPreference preference,
 | 
			
		||||
            AudioStreamsProgressCategoryController controller,
 | 
			
		||||
            AudioStreamsHelper helper) {
 | 
			
		||||
        var metadata = preference.getAudioStreamMetadata();
 | 
			
		||||
        if (metadata != null) {
 | 
			
		||||
            mHandler.postDelayed(
 | 
			
		||||
                    () -> {
 | 
			
		||||
                        if (preference.isShown()
 | 
			
		||||
                                && preference.getAudioStreamState() == getStateEnum()) {
 | 
			
		||||
                            controller.handleSourceLost(preference.getAudioStreamBroadcastId());
 | 
			
		||||
                            ThreadUtils.postOnMainThread(
 | 
			
		||||
                                    () -> {
 | 
			
		||||
                                        if (controller.getFragment() != null) {
 | 
			
		||||
                                            AudioStreamsDialogFragment.show(
 | 
			
		||||
                                                    controller.getFragment(),
 | 
			
		||||
                                                    getBroadcastUnavailableDialog(
 | 
			
		||||
                                                            preference.getContext(),
 | 
			
		||||
                                                            AudioStreamsHelper.getBroadcastName(
 | 
			
		||||
                                                                    metadata),
 | 
			
		||||
                                                            controller));
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    WAIT_FOR_SYNC_TIMEOUT_MILLIS);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    int getSummary() {
 | 
			
		||||
        return AUDIO_STREAM_WAIT_FOR_SYNC_STATE_SUMMARY;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    AudioStreamsProgressCategoryController.AudioStreamState getStateEnum() {
 | 
			
		||||
        return AudioStreamsProgressCategoryController.AudioStreamState.WAIT_FOR_SYNC;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private AudioStreamsDialogFragment.DialogBuilder getBroadcastUnavailableDialog(
 | 
			
		||||
            Context context,
 | 
			
		||||
            String broadcastName,
 | 
			
		||||
            AudioStreamsProgressCategoryController controller) {
 | 
			
		||||
        return new AudioStreamsDialogFragment.DialogBuilder(context)
 | 
			
		||||
                .setTitle(context.getString(R.string.audio_streams_dialog_stream_is_not_available))
 | 
			
		||||
                .setSubTitle1(broadcastName)
 | 
			
		||||
                .setSubTitle2(context.getString(R.string.audio_streams_is_not_playing))
 | 
			
		||||
                .setLeftButtonText(context.getString(R.string.audio_streams_dialog_close))
 | 
			
		||||
                .setLeftButtonOnClickListener(AlertDialog::dismiss)
 | 
			
		||||
                .setRightButtonText(context.getString(R.string.audio_streams_dialog_retry))
 | 
			
		||||
                .setRightButtonOnClickListener(
 | 
			
		||||
                        dialog -> {
 | 
			
		||||
                            if (controller.getFragment() != null) {
 | 
			
		||||
                                Intent intent = new Intent(context, QrCodeScanModeActivity.class);
 | 
			
		||||
                                intent.setAction(
 | 
			
		||||
                                        BluetoothBroadcastUtils
 | 
			
		||||
                                                .ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER);
 | 
			
		||||
                                controller
 | 
			
		||||
                                        .getFragment()
 | 
			
		||||
                                        .startActivityForResult(
 | 
			
		||||
                                                intent, REQUEST_SCAN_BT_BROADCAST_QR_CODE);
 | 
			
		||||
                                dialog.dismiss();
 | 
			
		||||
                            }
 | 
			
		||||
                        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,117 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams.qrcode;
 | 
			
		||||
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
 | 
			
		||||
import androidx.fragment.app.FragmentTransaction;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Finding a broadcast through QR code.
 | 
			
		||||
 *
 | 
			
		||||
 * <p>To use intent action {@link
 | 
			
		||||
 * BluetoothBroadcastUtils#ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER}, specify the bluetooth device
 | 
			
		||||
 * sink of the broadcast to be provisioned in {@link
 | 
			
		||||
 * BluetoothBroadcastUtils#EXTRA_BLUETOOTH_DEVICE_SINK} and check the operation for all coordinated
 | 
			
		||||
 * set members throughout one session or not by {@link
 | 
			
		||||
 * BluetoothBroadcastUtils#EXTRA_BLUETOOTH_SINK_IS_GROUP}.
 | 
			
		||||
 */
 | 
			
		||||
public class QrCodeScanModeActivity extends QrCodeScanModeBaseActivity {
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
    private static final String TAG = "QrCodeScanModeActivity";
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void handleIntent(Intent intent) {
 | 
			
		||||
        if (!AudioSharingUtils.isFeatureEnabled()) {
 | 
			
		||||
            finish();
 | 
			
		||||
        }
 | 
			
		||||
        String action = intent != null ? intent.getAction() : null;
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "handleIntent(), action = " + action);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (action == null) {
 | 
			
		||||
            finish();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        switch (action) {
 | 
			
		||||
            case BluetoothBroadcastUtils.ACTION_BLUETOOTH_LE_AUDIO_QR_CODE_SCANNER:
 | 
			
		||||
                showQrCodeScannerFragment(intent);
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                if (DEBUG) {
 | 
			
		||||
                    Log.e(TAG, "Launch with an invalid action");
 | 
			
		||||
                }
 | 
			
		||||
                finish();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void showQrCodeScannerFragment(Intent intent) {
 | 
			
		||||
        if (intent == null) {
 | 
			
		||||
            if (DEBUG) {
 | 
			
		||||
                Log.d(TAG, "intent is null, can not get bluetooth information from intent.");
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "showQrCodeScannerFragment");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "get extra from intent");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        QrCodeScanModeFragment fragment =
 | 
			
		||||
                (QrCodeScanModeFragment)
 | 
			
		||||
                        mFragmentManager.findFragmentByTag(
 | 
			
		||||
                                BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
 | 
			
		||||
 | 
			
		||||
        if (fragment == null) {
 | 
			
		||||
            fragment = new QrCodeScanModeFragment();
 | 
			
		||||
        } else {
 | 
			
		||||
            if (fragment.isVisible()) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // When the fragment in back stack but not on top of the stack, we can simply pop
 | 
			
		||||
            // stack because current fragment transactions are arranged in an order
 | 
			
		||||
            mFragmentManager.popBackStackImmediate();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        final FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
 | 
			
		||||
 | 
			
		||||
        fragmentTransaction.replace(
 | 
			
		||||
                R.id.fragment_container,
 | 
			
		||||
                fragment,
 | 
			
		||||
                BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
 | 
			
		||||
        fragmentTransaction.commit();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,64 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams.qrcode;
 | 
			
		||||
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.os.SystemProperties;
 | 
			
		||||
 | 
			
		||||
import androidx.fragment.app.FragmentManager;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settingslib.core.lifecycle.ObservableActivity;
 | 
			
		||||
 | 
			
		||||
import com.google.android.setupdesign.util.ThemeHelper;
 | 
			
		||||
import com.google.android.setupdesign.util.ThemeResolver;
 | 
			
		||||
 | 
			
		||||
public abstract class QrCodeScanModeBaseActivity extends ObservableActivity {
 | 
			
		||||
 | 
			
		||||
    private static final String THEME_KEY = "setupwizard.theme";
 | 
			
		||||
    private static final String THEME_DEFAULT_VALUE = "SudThemeGlifV3_DayNight";
 | 
			
		||||
    protected FragmentManager mFragmentManager;
 | 
			
		||||
 | 
			
		||||
    protected abstract void handleIntent(Intent intent);
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    protected void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
 | 
			
		||||
        int defaultTheme =
 | 
			
		||||
                ThemeHelper.isSetupWizardDayNightEnabled(this)
 | 
			
		||||
                        ? com.google.android.setupdesign.R.style.SudThemeGlifV3_DayNight
 | 
			
		||||
                        : com.google.android.setupdesign.R.style.SudThemeGlifV3_Light;
 | 
			
		||||
        ThemeResolver themeResolver =
 | 
			
		||||
                new ThemeResolver.Builder(ThemeResolver.getDefault())
 | 
			
		||||
                        .setDefaultTheme(defaultTheme)
 | 
			
		||||
                        .setUseDayNight(true)
 | 
			
		||||
                        .build();
 | 
			
		||||
        setTheme(
 | 
			
		||||
                themeResolver.resolve(
 | 
			
		||||
                        SystemProperties.get(THEME_KEY, THEME_DEFAULT_VALUE),
 | 
			
		||||
                        /* suppressDayNight= */ !ThemeHelper.isSetupWizardDayNightEnabled(this)));
 | 
			
		||||
 | 
			
		||||
        setContentView(R.layout.qrcode_scan_mode_activity);
 | 
			
		||||
        mFragmentManager = getSupportFragmentManager();
 | 
			
		||||
 | 
			
		||||
        if (savedInstanceState == null) {
 | 
			
		||||
            handleIntent(getIntent());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,283 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.connecteddevice.audiosharing.audiostreams.qrcode;
 | 
			
		||||
 | 
			
		||||
import android.app.Activity;
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.Intent;
 | 
			
		||||
import android.graphics.Matrix;
 | 
			
		||||
import android.graphics.Outline;
 | 
			
		||||
import android.graphics.Rect;
 | 
			
		||||
import android.graphics.SurfaceTexture;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.os.Handler;
 | 
			
		||||
import android.os.Message;
 | 
			
		||||
import android.os.VibrationEffect;
 | 
			
		||||
import android.os.Vibrator;
 | 
			
		||||
import android.util.Log;
 | 
			
		||||
import android.util.Size;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.TextureView;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
import android.view.ViewOutlineProvider;
 | 
			
		||||
import android.view.accessibility.AccessibilityEvent;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
import androidx.annotation.Nullable;
 | 
			
		||||
import androidx.annotation.StringRes;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsHelper;
 | 
			
		||||
import com.android.settings.core.InstrumentedFragment;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.qrcode.QrCamera;
 | 
			
		||||
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
 | 
			
		||||
public class QrCodeScanModeFragment extends InstrumentedFragment
 | 
			
		||||
        implements TextureView.SurfaceTextureListener, QrCamera.ScannerCallback {
 | 
			
		||||
    private static final boolean DEBUG = BluetoothUtils.D;
 | 
			
		||||
    private static final String TAG = "QrCodeScanModeFragment";
 | 
			
		||||
 | 
			
		||||
    /** Message sent to hide error message */
 | 
			
		||||
    private static final int MESSAGE_HIDE_ERROR_MESSAGE = 1;
 | 
			
		||||
 | 
			
		||||
    /** Message sent to show error message */
 | 
			
		||||
    private static final int MESSAGE_SHOW_ERROR_MESSAGE = 2;
 | 
			
		||||
 | 
			
		||||
    /** Message sent to broadcast QR code */
 | 
			
		||||
    private static final int MESSAGE_SCAN_BROADCAST_SUCCESS = 3;
 | 
			
		||||
 | 
			
		||||
    private static final long SHOW_ERROR_MESSAGE_INTERVAL = 10000;
 | 
			
		||||
    private static final long SHOW_SUCCESS_SQUARE_INTERVAL = 1000;
 | 
			
		||||
 | 
			
		||||
    private static final Duration VIBRATE_DURATION_QR_CODE_RECOGNITION = Duration.ofMillis(3);
 | 
			
		||||
 | 
			
		||||
    public static final String KEY_BROADCAST_METADATA = "key_broadcast_metadata";
 | 
			
		||||
 | 
			
		||||
    private LocalBluetoothManager mLocalBluetoothManager;
 | 
			
		||||
    private int mCornerRadius;
 | 
			
		||||
    @Nullable private String mBroadcastMetadata;
 | 
			
		||||
    private Context mContext;
 | 
			
		||||
    @Nullable private QrCamera mCamera;
 | 
			
		||||
    private TextureView mTextureView;
 | 
			
		||||
    private TextView mSummary;
 | 
			
		||||
    private TextView mErrorMessage;
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onCreate(Bundle savedInstanceState) {
 | 
			
		||||
        super.onCreate(savedInstanceState);
 | 
			
		||||
        mContext = getContext();
 | 
			
		||||
        mLocalBluetoothManager = Utils.getLocalBluetoothManager(mContext);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public final View onCreateView(
 | 
			
		||||
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 | 
			
		||||
        return inflater.inflate(
 | 
			
		||||
                R.layout.qrcode_scanner_fragment, container, /* attachToRoot */ false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onViewCreated(View view, Bundle savedInstanceState) {
 | 
			
		||||
        mTextureView = view.findViewById(R.id.preview_view);
 | 
			
		||||
        mCornerRadius =
 | 
			
		||||
                mContext.getResources()
 | 
			
		||||
                        .getDimensionPixelSize(R.dimen.audio_streams_qrcode_preview_radius);
 | 
			
		||||
        mTextureView.setSurfaceTextureListener(this);
 | 
			
		||||
        mTextureView.setOutlineProvider(
 | 
			
		||||
                new ViewOutlineProvider() {
 | 
			
		||||
                    @Override
 | 
			
		||||
                    public void getOutline(View view, Outline outline) {
 | 
			
		||||
                        outline.setRoundRect(
 | 
			
		||||
                                0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        mTextureView.setClipToOutline(true);
 | 
			
		||||
        mErrorMessage = view.findViewById(R.id.error_message);
 | 
			
		||||
 | 
			
		||||
        var device =
 | 
			
		||||
                AudioStreamsHelper.getCachedBluetoothDeviceInSharingOrLeConnected(
 | 
			
		||||
                        mLocalBluetoothManager);
 | 
			
		||||
        mSummary = view.findViewById(android.R.id.summary);
 | 
			
		||||
        if (mSummary != null && device.isPresent()) {
 | 
			
		||||
            mSummary.setText(
 | 
			
		||||
                    getString(
 | 
			
		||||
                            R.string.audio_streams_main_page_qr_code_scanner_summary,
 | 
			
		||||
                            device.get().getName()));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void initCamera(SurfaceTexture surface) {
 | 
			
		||||
        // Check if the camera has already created.
 | 
			
		||||
        if (mCamera == null) {
 | 
			
		||||
            mCamera = new QrCamera(mContext, this);
 | 
			
		||||
            mCamera.start(surface);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void destroyCamera() {
 | 
			
		||||
        if (mCamera != null) {
 | 
			
		||||
            mCamera.stop();
 | 
			
		||||
            mCamera = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
 | 
			
		||||
        initCamera(surface);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSurfaceTextureSizeChanged(
 | 
			
		||||
            @NonNull SurfaceTexture surface, int width, int height) {}
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
 | 
			
		||||
        destroyCamera();
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {}
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handleSuccessfulResult(String qrCode) {
 | 
			
		||||
        if (DEBUG) {
 | 
			
		||||
            Log.d(TAG, "handleSuccessfulResult(), get the qr code string.");
 | 
			
		||||
        }
 | 
			
		||||
        mBroadcastMetadata = qrCode;
 | 
			
		||||
        handleBtLeAudioScanner();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void handleCameraFailure() {
 | 
			
		||||
        destroyCamera();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Size getViewSize() {
 | 
			
		||||
        return new Size(mTextureView.getWidth(), mTextureView.getHeight());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Rect getFramePosition(Size previewSize, int cameraOrientation) {
 | 
			
		||||
        return new Rect(0, 0, previewSize.getHeight(), previewSize.getHeight());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void setTransform(Matrix transform) {
 | 
			
		||||
        mTextureView.setTransform(transform);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isValid(String qrCode) {
 | 
			
		||||
        if (qrCode.startsWith(BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA)) {
 | 
			
		||||
            return true;
 | 
			
		||||
        } else {
 | 
			
		||||
            showErrorMessage(R.string.audio_streams_qr_code_is_not_valid_format);
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected boolean isDecodeTaskAlive() {
 | 
			
		||||
        return mCamera != null && mCamera.isDecodeTaskAlive();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private final Handler mHandler =
 | 
			
		||||
            new Handler() {
 | 
			
		||||
                @Override
 | 
			
		||||
                public void handleMessage(Message msg) {
 | 
			
		||||
                    switch (msg.what) {
 | 
			
		||||
                        case MESSAGE_HIDE_ERROR_MESSAGE:
 | 
			
		||||
                            mErrorMessage.setVisibility(View.INVISIBLE);
 | 
			
		||||
                            break;
 | 
			
		||||
 | 
			
		||||
                        case MESSAGE_SHOW_ERROR_MESSAGE:
 | 
			
		||||
                            final String errorMessage = (String) msg.obj;
 | 
			
		||||
 | 
			
		||||
                            mErrorMessage.setVisibility(View.VISIBLE);
 | 
			
		||||
                            mErrorMessage.setText(errorMessage);
 | 
			
		||||
                            mErrorMessage.sendAccessibilityEvent(
 | 
			
		||||
                                    AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
 | 
			
		||||
 | 
			
		||||
                            // Cancel any pending messages to hide error view and requeue the
 | 
			
		||||
                            // message so
 | 
			
		||||
                            // user has time to see error
 | 
			
		||||
                            removeMessages(MESSAGE_HIDE_ERROR_MESSAGE);
 | 
			
		||||
                            sendEmptyMessageDelayed(
 | 
			
		||||
                                    MESSAGE_HIDE_ERROR_MESSAGE, SHOW_ERROR_MESSAGE_INTERVAL);
 | 
			
		||||
                            break;
 | 
			
		||||
 | 
			
		||||
                        case MESSAGE_SCAN_BROADCAST_SUCCESS:
 | 
			
		||||
                            Log.d(TAG, "scan success");
 | 
			
		||||
                            final Intent resultIntent = new Intent();
 | 
			
		||||
                            resultIntent.putExtra(KEY_BROADCAST_METADATA, mBroadcastMetadata);
 | 
			
		||||
                            getActivity().setResult(Activity.RESULT_OK, resultIntent);
 | 
			
		||||
                            notifyUserForQrCodeRecognition();
 | 
			
		||||
                            break;
 | 
			
		||||
                        default:
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
    private void notifyUserForQrCodeRecognition() {
 | 
			
		||||
        if (mCamera != null) {
 | 
			
		||||
            mCamera.stop();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        mErrorMessage.setVisibility(View.INVISIBLE);
 | 
			
		||||
        mTextureView.setVisibility(View.INVISIBLE);
 | 
			
		||||
 | 
			
		||||
        triggerVibrationForQrCodeRecognition(getContext());
 | 
			
		||||
 | 
			
		||||
        getActivity().finish();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static void triggerVibrationForQrCodeRecognition(Context context) {
 | 
			
		||||
        Vibrator vibrator = context.getSystemService(Vibrator.class);
 | 
			
		||||
        if (vibrator == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        vibrator.vibrate(
 | 
			
		||||
                VibrationEffect.createOneShot(
 | 
			
		||||
                        VIBRATE_DURATION_QR_CODE_RECOGNITION.toMillis(),
 | 
			
		||||
                        VibrationEffect.DEFAULT_AMPLITUDE));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void showErrorMessage(@StringRes int messageResId) {
 | 
			
		||||
        final Message message =
 | 
			
		||||
                mHandler.obtainMessage(MESSAGE_SHOW_ERROR_MESSAGE, getString(messageResId));
 | 
			
		||||
        message.sendToTarget();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void handleBtLeAudioScanner() {
 | 
			
		||||
        Message message = mHandler.obtainMessage(MESSAGE_SCAN_BROADCAST_SUCCESS);
 | 
			
		||||
        mHandler.sendMessageDelayed(message, SHOW_SUCCESS_SQUARE_INTERVAL);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public int getMetricsCategory() {
 | 
			
		||||
        return SettingsEnums.LE_AUDIO_BROADCAST_SCAN_QR_CODE;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -24,7 +24,6 @@ import com.android.settings.biometrics.face.FaceFeatureProvider
 | 
			
		||||
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider
 | 
			
		||||
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothFeatureProvider
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider
 | 
			
		||||
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider
 | 
			
		||||
import com.android.settings.connecteddevice.stylus.StylusFeatureProvider
 | 
			
		||||
import com.android.settings.dashboard.DashboardFeatureProvider
 | 
			
		||||
@@ -184,11 +183,6 @@ abstract class FeatureFactory {
 | 
			
		||||
     */
 | 
			
		||||
    abstract val displayFeatureProvider: DisplayFeatureProvider
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets implementation for audio sharing related feature.
 | 
			
		||||
     */
 | 
			
		||||
    abstract val audioSharingFeatureProvider: AudioSharingFeatureProvider
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets implementation for sync across devices related feature.
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -34,8 +34,6 @@ import com.android.settings.biometrics.fingerprint.FingerprintFeatureProviderImp
 | 
			
		||||
import com.android.settings.biometrics2.factory.BiometricsRepositoryProviderImpl
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothFeatureProvider
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothFeatureProviderImpl
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProviderImpl
 | 
			
		||||
import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl
 | 
			
		||||
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider
 | 
			
		||||
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProviderImpl
 | 
			
		||||
@@ -196,10 +194,6 @@ open class FeatureFactoryImpl : FeatureFactory() {
 | 
			
		||||
        DisplayFeatureProviderImpl()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val audioSharingFeatureProvider: AudioSharingFeatureProvider by lazy {
 | 
			
		||||
        AudioSharingFeatureProviderImpl()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override val syncAcrossDevicesFeatureProvider: SyncAcrossDevicesFeatureProvider by lazy {
 | 
			
		||||
        SyncAcrossDevicesFeatureProviderImpl()
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -24,26 +24,32 @@ import static org.mockito.Mockito.when;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.bluetooth.BluetoothStatusCodes;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.graphics.drawable.Drawable;
 | 
			
		||||
import android.media.AudioManager;
 | 
			
		||||
import android.platform.test.flag.junit.SetFlagsRule;
 | 
			
		||||
import android.util.Pair;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.connecteddevice.DevicePreferenceCallback;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingFeatureProvider;
 | 
			
		||||
import com.android.settings.dashboard.DashboardFragment;
 | 
			
		||||
import com.android.settings.testutils.FakeFeatureFactory;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowAudioManager;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.flags.Flags;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.google.common.collect.ImmutableSet;
 | 
			
		||||
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Rule;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.junit.runner.RunWith;
 | 
			
		||||
import org.mockito.Mock;
 | 
			
		||||
@@ -55,6 +61,7 @@ import org.robolectric.shadow.api.Shadow;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
@RunWith(RobolectricTestRunner.class)
 | 
			
		||||
@Config(
 | 
			
		||||
@@ -66,6 +73,8 @@ import java.util.Collection;
 | 
			
		||||
public class AvailableMediaBluetoothDeviceUpdaterTest {
 | 
			
		||||
    private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C";
 | 
			
		||||
 | 
			
		||||
    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 | 
			
		||||
 | 
			
		||||
    @Mock private DashboardFragment mDashboardFragment;
 | 
			
		||||
    @Mock private DevicePreferenceCallback mDevicePreferenceCallback;
 | 
			
		||||
    @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
 | 
			
		||||
@@ -73,6 +82,9 @@ public class AvailableMediaBluetoothDeviceUpdaterTest {
 | 
			
		||||
    @Mock private Drawable mDrawable;
 | 
			
		||||
    @Mock private LocalBluetoothManager mLocalBtManager;
 | 
			
		||||
    @Mock private CachedBluetoothDeviceManager mCachedDeviceManager;
 | 
			
		||||
    @Mock private LocalBluetoothProfileManager mProfileManager;
 | 
			
		||||
    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
 | 
			
		||||
    @Mock private BluetoothLeBroadcastReceiveState mBroadcastReceiveState;
 | 
			
		||||
 | 
			
		||||
    private Context mContext;
 | 
			
		||||
    private AvailableMediaBluetoothDeviceUpdater mBluetoothDeviceUpdater;
 | 
			
		||||
@@ -80,20 +92,24 @@ public class AvailableMediaBluetoothDeviceUpdaterTest {
 | 
			
		||||
    private AudioManager mAudioManager;
 | 
			
		||||
    private BluetoothDevicePreference mPreference;
 | 
			
		||||
    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
 | 
			
		||||
    private AudioSharingFeatureProvider mFeatureProvider;
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void setUp() {
 | 
			
		||||
        MockitoAnnotations.initMocks(this);
 | 
			
		||||
 | 
			
		||||
        mContext = RuntimeEnvironment.application;
 | 
			
		||||
        mFeatureProvider = FakeFeatureFactory.setupForTest().getAudioSharingFeatureProvider();
 | 
			
		||||
        mAudioManager = mContext.getSystemService(AudioManager.class);
 | 
			
		||||
        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
 | 
			
		||||
        mLocalBtManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
        when(mProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
 | 
			
		||||
        when(mLocalBtManager.getProfileManager()).thenReturn(mProfileManager);
 | 
			
		||||
        when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
 | 
			
		||||
        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
 | 
			
		||||
        mShadowBluetoothAdapter.setEnabled(true);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_SUPPORTED);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_SUPPORTED);
 | 
			
		||||
        mCachedDevices = new ArrayList<>();
 | 
			
		||||
        mCachedDevices.add(mCachedBluetoothDevice);
 | 
			
		||||
        when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
 | 
			
		||||
@@ -252,14 +268,16 @@ public class AvailableMediaBluetoothDeviceUpdaterTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void
 | 
			
		||||
            onProfileConnectionStateChanged_leaDeviceConnected_notInCallNoSharing_addsPreference() {
 | 
			
		||||
            onProfileConnectionStateChanged_leaConnected_notInCallSharingFlagOff_addsPreference() {
 | 
			
		||||
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mAudioManager.setMode(AudioManager.MODE_NORMAL);
 | 
			
		||||
        when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
 | 
			
		||||
                .thenReturn(true);
 | 
			
		||||
        when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
 | 
			
		||||
        when(mFeatureProvider.isAudioSharingFilterMatched(
 | 
			
		||||
                        any(CachedBluetoothDevice.class), any(LocalBluetoothManager.class)))
 | 
			
		||||
                .thenReturn(false);
 | 
			
		||||
        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mBroadcastReceiveState));
 | 
			
		||||
        List<Long> bisSyncState = new ArrayList<>();
 | 
			
		||||
        bisSyncState.add(1L);
 | 
			
		||||
        when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
 | 
			
		||||
 | 
			
		||||
        mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
@@ -271,14 +289,50 @@ public class AvailableMediaBluetoothDeviceUpdaterTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void
 | 
			
		||||
            onProfileConnectionStateChanged_leaDeviceConnected_inCallNoSharing_addsPreference() {
 | 
			
		||||
            onProfileConnectionStateChanged_leaConnected_notInCallNotInSharing_addsPreference() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mAudioManager.setMode(AudioManager.MODE_NORMAL);
 | 
			
		||||
        when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
 | 
			
		||||
                .thenReturn(true);
 | 
			
		||||
        when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
 | 
			
		||||
        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
 | 
			
		||||
 | 
			
		||||
        mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
                BluetoothProfile.STATE_CONNECTED,
 | 
			
		||||
                BluetoothProfile.LE_AUDIO);
 | 
			
		||||
 | 
			
		||||
        verify(mBluetoothDeviceUpdater).addPreference(mCachedBluetoothDevice);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onProfileConnectionStateChanged_leaConnected_inCallSharingFlagOff_addsPreference() {
 | 
			
		||||
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mAudioManager.setMode(AudioManager.MODE_IN_CALL);
 | 
			
		||||
        when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
 | 
			
		||||
                .thenReturn(true);
 | 
			
		||||
        when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
 | 
			
		||||
        when(mFeatureProvider.isAudioSharingFilterMatched(
 | 
			
		||||
                        any(CachedBluetoothDevice.class), any(LocalBluetoothManager.class)))
 | 
			
		||||
                .thenReturn(false);
 | 
			
		||||
        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mBroadcastReceiveState));
 | 
			
		||||
        List<Long> bisSyncState = new ArrayList<>();
 | 
			
		||||
        bisSyncState.add(1L);
 | 
			
		||||
        when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
 | 
			
		||||
 | 
			
		||||
        mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
                BluetoothProfile.STATE_CONNECTED,
 | 
			
		||||
                BluetoothProfile.LE_AUDIO);
 | 
			
		||||
 | 
			
		||||
        verify(mBluetoothDeviceUpdater).addPreference(mCachedBluetoothDevice);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onProfileConnectionStateChanged_leaConnected_inCallNotInSharing_addsPreference() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mAudioManager.setMode(AudioManager.MODE_IN_CALL);
 | 
			
		||||
        when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
 | 
			
		||||
                .thenReturn(true);
 | 
			
		||||
        when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
 | 
			
		||||
        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of());
 | 
			
		||||
 | 
			
		||||
        mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
@@ -291,14 +345,16 @@ public class AvailableMediaBluetoothDeviceUpdaterTest {
 | 
			
		||||
    @Test
 | 
			
		||||
    public void
 | 
			
		||||
            onProfileConnectionStateChanged_leaDeviceConnected_notInCallInSharing_removesPref() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mAudioManager.setMode(AudioManager.MODE_NORMAL);
 | 
			
		||||
        when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
 | 
			
		||||
                .thenReturn(true);
 | 
			
		||||
        when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
 | 
			
		||||
        when(mCachedBluetoothDevice.isConnectedA2dpDevice()).thenReturn(true);
 | 
			
		||||
        when(mFeatureProvider.isAudioSharingFilterMatched(
 | 
			
		||||
                        any(CachedBluetoothDevice.class), any(LocalBluetoothManager.class)))
 | 
			
		||||
                .thenReturn(true);
 | 
			
		||||
        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mBroadcastReceiveState));
 | 
			
		||||
        List<Long> bisSyncState = new ArrayList<>();
 | 
			
		||||
        bisSyncState.add(1L);
 | 
			
		||||
        when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
 | 
			
		||||
 | 
			
		||||
        mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
@@ -310,14 +366,16 @@ public class AvailableMediaBluetoothDeviceUpdaterTest {
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onProfileConnectionStateChanged_leaDeviceConnected_inCallInSharing_removesPref() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mAudioManager.setMode(AudioManager.MODE_NORMAL);
 | 
			
		||||
        when(mBluetoothDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class)))
 | 
			
		||||
                .thenReturn(true);
 | 
			
		||||
        when(mCachedBluetoothDevice.isConnectedLeAudioDevice()).thenReturn(true);
 | 
			
		||||
        when(mCachedBluetoothDevice.isConnectedHfpDevice()).thenReturn(true);
 | 
			
		||||
        when(mFeatureProvider.isAudioSharingFilterMatched(
 | 
			
		||||
                        any(CachedBluetoothDevice.class), any(LocalBluetoothManager.class)))
 | 
			
		||||
                .thenReturn(true);
 | 
			
		||||
        when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mBroadcastReceiveState));
 | 
			
		||||
        List<Long> bisSyncState = new ArrayList<>();
 | 
			
		||||
        bisSyncState.add(1L);
 | 
			
		||||
        when(mBroadcastReceiveState.getBisSyncState()).thenReturn(bisSyncState);
 | 
			
		||||
 | 
			
		||||
        mBluetoothDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
 
 | 
			
		||||
@@ -22,18 +22,24 @@ import static com.google.common.truth.Truth.assertThat;
 | 
			
		||||
 | 
			
		||||
import static org.mockito.ArgumentMatchers.any;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.anyString;
 | 
			
		||||
import static org.mockito.Mockito.doNothing;
 | 
			
		||||
import static org.mockito.Mockito.doReturn;
 | 
			
		||||
import static org.mockito.Mockito.spy;
 | 
			
		||||
import static org.mockito.Mockito.times;
 | 
			
		||||
import static org.mockito.Mockito.verify;
 | 
			
		||||
import static org.mockito.Mockito.when;
 | 
			
		||||
import static org.robolectric.shadows.ShadowLooper.shadowMainLooper;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastAssistant;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.bluetooth.BluetoothStatusCodes;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.content.pm.PackageManager;
 | 
			
		||||
import android.graphics.drawable.Drawable;
 | 
			
		||||
import android.media.AudioManager;
 | 
			
		||||
import android.platform.test.flag.junit.SetFlagsRule;
 | 
			
		||||
import android.util.Pair;
 | 
			
		||||
 | 
			
		||||
import androidx.appcompat.app.AlertDialog;
 | 
			
		||||
@@ -48,16 +54,21 @@ import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.AvailableMediaBluetoothDeviceUpdater;
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothDevicePreference;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.audiosharing.AudioSharingDialogHandler;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowAudioManager;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothCallback;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothEventManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.HearingAidInfo;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.core.lifecycle.Lifecycle;
 | 
			
		||||
import com.android.settingslib.flags.Flags;
 | 
			
		||||
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Rule;
 | 
			
		||||
@@ -71,17 +82,22 @@ import org.robolectric.Robolectric;
 | 
			
		||||
import org.robolectric.RobolectricTestRunner;
 | 
			
		||||
import org.robolectric.RuntimeEnvironment;
 | 
			
		||||
import org.robolectric.annotation.Config;
 | 
			
		||||
import org.robolectric.shadow.api.Shadow;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
 | 
			
		||||
/** Tests for {@link AvailableMediaDeviceGroupController}. */
 | 
			
		||||
@RunWith(RobolectricTestRunner.class)
 | 
			
		||||
@Config(
 | 
			
		||||
        shadows = {
 | 
			
		||||
            ShadowAudioManager.class,
 | 
			
		||||
            ShadowBluetoothAdapter.class,
 | 
			
		||||
            ShadowBluetoothUtils.class,
 | 
			
		||||
            ShadowAlertDialogCompat.class,
 | 
			
		||||
        })
 | 
			
		||||
public class AvailableMediaDeviceGroupControllerTest {
 | 
			
		||||
    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 | 
			
		||||
    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 | 
			
		||||
 | 
			
		||||
    private static final String TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1";
 | 
			
		||||
    private static final String PREFERENCE_KEY_1 = "pref_key_1";
 | 
			
		||||
@@ -96,17 +112,20 @@ public class AvailableMediaDeviceGroupControllerTest {
 | 
			
		||||
    @Mock private PackageManager mPackageManager;
 | 
			
		||||
    @Mock private BluetoothEventManager mEventManager;
 | 
			
		||||
    @Mock private LocalBluetoothManager mLocalBluetoothManager;
 | 
			
		||||
    @Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
 | 
			
		||||
    @Mock private CachedBluetoothDeviceManager mCachedDeviceManager;
 | 
			
		||||
    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
 | 
			
		||||
    @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
 | 
			
		||||
    @Mock private BluetoothDevice mDevice;
 | 
			
		||||
    @Mock
 | 
			
		||||
    private Drawable mDrawable;
 | 
			
		||||
    @Mock private Drawable mDrawable;
 | 
			
		||||
    @Mock private AudioSharingDialogHandler mDialogHandler;
 | 
			
		||||
 | 
			
		||||
    private PreferenceGroup mPreferenceGroup;
 | 
			
		||||
    private Context mContext;
 | 
			
		||||
    private Preference mPreference;
 | 
			
		||||
    private AvailableMediaDeviceGroupController mAvailableMediaDeviceGroupController;
 | 
			
		||||
    private AudioManager mAudioManager;
 | 
			
		||||
    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
 | 
			
		||||
    private LifecycleOwner mLifecycleOwner;
 | 
			
		||||
    private Lifecycle mLifecycle;
 | 
			
		||||
 | 
			
		||||
@@ -123,19 +142,27 @@ public class AvailableMediaDeviceGroupControllerTest {
 | 
			
		||||
        doReturn(mPackageManager).when(mContext).getPackageManager();
 | 
			
		||||
        doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
 | 
			
		||||
 | 
			
		||||
        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
 | 
			
		||||
        mShadowBluetoothAdapter.setEnabled(true);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_NOT_SUPPORTED);
 | 
			
		||||
        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager;
 | 
			
		||||
        mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
        mAudioManager = mContext.getSystemService(AudioManager.class);
 | 
			
		||||
        doReturn(mEventManager).when(mLocalBluetoothManager).getEventManager();
 | 
			
		||||
        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
 | 
			
		||||
        when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
 | 
			
		||||
        when(mCachedDeviceManager.findDevice(any(BluetoothDevice.class)))
 | 
			
		||||
                .thenReturn(mCachedBluetoothDevice);
 | 
			
		||||
        when(mCachedBluetoothDevice.getAddress()).thenReturn(TEST_DEVICE_ADDRESS);
 | 
			
		||||
 | 
			
		||||
        mAvailableMediaDeviceGroupController =
 | 
			
		||||
                spy(new AvailableMediaDeviceGroupController(mContext, null, mLifecycle));
 | 
			
		||||
                spy(new AvailableMediaDeviceGroupController(mContext));
 | 
			
		||||
        mAvailableMediaDeviceGroupController.setBluetoothDeviceUpdater(
 | 
			
		||||
                mAvailableMediaBluetoothDeviceUpdater);
 | 
			
		||||
        mAvailableMediaDeviceGroupController.setDialogHandler(mDialogHandler);
 | 
			
		||||
        mAvailableMediaDeviceGroupController.setFragmentManager(
 | 
			
		||||
                mActivity.getSupportFragmentManager());
 | 
			
		||||
        mAvailableMediaDeviceGroupController.mPreferenceGroup = mPreferenceGroup;
 | 
			
		||||
@@ -181,23 +208,58 @@ public class AvailableMediaDeviceGroupControllerTest {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testRegister() {
 | 
			
		||||
    public void testRegister_audioSharingOff() {
 | 
			
		||||
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        // register the callback in onStart()
 | 
			
		||||
        mAvailableMediaDeviceGroupController.onStart(mLifecycleOwner);
 | 
			
		||||
 | 
			
		||||
        verify(mAvailableMediaBluetoothDeviceUpdater).registerCallback();
 | 
			
		||||
        verify(mLocalBluetoothManager.getEventManager())
 | 
			
		||||
                .registerCallback(any(BluetoothCallback.class));
 | 
			
		||||
        verify(mEventManager).registerCallback(any(BluetoothCallback.class));
 | 
			
		||||
        verify(mAvailableMediaBluetoothDeviceUpdater).refreshPreference();
 | 
			
		||||
        verify(mAssistant, times(0))
 | 
			
		||||
                .registerServiceCallBack(
 | 
			
		||||
                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
 | 
			
		||||
        verify(mDialogHandler, times(0)).registerCallbacks(any(Executor.class));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testUnregister() {
 | 
			
		||||
    public void testRegister_audioSharingOn() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        setUpBroadcast();
 | 
			
		||||
        // register the callback in onStart()
 | 
			
		||||
        mAvailableMediaDeviceGroupController.onStart(mLifecycleOwner);
 | 
			
		||||
        verify(mAvailableMediaBluetoothDeviceUpdater).registerCallback();
 | 
			
		||||
        verify(mEventManager).registerCallback(any(BluetoothCallback.class));
 | 
			
		||||
        verify(mAvailableMediaBluetoothDeviceUpdater).refreshPreference();
 | 
			
		||||
        verify(mAssistant)
 | 
			
		||||
                .registerServiceCallBack(
 | 
			
		||||
                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
 | 
			
		||||
        verify(mDialogHandler).registerCallbacks(any(Executor.class));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testUnregister_audioSharingOff() {
 | 
			
		||||
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        // unregister the callback in onStop()
 | 
			
		||||
        mAvailableMediaDeviceGroupController.onStop(mLifecycleOwner);
 | 
			
		||||
        verify(mAvailableMediaBluetoothDeviceUpdater).unregisterCallback();
 | 
			
		||||
        verify(mLocalBluetoothManager.getEventManager())
 | 
			
		||||
                .unregisterCallback(any(BluetoothCallback.class));
 | 
			
		||||
        verify(mEventManager).unregisterCallback(any(BluetoothCallback.class));
 | 
			
		||||
        verify(mAssistant, times(0))
 | 
			
		||||
                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
 | 
			
		||||
        verify(mDialogHandler, times(0)).unregisterCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testUnregister_audioSharingOn() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        setUpBroadcast();
 | 
			
		||||
        // unregister the callback in onStop()
 | 
			
		||||
        mAvailableMediaDeviceGroupController.onStop(mLifecycleOwner);
 | 
			
		||||
        verify(mAvailableMediaBluetoothDeviceUpdater).unregisterCallback();
 | 
			
		||||
        verify(mEventManager).unregisterCallback(any(BluetoothCallback.class));
 | 
			
		||||
        verify(mAssistant)
 | 
			
		||||
                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
 | 
			
		||||
        verify(mDialogHandler).unregisterCallbacks();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
@@ -267,7 +329,8 @@ public class AvailableMediaDeviceGroupControllerTest {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onDeviceClick_setActive() {
 | 
			
		||||
    public void onDeviceClick_audioSharingOff_setActive() {
 | 
			
		||||
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        when(mCachedBluetoothDevice.getDevice()).thenReturn(mDevice);
 | 
			
		||||
        Pair<Drawable, String> pair = new Pair<>(mDrawable, TEST_DEVICE_NAME);
 | 
			
		||||
        when(mCachedBluetoothDevice.getDrawableWithDescription()).thenReturn(pair);
 | 
			
		||||
@@ -280,4 +343,37 @@ public class AvailableMediaDeviceGroupControllerTest {
 | 
			
		||||
        mAvailableMediaDeviceGroupController.onDeviceClick(preference);
 | 
			
		||||
        verify(mCachedBluetoothDevice).setActive();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onDeviceClick_audioSharingOn_dialogHandler() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        setUpBroadcast();
 | 
			
		||||
        when(mCachedBluetoothDevice.getDevice()).thenReturn(mDevice);
 | 
			
		||||
        Pair<Drawable, String> pair = new Pair<>(mDrawable, TEST_DEVICE_NAME);
 | 
			
		||||
        when(mCachedBluetoothDevice.getDrawableWithDescription()).thenReturn(pair);
 | 
			
		||||
        BluetoothDevicePreference preference =
 | 
			
		||||
                new BluetoothDevicePreference(
 | 
			
		||||
                        mContext,
 | 
			
		||||
                        mCachedBluetoothDevice,
 | 
			
		||||
                        true,
 | 
			
		||||
                        BluetoothDevicePreference.SortType.TYPE_NO_SORT);
 | 
			
		||||
        mAvailableMediaDeviceGroupController.onDeviceClick(preference);
 | 
			
		||||
        verify(mDialogHandler)
 | 
			
		||||
                .handleDeviceConnected(mCachedBluetoothDevice, /* userTriggered= */ true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void setUpBroadcast() {
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_SUPPORTED);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_SUPPORTED);
 | 
			
		||||
        when(mLocalBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
 | 
			
		||||
        doNothing()
 | 
			
		||||
                .when(mAssistant)
 | 
			
		||||
                .registerServiceCallBack(
 | 
			
		||||
                        any(Executor.class), any(BluetoothLeBroadcastAssistant.Callback.class));
 | 
			
		||||
        doNothing()
 | 
			
		||||
                .when(mAssistant)
 | 
			
		||||
                .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.Callback.class));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -72,6 +72,9 @@ public class ConnectedDeviceDashboardFragmentTest {
 | 
			
		||||
    private static final String KEY_DISCOVERABLE_FOOTER = "discoverable_footer";
 | 
			
		||||
    private static final String KEY_SAVED_DEVICE_SEE_ALL = "previously_connected_devices_see_all";
 | 
			
		||||
    private static final String KEY_FAST_PAIR_DEVICE_SEE_ALL = "fast_pair_devices_see_all";
 | 
			
		||||
    private static final String KEY_AUDIO_SHARING_DEVICES = "audio_sharing_device_list";
 | 
			
		||||
    private static final String KEY_AUDIO_SHARING_SETTINGS =
 | 
			
		||||
            "connected_device_audio_sharing_settings";
 | 
			
		||||
    private static final String KEY_ADD_BT_DEVICES = "add_bt_devices";
 | 
			
		||||
    private static final String SETTINGS_PACKAGE_NAME = "com.android.settings";
 | 
			
		||||
    private static final String SYSTEMUI_PACKAGE_NAME = "com.android.systemui";
 | 
			
		||||
@@ -84,7 +87,6 @@ public class ConnectedDeviceDashboardFragmentTest {
 | 
			
		||||
    private Context mContext;
 | 
			
		||||
    private ConnectedDeviceDashboardFragment mFragment;
 | 
			
		||||
    private FakeFeatureFactory mFeatureFactory;
 | 
			
		||||
    private AvailableMediaDeviceGroupController mMediaDeviceGroupController;
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void setUp() {
 | 
			
		||||
@@ -93,21 +95,13 @@ public class ConnectedDeviceDashboardFragmentTest {
 | 
			
		||||
        mContext = spy(RuntimeEnvironment.application);
 | 
			
		||||
        mFragment = new ConnectedDeviceDashboardFragment();
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_SUBSEQUENT_PAIR_SETTINGS_INTEGRATION);
 | 
			
		||||
        mSetFlagsRule.enableFlags(com.android.settingslib.flags.Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mFeatureFactory = FakeFeatureFactory.setupForTest();
 | 
			
		||||
        when(mFeatureFactory
 | 
			
		||||
                        .getFastPairFeatureProvider()
 | 
			
		||||
                        .getFastPairDeviceUpdater(
 | 
			
		||||
                                any(Context.class), any(DevicePreferenceCallback.class)))
 | 
			
		||||
                .thenReturn(mFastPairDeviceUpdater);
 | 
			
		||||
        when(mFeatureFactory
 | 
			
		||||
                        .getAudioSharingFeatureProvider()
 | 
			
		||||
                        .createAudioSharingDevicePreferenceController(mContext, null, null))
 | 
			
		||||
                .thenReturn(null);
 | 
			
		||||
        mMediaDeviceGroupController = new AvailableMediaDeviceGroupController(mContext, null, null);
 | 
			
		||||
        when(mFeatureFactory
 | 
			
		||||
                        .getAudioSharingFeatureProvider()
 | 
			
		||||
                        .createAvailableMediaDeviceGroupController(mContext, null, null))
 | 
			
		||||
                .thenReturn(mMediaDeviceGroupController);
 | 
			
		||||
        doReturn(mPackageManager).when(mContext).getPackageManager();
 | 
			
		||||
        doReturn(true).when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
 | 
			
		||||
    }
 | 
			
		||||
@@ -135,7 +129,9 @@ public class ConnectedDeviceDashboardFragmentTest {
 | 
			
		||||
                        KEY_NEARBY_DEVICES,
 | 
			
		||||
                        KEY_DISCOVERABLE_FOOTER,
 | 
			
		||||
                        KEY_SAVED_DEVICE_SEE_ALL,
 | 
			
		||||
                        KEY_FAST_PAIR_DEVICE_SEE_ALL);
 | 
			
		||||
                        KEY_FAST_PAIR_DEVICE_SEE_ALL,
 | 
			
		||||
                        KEY_AUDIO_SHARING_DEVICES,
 | 
			
		||||
                        KEY_AUDIO_SHARING_SETTINGS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,88 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import static com.google.common.truth.Truth.assertThat;
 | 
			
		||||
 | 
			
		||||
import static org.mockito.Mockito.spy;
 | 
			
		||||
import static org.mockito.Mockito.times;
 | 
			
		||||
import static org.mockito.Mockito.verify;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.bluetooth.BluetoothStatusCodes;
 | 
			
		||||
import android.os.Bundle;
 | 
			
		||||
import android.platform.test.flag.junit.SetFlagsRule;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 | 
			
		||||
import com.android.settingslib.flags.Flags;
 | 
			
		||||
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Rule;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.junit.runner.RunWith;
 | 
			
		||||
import org.mockito.junit.MockitoJUnit;
 | 
			
		||||
import org.mockito.junit.MockitoRule;
 | 
			
		||||
import org.robolectric.Robolectric;
 | 
			
		||||
import org.robolectric.RobolectricTestRunner;
 | 
			
		||||
import org.robolectric.annotation.Config;
 | 
			
		||||
import org.robolectric.shadow.api.Shadow;
 | 
			
		||||
 | 
			
		||||
@RunWith(RobolectricTestRunner.class)
 | 
			
		||||
@Config(shadows = {ShadowBluetoothAdapter.class})
 | 
			
		||||
public class AudioSharingActivityTest {
 | 
			
		||||
    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 | 
			
		||||
    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 | 
			
		||||
 | 
			
		||||
    private AudioSharingActivity mActivity;
 | 
			
		||||
    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void setUp() {
 | 
			
		||||
        mActivity = spy(Robolectric.buildActivity(AudioSharingActivity.class).get());
 | 
			
		||||
        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
 | 
			
		||||
        mShadowBluetoothAdapter.setEnabled(true);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_SUPPORTED);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_SUPPORTED);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void isValidFragment_returnsTrue() {
 | 
			
		||||
        assertThat(mActivity.isValidFragment(AudioSharingDashboardFragment.class.getName()))
 | 
			
		||||
                .isTrue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void isValidFragment_returnsFalse() {
 | 
			
		||||
        assertThat(mActivity.isValidFragment("")).isFalse();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onCreate_flagOff_finish() {
 | 
			
		||||
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mActivity.onCreate(new Bundle());
 | 
			
		||||
        verify(mActivity).finish();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onCreate_flagOn_create() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mActivity.onCreate(new Bundle());
 | 
			
		||||
        verify(mActivity, times(0)).finish();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,270 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import static com.google.common.truth.Truth.assertThat;
 | 
			
		||||
 | 
			
		||||
import static org.mockito.ArgumentMatchers.any;
 | 
			
		||||
import static org.mockito.Mockito.doNothing;
 | 
			
		||||
import static org.mockito.Mockito.doReturn;
 | 
			
		||||
import static org.mockito.Mockito.spy;
 | 
			
		||||
import static org.mockito.Mockito.verify;
 | 
			
		||||
import static org.mockito.Mockito.when;
 | 
			
		||||
import static org.robolectric.Shadows.shadowOf;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.bluetooth.BluetoothDevice;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcastReceiveState;
 | 
			
		||||
import android.bluetooth.BluetoothProfile;
 | 
			
		||||
import android.bluetooth.BluetoothStatusCodes;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.graphics.drawable.Drawable;
 | 
			
		||||
import android.os.Looper;
 | 
			
		||||
import android.platform.test.flag.junit.SetFlagsRule;
 | 
			
		||||
import android.util.Pair;
 | 
			
		||||
 | 
			
		||||
import androidx.preference.Preference;
 | 
			
		||||
import androidx.test.core.app.ApplicationProvider;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.bluetooth.BluetoothDevicePreference;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.connecteddevice.DevicePreferenceCallback;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowThreadUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 | 
			
		||||
import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.flags.Flags;
 | 
			
		||||
 | 
			
		||||
import com.google.common.collect.ImmutableList;
 | 
			
		||||
import com.google.common.collect.ImmutableSet;
 | 
			
		||||
 | 
			
		||||
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.junit.MockitoJUnit;
 | 
			
		||||
import org.mockito.junit.MockitoRule;
 | 
			
		||||
import org.robolectric.RobolectricTestRunner;
 | 
			
		||||
import org.robolectric.annotation.Config;
 | 
			
		||||
import org.robolectric.shadow.api.Shadow;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
 | 
			
		||||
@RunWith(RobolectricTestRunner.class)
 | 
			
		||||
@Config(
 | 
			
		||||
        shadows = {
 | 
			
		||||
            ShadowBluetoothAdapter.class,
 | 
			
		||||
            ShadowBluetoothUtils.class,
 | 
			
		||||
            ShadowThreadUtils.class
 | 
			
		||||
        })
 | 
			
		||||
public class AudioSharingBluetoothDeviceUpdaterTest {
 | 
			
		||||
    private static final String MAC_ADDRESS = "04:52:C7:0B:D8:3C";
 | 
			
		||||
    private static final String TEST_DEVICE_NAME = "test";
 | 
			
		||||
    private static final String PREF_KEY = "audio_sharing_bt";
 | 
			
		||||
    private static final String TAG = "AudioSharingBluetoothDeviceUpdater";
 | 
			
		||||
 | 
			
		||||
    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 | 
			
		||||
    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 | 
			
		||||
 | 
			
		||||
    @Mock private DevicePreferenceCallback mDevicePreferenceCallback;
 | 
			
		||||
    @Mock private CachedBluetoothDevice mCachedBluetoothDevice;
 | 
			
		||||
    @Mock private BluetoothDevice mBluetoothDevice;
 | 
			
		||||
    @Mock private Drawable mDrawable;
 | 
			
		||||
    @Mock private LocalBluetoothManager mLocalBtManager;
 | 
			
		||||
    @Mock private CachedBluetoothDeviceManager mCachedDeviceManager;
 | 
			
		||||
    @Mock private LocalBluetoothProfileManager mLocalBtProfileManager;
 | 
			
		||||
    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
 | 
			
		||||
    @Mock private BluetoothLeBroadcastReceiveState mState;
 | 
			
		||||
 | 
			
		||||
    private Context mContext;
 | 
			
		||||
    private AudioSharingBluetoothDeviceUpdater mDeviceUpdater;
 | 
			
		||||
    private Collection<CachedBluetoothDevice> mCachedDevices;
 | 
			
		||||
    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void setUp() {
 | 
			
		||||
        mContext = ApplicationProvider.getApplicationContext();
 | 
			
		||||
        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
 | 
			
		||||
        mShadowBluetoothAdapter.setEnabled(true);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_SUPPORTED);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_SUPPORTED);
 | 
			
		||||
        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
 | 
			
		||||
        mLocalBtManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
        when(mLocalBtManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
 | 
			
		||||
        when(mLocalBtManager.getProfileManager()).thenReturn(mLocalBtProfileManager);
 | 
			
		||||
        when(mLocalBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
 | 
			
		||||
        List<Long> bisSyncState = new ArrayList<>();
 | 
			
		||||
        bisSyncState.add(1L);
 | 
			
		||||
        when(mState.getBisSyncState()).thenReturn(bisSyncState);
 | 
			
		||||
        Pair<Drawable, String> pairs = new Pair<>(mDrawable, TEST_DEVICE_NAME);
 | 
			
		||||
        doReturn(TEST_DEVICE_NAME).when(mCachedBluetoothDevice).getName();
 | 
			
		||||
        doReturn(mBluetoothDevice).when(mCachedBluetoothDevice).getDevice();
 | 
			
		||||
        doReturn(MAC_ADDRESS).when(mCachedBluetoothDevice).getAddress();
 | 
			
		||||
        doReturn(pairs).when(mCachedBluetoothDevice).getDrawableWithDescription();
 | 
			
		||||
        doReturn(ImmutableSet.of()).when(mCachedBluetoothDevice).getMemberDevice();
 | 
			
		||||
        doReturn("").when(mCachedBluetoothDevice).getConnectionSummary();
 | 
			
		||||
        mCachedDevices = new ArrayList<>();
 | 
			
		||||
        mCachedDevices.add(mCachedBluetoothDevice);
 | 
			
		||||
        when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
 | 
			
		||||
        doNothing().when(mDevicePreferenceCallback).onDeviceAdded(any(Preference.class));
 | 
			
		||||
        doNothing().when(mDevicePreferenceCallback).onDeviceRemoved(any(Preference.class));
 | 
			
		||||
        mDeviceUpdater =
 | 
			
		||||
                spy(
 | 
			
		||||
                        new AudioSharingBluetoothDeviceUpdater(
 | 
			
		||||
                                mContext, mDevicePreferenceCallback, /* metricsCategory= */ 0));
 | 
			
		||||
        mDeviceUpdater.setPrefContext(mContext);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onProfileConnectionStateChanged_leaDeviceConnected_flagOff_removesPref() {
 | 
			
		||||
        setupPreferenceMapWithDevice();
 | 
			
		||||
 | 
			
		||||
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
 | 
			
		||||
 | 
			
		||||
        mDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
                BluetoothProfile.STATE_CONNECTED,
 | 
			
		||||
                BluetoothProfile.LE_AUDIO);
 | 
			
		||||
        shadowOf(Looper.getMainLooper()).idle();
 | 
			
		||||
 | 
			
		||||
        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
 | 
			
		||||
        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
 | 
			
		||||
        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
 | 
			
		||||
                .isEqualTo(mCachedBluetoothDevice);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onProfileConnectionStateChanged_leaDeviceConnected_noSource_removesPref() {
 | 
			
		||||
        setupPreferenceMapWithDevice();
 | 
			
		||||
 | 
			
		||||
        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(ImmutableList.of());
 | 
			
		||||
        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
 | 
			
		||||
 | 
			
		||||
        mDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
                BluetoothProfile.STATE_CONNECTED,
 | 
			
		||||
                BluetoothProfile.LE_AUDIO);
 | 
			
		||||
        shadowOf(Looper.getMainLooper()).idle();
 | 
			
		||||
 | 
			
		||||
        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
 | 
			
		||||
        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
 | 
			
		||||
        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
 | 
			
		||||
                .isEqualTo(mCachedBluetoothDevice);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onProfileConnectionStateChanged_deviceIsNotInList_removesPref() {
 | 
			
		||||
        setupPreferenceMapWithDevice();
 | 
			
		||||
 | 
			
		||||
        mCachedDevices.clear();
 | 
			
		||||
        when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mCachedDevices);
 | 
			
		||||
        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
 | 
			
		||||
 | 
			
		||||
        mDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
                BluetoothProfile.STATE_CONNECTED,
 | 
			
		||||
                BluetoothProfile.LE_AUDIO);
 | 
			
		||||
        shadowOf(Looper.getMainLooper()).idle();
 | 
			
		||||
 | 
			
		||||
        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
 | 
			
		||||
        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
 | 
			
		||||
        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
 | 
			
		||||
                .isEqualTo(mCachedBluetoothDevice);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onProfileConnectionStateChanged_leaDeviceDisconnected_removesPref() {
 | 
			
		||||
        setupPreferenceMapWithDevice();
 | 
			
		||||
 | 
			
		||||
        when(mDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(false);
 | 
			
		||||
        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
 | 
			
		||||
 | 
			
		||||
        mDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
                BluetoothProfile.STATE_DISCONNECTED,
 | 
			
		||||
                BluetoothProfile.LE_AUDIO);
 | 
			
		||||
        shadowOf(Looper.getMainLooper()).idle();
 | 
			
		||||
 | 
			
		||||
        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
 | 
			
		||||
        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
 | 
			
		||||
        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
 | 
			
		||||
                .isEqualTo(mCachedBluetoothDevice);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onProfileConnectionStateChanged_leaDeviceDisconnecting_removesPref() {
 | 
			
		||||
        setupPreferenceMapWithDevice();
 | 
			
		||||
        doReturn(false).when(mCachedBluetoothDevice).isConnectedLeAudioDevice();
 | 
			
		||||
        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
 | 
			
		||||
 | 
			
		||||
        mDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
                BluetoothProfile.STATE_CONNECTED,
 | 
			
		||||
                BluetoothProfile.LE_AUDIO);
 | 
			
		||||
        shadowOf(Looper.getMainLooper()).idle();
 | 
			
		||||
 | 
			
		||||
        verify(mDevicePreferenceCallback).onDeviceRemoved(captor.capture());
 | 
			
		||||
        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
 | 
			
		||||
        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
 | 
			
		||||
                .isEqualTo(mCachedBluetoothDevice);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onProfileConnectionStateChanged_leaDeviceConnected_hasSource_addsPreference() {
 | 
			
		||||
        ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
 | 
			
		||||
        setupPreferenceMapWithDevice();
 | 
			
		||||
 | 
			
		||||
        verify(mDevicePreferenceCallback).onDeviceAdded(captor.capture());
 | 
			
		||||
        assertThat(captor.getValue() instanceof BluetoothDevicePreference).isTrue();
 | 
			
		||||
        assertThat(((BluetoothDevicePreference) captor.getValue()).getBluetoothDevice())
 | 
			
		||||
                .isEqualTo(mCachedBluetoothDevice);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getLogTag_returnsCorrectTag() {
 | 
			
		||||
        assertThat(mDeviceUpdater.getLogTag()).isEqualTo(TAG);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getPreferenceKey_returnsCorrectKey() {
 | 
			
		||||
        assertThat(mDeviceUpdater.getPreferenceKey()).isEqualTo(PREF_KEY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void setupPreferenceMapWithDevice() {
 | 
			
		||||
        // Add device to preferenceMap
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        when(mAssistant.getAllSources(mBluetoothDevice)).thenReturn(ImmutableList.of(mState));
 | 
			
		||||
        when(mDeviceUpdater.isDeviceConnected(any(CachedBluetoothDevice.class))).thenReturn(true);
 | 
			
		||||
        doReturn(true).when(mCachedBluetoothDevice).isConnectedLeAudioDevice();
 | 
			
		||||
        mDeviceUpdater.onProfileConnectionStateChanged(
 | 
			
		||||
                mCachedBluetoothDevice,
 | 
			
		||||
                BluetoothProfile.STATE_CONNECTED,
 | 
			
		||||
                BluetoothProfile.LE_AUDIO);
 | 
			
		||||
        shadowOf(Looper.getMainLooper()).idle();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,271 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
 | 
			
		||||
import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE;
 | 
			
		||||
 | 
			
		||||
import static com.google.common.truth.Truth.assertThat;
 | 
			
		||||
 | 
			
		||||
import static org.mockito.ArgumentMatchers.any;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.anyBoolean;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.eq;
 | 
			
		||||
import static org.mockito.Mockito.doNothing;
 | 
			
		||||
import static org.mockito.Mockito.times;
 | 
			
		||||
import static org.mockito.Mockito.verify;
 | 
			
		||||
import static org.mockito.Mockito.when;
 | 
			
		||||
import static org.robolectric.Shadows.shadowOf;
 | 
			
		||||
 | 
			
		||||
import android.bluetooth.BluetoothAdapter;
 | 
			
		||||
import android.bluetooth.BluetoothLeBroadcast;
 | 
			
		||||
import android.bluetooth.BluetoothStatusCodes;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import android.os.Looper;
 | 
			
		||||
import android.platform.test.flag.junit.SetFlagsRule;
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.LifecycleOwner;
 | 
			
		||||
import androidx.preference.PreferenceScreen;
 | 
			
		||||
import androidx.preference.TwoStatePreference;
 | 
			
		||||
import androidx.test.core.app.ApplicationProvider;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
import com.android.settings.bluetooth.Utils;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowBluetoothAdapter;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowBluetoothUtils;
 | 
			
		||||
import com.android.settings.testutils.shadow.ShadowThreadUtils;
 | 
			
		||||
import com.android.settingslib.bluetooth.BluetoothEventManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 | 
			
		||||
import com.android.settingslib.bluetooth.VolumeControlProfile;
 | 
			
		||||
import com.android.settingslib.core.lifecycle.Lifecycle;
 | 
			
		||||
import com.android.settingslib.flags.Flags;
 | 
			
		||||
 | 
			
		||||
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.robolectric.RobolectricTestRunner;
 | 
			
		||||
import org.robolectric.annotation.Config;
 | 
			
		||||
import org.robolectric.shadow.api.Shadow;
 | 
			
		||||
 | 
			
		||||
import java.util.concurrent.Executor;
 | 
			
		||||
 | 
			
		||||
@RunWith(RobolectricTestRunner.class)
 | 
			
		||||
@Config(
 | 
			
		||||
        shadows = {
 | 
			
		||||
            ShadowBluetoothAdapter.class,
 | 
			
		||||
            ShadowBluetoothUtils.class,
 | 
			
		||||
            ShadowThreadUtils.class,
 | 
			
		||||
        })
 | 
			
		||||
public class AudioSharingCompatibilityPreferenceControllerTest {
 | 
			
		||||
    private static final String PREF_KEY = "audio_sharing_stream_compatibility";
 | 
			
		||||
 | 
			
		||||
    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 | 
			
		||||
    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
 | 
			
		||||
    @Spy Context mContext = ApplicationProvider.getApplicationContext();
 | 
			
		||||
    @Mock private PreferenceScreen mScreen;
 | 
			
		||||
    @Mock private LocalBluetoothManager mLocalBtManager;
 | 
			
		||||
    @Mock private BluetoothEventManager mBtEventManager;
 | 
			
		||||
    @Mock private LocalBluetoothProfileManager mBtProfileManager;
 | 
			
		||||
    @Mock private LocalBluetoothLeBroadcast mBroadcast;
 | 
			
		||||
    @Mock private LocalBluetoothLeBroadcastAssistant mAssistant;
 | 
			
		||||
    @Mock private VolumeControlProfile mVolumeControl;
 | 
			
		||||
    @Mock private TwoStatePreference mPreference;
 | 
			
		||||
    private AudioSharingCompatibilityPreferenceController mController;
 | 
			
		||||
    private ShadowBluetoothAdapter mShadowBluetoothAdapter;
 | 
			
		||||
    private LocalBluetoothManager mLocalBluetoothManager;
 | 
			
		||||
    private Lifecycle mLifecycle;
 | 
			
		||||
    private LifecycleOwner mLifecycleOwner;
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void setUp() {
 | 
			
		||||
        mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter());
 | 
			
		||||
        mShadowBluetoothAdapter.setEnabled(true);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_SUPPORTED);
 | 
			
		||||
        mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported(
 | 
			
		||||
                BluetoothStatusCodes.FEATURE_SUPPORTED);
 | 
			
		||||
        mLifecycleOwner = () -> mLifecycle;
 | 
			
		||||
        mLifecycle = new Lifecycle(mLifecycleOwner);
 | 
			
		||||
        ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager;
 | 
			
		||||
        mLocalBluetoothManager = Utils.getLocalBtManager(mContext);
 | 
			
		||||
        when(mLocalBluetoothManager.getEventManager()).thenReturn(mBtEventManager);
 | 
			
		||||
        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mBtProfileManager);
 | 
			
		||||
        when(mBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
 | 
			
		||||
        when(mBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant);
 | 
			
		||||
        when(mBtProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControl);
 | 
			
		||||
        when(mBroadcast.isProfileReady()).thenReturn(true);
 | 
			
		||||
        when(mAssistant.isProfileReady()).thenReturn(true);
 | 
			
		||||
        when(mVolumeControl.isProfileReady()).thenReturn(true);
 | 
			
		||||
        mController = new AudioSharingCompatibilityPreferenceController(mContext, PREF_KEY);
 | 
			
		||||
        when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onStart_flagOn_registerCallback() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mController.onStart(mLifecycleOwner);
 | 
			
		||||
        verify(mBroadcast)
 | 
			
		||||
                .registerServiceCallBack(
 | 
			
		||||
                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
 | 
			
		||||
        verify(mBtProfileManager, times(0)).addServiceListener(mController);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onStart_flagOnProfileNotReady_registerProfileCallback() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        when(mBroadcast.isProfileReady()).thenReturn(false);
 | 
			
		||||
        mController.onStart(mLifecycleOwner);
 | 
			
		||||
        verify(mBroadcast, times(0))
 | 
			
		||||
                .registerServiceCallBack(
 | 
			
		||||
                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
 | 
			
		||||
        verify(mBtProfileManager).addServiceListener(mController);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onStart_flagOff_doNothing() {
 | 
			
		||||
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mController.onStart(mLifecycleOwner);
 | 
			
		||||
        verify(mBroadcast, times(0))
 | 
			
		||||
                .registerServiceCallBack(
 | 
			
		||||
                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onStop_flagOn_unregisterCallback() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mController.setCallbacksRegistered(true);
 | 
			
		||||
        mController.onStop(mLifecycleOwner);
 | 
			
		||||
        verify(mBroadcast).unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
 | 
			
		||||
        verify(mBtProfileManager).removeServiceListener(mController);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onStop_flagOff_doNothing() {
 | 
			
		||||
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        mController.setCallbacksRegistered(true);
 | 
			
		||||
        mController.onStop(mLifecycleOwner);
 | 
			
		||||
        verify(mBroadcast, times(0))
 | 
			
		||||
                .unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class));
 | 
			
		||||
        verify(mBtProfileManager, times(0)).removeServiceListener(mController);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void onServiceConnected_updateSwitch() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        when(mBroadcast.isEnabled(null)).thenReturn(false);
 | 
			
		||||
        when(mBroadcast.isProfileReady()).thenReturn(false);
 | 
			
		||||
        mController.displayPreference(mScreen);
 | 
			
		||||
        shadowOf(Looper.getMainLooper()).idle();
 | 
			
		||||
        verify(mPreference).setEnabled(true);
 | 
			
		||||
 | 
			
		||||
        when(mBroadcast.isEnabled(null)).thenReturn(true);
 | 
			
		||||
        when(mBroadcast.isProfileReady()).thenReturn(true);
 | 
			
		||||
        mController.onServiceConnected();
 | 
			
		||||
        shadowOf(Looper.getMainLooper()).idle();
 | 
			
		||||
 | 
			
		||||
        verify(mBroadcast)
 | 
			
		||||
                .registerServiceCallBack(
 | 
			
		||||
                        any(Executor.class), any(BluetoothLeBroadcast.Callback.class));
 | 
			
		||||
        verify(mBtProfileManager).removeServiceListener(mController);
 | 
			
		||||
        verify(mPreference).setEnabled(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getAvailabilityStatus_flagOn() {
 | 
			
		||||
        mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getAvailabilityStatus_flagOff() {
 | 
			
		||||
        mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING);
 | 
			
		||||
        assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getPreferenceKey_returnsCorrectKey() {
 | 
			
		||||
        assertThat(mController.getPreferenceKey()).isEqualTo(PREF_KEY);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getSliceHighlightMenuRes_returnsZero() {
 | 
			
		||||
        assertThat(mController.getSliceHighlightMenuRes()).isEqualTo(0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void displayPreference_broadcastOn_Disabled() {
 | 
			
		||||
        when(mBroadcast.isEnabled(any())).thenReturn(true);
 | 
			
		||||
        mController.displayPreference(mScreen);
 | 
			
		||||
        shadowOf(Looper.getMainLooper()).idle();
 | 
			
		||||
        verify(mPreference).setEnabled(false);
 | 
			
		||||
        verify(mPreference)
 | 
			
		||||
                .setSummary(
 | 
			
		||||
                        eq(mContext.getString(
 | 
			
		||||
                                R.string
 | 
			
		||||
                                        .audio_sharing_stream_compatibility_disabled_description)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void displayPreference_broadcastOff_Enabled() {
 | 
			
		||||
        when(mBroadcast.isEnabled(any())).thenReturn(false);
 | 
			
		||||
        mController.displayPreference(mScreen);
 | 
			
		||||
        shadowOf(Looper.getMainLooper()).idle();
 | 
			
		||||
        verify(mPreference).setEnabled(true);
 | 
			
		||||
        verify(mPreference)
 | 
			
		||||
                .setSummary(
 | 
			
		||||
                        eq(mContext.getString(
 | 
			
		||||
                                R.string.audio_sharing_stream_compatibility_description)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void isChecked_returnsTrue() {
 | 
			
		||||
        when(mBroadcast.getImproveCompatibility()).thenReturn(true);
 | 
			
		||||
        assertThat(mController.isChecked()).isTrue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void isChecked_returnsFalse() {
 | 
			
		||||
        when(mBroadcast.getImproveCompatibility()).thenReturn(false);
 | 
			
		||||
        assertThat(mController.isChecked()).isFalse();
 | 
			
		||||
        mBroadcast = null;
 | 
			
		||||
        assertThat(mController.isChecked()).isFalse();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void setCheckedToNewValue_returnsTrue() {
 | 
			
		||||
        when(mBroadcast.getImproveCompatibility()).thenReturn(true);
 | 
			
		||||
        doNothing().when(mBroadcast).setImproveCompatibility(anyBoolean());
 | 
			
		||||
        boolean setChecked = mController.setChecked(false);
 | 
			
		||||
        verify(mBroadcast).setImproveCompatibility(false);
 | 
			
		||||
        assertThat(setChecked).isTrue();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void setCheckedToCurrentValue_returnsFalse() {
 | 
			
		||||
        when(mBroadcast.getImproveCompatibility()).thenReturn(true);
 | 
			
		||||
        boolean setChecked = mController.setChecked(true);
 | 
			
		||||
        verify(mBroadcast, times(0)).setImproveCompatibility(anyBoolean());
 | 
			
		||||
        assertThat(setChecked).isFalse();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,65 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2024 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.connecteddevice.audiosharing;
 | 
			
		||||
 | 
			
		||||
import static com.google.common.truth.Truth.assertThat;
 | 
			
		||||
 | 
			
		||||
import android.app.settings.SettingsEnums;
 | 
			
		||||
 | 
			
		||||
import com.android.settings.R;
 | 
			
		||||
 | 
			
		||||
import org.junit.Before;
 | 
			
		||||
import org.junit.Rule;
 | 
			
		||||
import org.junit.Test;
 | 
			
		||||
import org.junit.runner.RunWith;
 | 
			
		||||
import org.mockito.junit.MockitoJUnit;
 | 
			
		||||
import org.mockito.junit.MockitoRule;
 | 
			
		||||
import org.robolectric.RobolectricTestRunner;
 | 
			
		||||
 | 
			
		||||
@RunWith(RobolectricTestRunner.class)
 | 
			
		||||
public class AudioSharingDashboardFragmentTest {
 | 
			
		||||
 | 
			
		||||
    @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 | 
			
		||||
    private AudioSharingDashboardFragment mFragment;
 | 
			
		||||
 | 
			
		||||
    @Before
 | 
			
		||||
    public void setUp() {
 | 
			
		||||
        mFragment = new AudioSharingDashboardFragment();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getPreferenceScreenResId_returnsCorrectXml() {
 | 
			
		||||
        assertThat(mFragment.getPreferenceScreenResId())
 | 
			
		||||
                .isEqualTo(R.xml.bluetooth_le_audio_sharing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getLogTag_returnsCorrectTag() {
 | 
			
		||||
        assertThat(mFragment.getLogTag()).isEqualTo("AudioSharingDashboardFrag");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getMetricsCategory_returnsCorrectCategory() {
 | 
			
		||||
        assertThat(mFragment.getMetricsCategory()).isEqualTo(SettingsEnums.AUDIO_SHARING_SETTINGS);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void getHelpResource_returnsCorrectResource() {
 | 
			
		||||
        assertThat(mFragment.getHelpResource())
 | 
			
		||||
                .isEqualTo(R.string.help_url_audio_sharing);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user