1 /* 2 * Copyright (C) 2025 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.connecteddevice.audiosharing; 18 19 import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE; 20 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothLeBroadcastAssistant; 24 import android.bluetooth.BluetoothLeBroadcastMetadata; 25 import android.bluetooth.BluetoothLeBroadcastReceiveState; 26 import android.bluetooth.BluetoothProfile; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.util.Log; 30 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.annotation.VisibleForTesting; 34 import androidx.annotation.WorkerThread; 35 import androidx.lifecycle.DefaultLifecycleObserver; 36 import androidx.lifecycle.LifecycleOwner; 37 import androidx.preference.PreferenceScreen; 38 39 import com.android.settings.bluetooth.Utils; 40 import com.android.settings.core.BasePreferenceController; 41 import com.android.settings.dashboard.DashboardFragment; 42 import com.android.settingslib.bluetooth.BluetoothCallback; 43 import com.android.settingslib.bluetooth.BluetoothEventManager; 44 import com.android.settingslib.bluetooth.BluetoothUtils; 45 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 46 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 47 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; 48 import com.android.settingslib.bluetooth.LocalBluetoothManager; 49 import com.android.settingslib.flags.Flags; 50 import com.android.settingslib.utils.ThreadUtils; 51 52 import java.util.concurrent.Executor; 53 import java.util.concurrent.Executors; 54 55 public class AudioSharingJoinHandlerController extends BasePreferenceController 56 implements DefaultLifecycleObserver, BluetoothCallback { 57 private static final String TAG = "AudioSharingJoinHandlerCtrl"; 58 private static final String KEY = "audio_sharing_join_handler"; 59 60 @Nullable private final LocalBluetoothManager mBtManager; 61 @Nullable private final BluetoothEventManager mEventManager; 62 @Nullable private final CachedBluetoothDeviceManager mDeviceManager; 63 @Nullable private final LocalBluetoothLeBroadcastAssistant mAssistant; 64 private final Executor mExecutor; 65 @Nullable private DashboardFragment mFragment; 66 @Nullable private AudioSharingDialogHandler mDialogHandler; 67 @VisibleForTesting 68 BluetoothLeBroadcastAssistant.Callback mAssistantCallback = 69 new BluetoothLeBroadcastAssistant.Callback() { 70 @Override 71 public void onSearchStarted(int reason) { 72 } 73 74 @Override 75 public void onSearchStartFailed(int reason) { 76 } 77 78 @Override 79 public void onSearchStopped(int reason) { 80 } 81 82 @Override 83 public void onSearchStopFailed(int reason) { 84 } 85 86 @Override 87 public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) { 88 } 89 90 @Override 91 public void onSourceAdded( 92 @NonNull BluetoothDevice sink, int sourceId, int reason) { 93 Log.d(TAG, "onSourceAdded: dismiss stale dialog."); 94 if (mDeviceManager != null && mDialogHandler != null) { 95 CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(sink); 96 if (cachedDevice != null) { 97 mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice); 98 } 99 } 100 } 101 102 @Override 103 public void onSourceAddFailed( 104 @NonNull BluetoothDevice sink, 105 @NonNull BluetoothLeBroadcastMetadata source, 106 int reason) { 107 } 108 109 @Override 110 public void onSourceModified( 111 @NonNull BluetoothDevice sink, int sourceId, int reason) { 112 } 113 114 @Override 115 public void onSourceModifyFailed( 116 @NonNull BluetoothDevice sink, int sourceId, int reason) { 117 } 118 119 @Override 120 public void onSourceRemoved( 121 @NonNull BluetoothDevice sink, int sourceId, int reason) { 122 } 123 124 @Override 125 public void onSourceRemoveFailed( 126 @NonNull BluetoothDevice sink, int sourceId, int reason) { 127 } 128 129 @Override 130 public void onReceiveStateChanged( 131 @NonNull BluetoothDevice sink, 132 int sourceId, 133 @NonNull BluetoothLeBroadcastReceiveState state) { 134 } 135 }; 136 AudioSharingJoinHandlerController(@onNull Context context, @NonNull String preferenceKey)137 public AudioSharingJoinHandlerController(@NonNull Context context, 138 @NonNull String preferenceKey) { 139 super(context, preferenceKey); 140 mBtManager = Utils.getLocalBtManager(mContext); 141 mEventManager = mBtManager == null ? null : mBtManager.getEventManager(); 142 mDeviceManager = mBtManager == null ? null : mBtManager.getCachedDeviceManager(); 143 mAssistant = mBtManager == null ? null 144 : mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); 145 mExecutor = Executors.newSingleThreadExecutor(); 146 } 147 148 /** 149 * Initialize the controller. 150 * 151 * @param fragment The fragment to provide the context and metrics category for {@link 152 * AudioSharingBluetoothDeviceUpdater} and provide the host for dialogs. 153 */ init(@onNull DashboardFragment fragment)154 public void init(@NonNull DashboardFragment fragment) { 155 mFragment = fragment; 156 mDialogHandler = new AudioSharingDialogHandler(mContext, fragment); 157 } 158 159 @Override onStart(@onNull LifecycleOwner owner)160 public void onStart(@NonNull LifecycleOwner owner) { 161 var unused = ThreadUtils.postOnBackgroundThread(() -> { 162 if (!isAvailable()) { 163 Log.d(TAG, "Skip onStart(), feature is not supported."); 164 return; 165 } 166 if (mEventManager == null || mDialogHandler == null || mAssistant == null) { 167 Log.d(TAG, "Skip onStart(), profile is not ready."); 168 return; 169 } 170 Log.d(TAG, "onStart() Register callbacks."); 171 mEventManager.registerCallback(this); 172 mAssistant.registerServiceCallBack(mExecutor, mAssistantCallback); 173 mDialogHandler.registerCallbacks(mExecutor); 174 }); 175 } 176 177 @Override onStop(@onNull LifecycleOwner owner)178 public void onStop(@NonNull LifecycleOwner owner) { 179 var unused = ThreadUtils.postOnBackgroundThread(() -> { 180 if (!isAvailable()) { 181 Log.d(TAG, "Skip onStop(), feature is not supported."); 182 return; 183 } 184 if (mEventManager == null || mDialogHandler == null || mAssistant == null) { 185 Log.d(TAG, "Skip onStop(), profile is not ready."); 186 return; 187 } 188 Log.d(TAG, "onStop() Unregister callbacks."); 189 mEventManager.unregisterCallback(this); 190 mAssistant.unregisterServiceCallBack(mAssistantCallback); 191 mDialogHandler.unregisterCallbacks(); 192 }); 193 } 194 195 196 @Override getAvailabilityStatus()197 public int getAvailabilityStatus() { 198 return (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice() 199 && BluetoothUtils.isAudioSharingUIAvailable(mContext)) 200 ? AVAILABLE_UNSEARCHABLE 201 : UNSUPPORTED_ON_DEVICE; 202 } 203 204 @Override getPreferenceKey()205 public String getPreferenceKey() { 206 return KEY; 207 } 208 209 @Override getSliceHighlightMenuRes()210 public int getSliceHighlightMenuRes() { 211 return 0; 212 } 213 214 @Override displayPreference(@onNull PreferenceScreen screen)215 public void displayPreference(@NonNull PreferenceScreen screen) { 216 super.displayPreference(screen); 217 if (mFragment == null 218 || mFragment.getActivity() == null 219 || mFragment.getActivity().getIntent() == null) { 220 Log.d(TAG, "Skip handleDeviceConnectedFromIntent, fragment intent is null"); 221 return; 222 } 223 Intent intent = mFragment.getActivity().getIntent(); 224 var unused = 225 ThreadUtils.postOnBackgroundThread(() -> handleDeviceConnectedFromIntent(intent)); 226 } 227 228 @Override onProfileConnectionStateChanged( @onNull CachedBluetoothDevice cachedDevice, @ConnectionState int state, int bluetoothProfile)229 public void onProfileConnectionStateChanged( 230 @NonNull CachedBluetoothDevice cachedDevice, 231 @ConnectionState int state, 232 int bluetoothProfile) { 233 if (mDialogHandler == null || mFragment == null) { 234 Log.d(TAG, "Ignore onProfileConnectionStateChanged, not init correctly"); 235 return; 236 } 237 // Close related dialogs if the BT remote device is disconnected. 238 if (state == BluetoothAdapter.STATE_DISCONNECTED) { 239 boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice); 240 if (isLeAudioSupported 241 && bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) { 242 Log.d(TAG, "closeOpeningDialogsForLeaDevice"); 243 mDialogHandler.closeOpeningDialogsForLeaDevice(cachedDevice); 244 } else if (!isLeAudioSupported && !cachedDevice.isConnected()) { 245 Log.d(TAG, "closeOpeningDialogsForNonLeaDevice"); 246 mDialogHandler.closeOpeningDialogsForNonLeaDevice(cachedDevice); 247 } 248 } 249 } 250 251 @Override onBluetoothStateChanged(@dapterState int bluetoothState)252 public void onBluetoothStateChanged(@AdapterState int bluetoothState) { 253 if (bluetoothState == BluetoothAdapter.STATE_OFF) { 254 finishActivity(); 255 } 256 } 257 258 /** Handle just connected device via intent. */ 259 @WorkerThread handleDeviceConnectedFromIntent(@onNull Intent intent)260 public void handleDeviceConnectedFromIntent(@NonNull Intent intent) { 261 BluetoothDevice device = intent.getParcelableExtra(EXTRA_BLUETOOTH_DEVICE, 262 BluetoothDevice.class); 263 CachedBluetoothDevice cachedDevice = 264 (device == null || mDeviceManager == null) 265 ? null 266 : mDeviceManager.findDevice(device); 267 if (cachedDevice == null) { 268 Log.d(TAG, "Skip handleDeviceConnectedFromIntent and finish activity, device is null"); 269 finishActivity(); 270 return; 271 } 272 if (mDialogHandler == null) { 273 Log.d(TAG, "Skip handleDeviceConnectedFromIntent and finish activity, handler is null"); 274 finishActivity(); 275 return; 276 } 277 Log.d(TAG, "handleDeviceConnectedFromIntent, device = " + device.getAnonymizedAddress()); 278 if (!mDialogHandler.handleDeviceConnected(cachedDevice, /* userTriggered= */ false)) { 279 Log.d(TAG, "handleDeviceConnectedFromIntent, finish activity"); 280 finishActivity(); 281 } 282 } 283 finishActivity()284 private void finishActivity() { 285 AudioSharingUtils.postOnMainThread(mContext, () -> { 286 if (mFragment != null && mFragment.getActivity() != null) { 287 Log.d(TAG, "Finish activity"); 288 mFragment.getActivity().finish(); 289 } 290 }); 291 } 292 293 @VisibleForTesting setDialogHandler(@ullable AudioSharingDialogHandler dialogHandler)294 void setDialogHandler(@Nullable AudioSharingDialogHandler dialogHandler) { 295 mDialogHandler = dialogHandler; 296 } 297 } 298