1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.development; 18 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothStatusCodes; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.provider.Settings; 24 import android.sysprop.BluetoothProperties; 25 import android.util.Log; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 import androidx.annotation.VisibleForTesting; 30 import androidx.preference.Preference; 31 import androidx.preference.SwitchPreferenceCompat; 32 33 import com.android.settings.core.PreferenceControllerMixin; 34 import com.android.settingslib.development.DeveloperOptionsPreferenceController; 35 import com.android.settingslib.flags.Flags; 36 import com.android.settingslib.utils.ThreadUtils; 37 38 /** Preference controller to enable / disable the Bluetooth LE audio sharing UI flow */ 39 public class BluetoothLeAudioUiPreferenceController extends DeveloperOptionsPreferenceController 40 implements Preference.OnPreferenceChangeListener, 41 PreferenceControllerMixin, 42 BluetoothLeAudioModePreferenceController.OnModeChangeListener { 43 private static final String TAG = "BluetoothLeAudioUiPreferenceController"; 44 private static final String PREFERENCE_KEY = "bluetooth_leaudio_broadcast_ui"; 45 46 @VisibleForTesting 47 static final String VALUE_KEY = "bluetooth_le_audio_sharing_ui_preview_enabled"; 48 49 @VisibleForTesting static final int VALUE_OFF = 0; 50 @VisibleForTesting static final int VALUE_ON = 1; 51 @VisibleForTesting static final int VALUE_UNSET = -1; 52 @Nullable private final DevelopmentSettingsDashboardFragment mFragment; 53 private final BluetoothAdapter mBluetoothAdapter; 54 private boolean mCurrentSettingsValue = false; 55 private boolean mShouldToggleCurrentValue = false; 56 BluetoothLeAudioUiPreferenceController( @onNull Context context, @Nullable DevelopmentSettingsDashboardFragment fragment)57 public BluetoothLeAudioUiPreferenceController( 58 @NonNull Context context, @Nullable DevelopmentSettingsDashboardFragment fragment) { 59 super(context); 60 mFragment = fragment; 61 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 62 } 63 64 @Override isAvailable()65 public boolean isAvailable() { 66 return Flags.audioSharingDeveloperOption() 67 && BluetoothProperties.isProfileBapBroadcastSourceEnabled().orElse(false) 68 && BluetoothProperties.isProfileBapBroadcastAssistEnabled().orElse(false); 69 } 70 71 @Override onPreferenceChange(@onNull Preference preference, @Nullable Object newValue)72 public boolean onPreferenceChange(@NonNull Preference preference, @Nullable Object newValue) { 73 if (mFragment != null && newValue != null && (boolean) newValue != mCurrentSettingsValue) { 74 mShouldToggleCurrentValue = true; 75 BluetoothRebootDialog.show(mFragment); 76 } 77 return false; 78 } 79 80 @Override updateState(@onNull Preference preference)81 public void updateState(@NonNull Preference preference) { 82 if (mBluetoothAdapter == null) { 83 return; 84 } 85 var unused = ThreadUtils.postOnBackgroundThread( 86 () -> { 87 boolean shouldEnable = 88 mBluetoothAdapter.isEnabled() 89 && mBluetoothAdapter.isLeAudioBroadcastSourceSupported() 90 == BluetoothStatusCodes.FEATURE_SUPPORTED 91 && mBluetoothAdapter.isLeAudioBroadcastAssistantSupported() 92 == BluetoothStatusCodes.FEATURE_SUPPORTED; 93 boolean valueOn = 94 Settings.Global.getInt( 95 mContext.getContentResolver(), VALUE_KEY, VALUE_UNSET) 96 == VALUE_ON; 97 mContext.getMainExecutor() 98 .execute( 99 () -> { 100 if (!shouldEnable && valueOn) { 101 Log.e( 102 TAG, 103 "Error state: toggle disabled but current" 104 + " settings value is true."); 105 } 106 mCurrentSettingsValue = valueOn; 107 preference.setEnabled(shouldEnable); 108 ((SwitchPreferenceCompat) preference).setChecked(valueOn); 109 }); 110 }); 111 } 112 113 @Override getPreferenceKey()114 public @NonNull String getPreferenceKey() { 115 return PREFERENCE_KEY; 116 } 117 118 /** Called when the RebootDialog confirm is clicked. */ onRebootDialogConfirmed()119 public void onRebootDialogConfirmed() { 120 if (isAvailable() && mShouldToggleCurrentValue) { 121 // Blocking, ensure reboot happens after value is saved. 122 Log.d(TAG, "onRebootDialogConfirmed(): setting value to " + !mCurrentSettingsValue); 123 toggleSetting(mContext.getContentResolver(), !mCurrentSettingsValue); 124 } 125 } 126 127 /** Called when the RebootDialog cancel is clicked. */ onRebootDialogCanceled()128 public void onRebootDialogCanceled() { 129 mShouldToggleCurrentValue = false; 130 } 131 132 @Override onBroadcastDisabled()133 public void onBroadcastDisabled() { 134 if (isAvailable() && mCurrentSettingsValue) { 135 Log.d(TAG, "onBroadcastDisabled(): setting value to false"); 136 // Blocking, ensure reboot happens after value is saved. 137 toggleSetting(mContext.getContentResolver(), false); 138 } 139 } 140 toggleSetting(ContentResolver contentResolver, boolean valueOn)141 private static void toggleSetting(ContentResolver contentResolver, boolean valueOn) { 142 Settings.Global.putInt(contentResolver, VALUE_KEY, valueOn ? VALUE_ON : VALUE_OFF); 143 } 144 } 145