• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.bluetooth;
18 
19 import static android.os.Process.BLUETOOTH_UID;
20 
21 import android.app.settings.SettingsEnums;
22 import android.bluetooth.BluetoothCsipSetCoordinator;
23 import android.bluetooth.BluetoothDevice;
24 import android.bluetooth.BluetoothProfile;
25 import android.content.Context;
26 import android.content.DialogInterface;
27 import android.content.pm.ActivityInfo;
28 import android.content.pm.PackageInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.PackageManager.NameNotFoundException;
31 import android.os.SystemProperties;
32 import android.os.UserHandle;
33 import android.provider.Settings;
34 import android.util.Log;
35 import android.widget.Toast;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 import androidx.annotation.VisibleForTesting;
40 import androidx.appcompat.app.AlertDialog;
41 
42 import com.android.settings.R;
43 import com.android.settings.flags.Flags;
44 import com.android.settings.overlay.FeatureFactory;
45 import com.android.settingslib.bluetooth.BluetoothUtils;
46 import com.android.settingslib.bluetooth.BluetoothUtils.ErrorListener;
47 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
48 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
49 import com.android.settingslib.bluetooth.HearingAidProfile;
50 import com.android.settingslib.bluetooth.LeAudioProfile;
51 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
52 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant;
53 import com.android.settingslib.bluetooth.LocalBluetoothManager;
54 import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback;
55 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
56 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
57 import com.android.settingslib.utils.ThreadUtils;
58 
59 import com.google.common.base.Supplier;
60 import com.google.common.collect.ImmutableList;
61 
62 import java.util.ArrayList;
63 import java.util.Collection;
64 import java.util.HashSet;
65 import java.util.List;
66 import java.util.Set;
67 import java.util.concurrent.ExecutionException;
68 import java.util.concurrent.FutureTask;
69 import java.util.stream.Collectors;
70 
71 /**
72  * Utils is a helper class that contains constants for various
73  * Android resource IDs, debug logging flags, and static methods
74  * for creating dialogs.
75  */
76 public final class Utils {
77 
78     private static final String TAG = "BluetoothUtils";
79     private static final String ENABLE_DUAL_MODE_AUDIO = "persist.bluetooth.enable_dual_mode_audio";
80 
81     static final boolean V = BluetoothUtils.V; // verbose logging
82     static final boolean D = BluetoothUtils.D;  // regular logging
83 
Utils()84     private Utils() {
85     }
86 
getConnectionStateSummary(int connectionState)87     public static int getConnectionStateSummary(int connectionState) {
88         switch (connectionState) {
89             case BluetoothProfile.STATE_CONNECTED:
90                 return com.android.settingslib.R.string.bluetooth_connected;
91             case BluetoothProfile.STATE_CONNECTING:
92                 return com.android.settingslib.R.string.bluetooth_connecting;
93             case BluetoothProfile.STATE_DISCONNECTED:
94                 return com.android.settingslib.R.string.bluetooth_disconnected;
95             case BluetoothProfile.STATE_DISCONNECTING:
96                 return com.android.settingslib.R.string.bluetooth_disconnecting;
97             default:
98                 return 0;
99         }
100     }
101 
102     // Create (or recycle existing) and show disconnect dialog.
showDisconnectDialog(Context context, AlertDialog dialog, DialogInterface.OnClickListener disconnectListener, CharSequence title, CharSequence message)103     static AlertDialog showDisconnectDialog(Context context,
104             AlertDialog dialog,
105             DialogInterface.OnClickListener disconnectListener,
106             CharSequence title, CharSequence message) {
107         if (dialog == null) {
108             dialog = new AlertDialog.Builder(context)
109                     .setPositiveButton(android.R.string.ok, disconnectListener)
110                     .setNegativeButton(android.R.string.cancel, null)
111                     .create();
112         } else {
113             if (dialog.isShowing()) {
114                 dialog.dismiss();
115             }
116             // use disconnectListener for the correct profile(s)
117             CharSequence okText = context.getText(android.R.string.ok);
118             dialog.setButton(DialogInterface.BUTTON_POSITIVE,
119                     okText, disconnectListener);
120         }
121         dialog.setTitle(title);
122         dialog.setMessage(message);
123         dialog.show();
124         return dialog;
125     }
126 
127     @VisibleForTesting
showConnectingError(Context context, String name, LocalBluetoothManager manager)128     static void showConnectingError(Context context, String name, LocalBluetoothManager manager) {
129         FeatureFactory.getFeatureFactory().getMetricsFeatureProvider().visible(context,
130                 SettingsEnums.PAGE_UNKNOWN, SettingsEnums.ACTION_SETTINGS_BLUETOOTH_CONNECT_ERROR,
131                 0);
132         showError(context, name, R.string.bluetooth_connecting_error_message, manager);
133     }
134 
showError(Context context, String name, int messageResId)135     static void showError(Context context, String name, int messageResId) {
136         showError(context, name, messageResId, getLocalBtManager(context));
137     }
138 
showError(Context context, String name, int messageResId, LocalBluetoothManager manager)139     private static void showError(Context context, String name, int messageResId,
140             LocalBluetoothManager manager) {
141         String message = context.getString(messageResId, name);
142         Context activity = manager.getForegroundActivity();
143         if (manager.isForegroundActivity()) {
144             try {
145                 new AlertDialog.Builder(activity)
146                         .setTitle(R.string.bluetooth_error_title)
147                         .setMessage(message)
148                         .setPositiveButton(android.R.string.ok, null)
149                         .show();
150             } catch (Exception e) {
151                 Log.e(TAG, "Cannot show error dialog.", e);
152             }
153         } else {
154             Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
155         }
156     }
157 
getLocalBtManager(Context context)158     public static LocalBluetoothManager getLocalBtManager(Context context) {
159         return LocalBluetoothManager.getInstance(context, mOnInitCallback);
160     }
161 
162     /**
163      * Obtains a {@link LocalBluetoothManager}.
164      *
165      * To avoid StrictMode ThreadPolicy violation, will get it in another thread.
166      */
getLocalBluetoothManager(Context context)167     public static LocalBluetoothManager getLocalBluetoothManager(Context context) {
168         final FutureTask<LocalBluetoothManager> localBtManagerFutureTask = new FutureTask<>(
169                 // Avoid StrictMode ThreadPolicy violation
170                 () -> getLocalBtManager(context));
171         try {
172             localBtManagerFutureTask.run();
173             return localBtManagerFutureTask.get();
174         } catch (InterruptedException | ExecutionException e) {
175             Log.w(TAG, "Error getting LocalBluetoothManager.", e);
176             return null;
177         }
178     }
179 
createRemoteName(Context context, BluetoothDevice device)180     public static String createRemoteName(Context context, BluetoothDevice device) {
181         String mRemoteName = device != null ? device.getAlias() : null;
182 
183         if (mRemoteName == null) {
184             mRemoteName = context.getString(R.string.unknown);
185         }
186         return mRemoteName;
187     }
188 
189     private static final ErrorListener mErrorListener = new ErrorListener() {
190         @Override
191         public void onShowError(Context context, String name, int messageResId) {
192             showError(context, name, messageResId);
193         }
194     };
195 
196     private static final BluetoothManagerCallback mOnInitCallback = new BluetoothManagerCallback() {
197         @Override
198         public void onBluetoothManagerInitialized(Context appContext,
199                 LocalBluetoothManager bluetoothManager) {
200             BluetoothUtils.setErrorListener(mErrorListener);
201         }
202     };
203 
isBluetoothScanningEnabled(Context context)204     public static boolean isBluetoothScanningEnabled(Context context) {
205         return Settings.Global.getInt(context.getContentResolver(),
206                 Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, 0) == 1;
207     }
208 
209     /**
210      * Returns the Bluetooth Package name
211      */
findBluetoothPackageName(Context context)212     public static String findBluetoothPackageName(Context context)
213             throws NameNotFoundException {
214         // this activity will always be in the package where the rest of Bluetooth lives
215         final String sentinelActivity = "com.android.bluetooth.opp.BluetoothOppLauncherActivity";
216         PackageManager packageManager = context.createContextAsUser(UserHandle.SYSTEM, 0)
217                 .getPackageManager();
218         String[] allPackages = packageManager.getPackagesForUid(BLUETOOTH_UID);
219         String matchedPackage = null;
220         for (String candidatePackage : allPackages) {
221             PackageInfo packageInfo;
222             try {
223                 packageInfo =
224                         packageManager.getPackageInfo(
225                                 candidatePackage,
226                                 PackageManager.GET_ACTIVITIES
227                                         | PackageManager.MATCH_ANY_USER
228                                         | PackageManager.MATCH_UNINSTALLED_PACKAGES
229                                         | PackageManager.MATCH_DISABLED_COMPONENTS);
230             } catch (NameNotFoundException e) {
231                 // rethrow
232                 throw e;
233             }
234             if (packageInfo.activities == null) {
235                 continue;
236             }
237             for (ActivityInfo activity : packageInfo.activities) {
238                 if (sentinelActivity.equals(activity.name)) {
239                     if (matchedPackage == null) {
240                         matchedPackage = candidatePackage;
241                     } else {
242                         throw new NameNotFoundException("multiple main bluetooth packages found");
243                     }
244                 }
245             }
246         }
247         if (matchedPackage != null) {
248             return matchedPackage;
249         }
250         throw new NameNotFoundException("Could not find main bluetooth package");
251     }
252 
253     /**
254      * Returns all cachedBluetoothDevices with the same groupId.
255      * @param cachedBluetoothDevice The main cachedBluetoothDevice.
256      * @return all cachedBluetoothDevices with the same groupId.
257      */
findAllCachedBluetoothDevicesByGroupId( LocalBluetoothManager localBtMgr, CachedBluetoothDevice cachedBluetoothDevice)258     public static Set<CachedBluetoothDevice> findAllCachedBluetoothDevicesByGroupId(
259             LocalBluetoothManager localBtMgr,
260             CachedBluetoothDevice cachedBluetoothDevice) {
261         Set<CachedBluetoothDevice> cachedBluetoothDevices = new HashSet<>();
262         if (cachedBluetoothDevice == null) {
263             Log.e(TAG, "findAllCachedBluetoothDevicesByGroupId: no cachedBluetoothDevice");
264             return cachedBluetoothDevices;
265         }
266         int deviceGroupId = cachedBluetoothDevice.getGroupId();
267         if (deviceGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
268             cachedBluetoothDevices.add(cachedBluetoothDevice);
269             return cachedBluetoothDevices;
270         }
271 
272         if (localBtMgr == null) {
273             Log.e(TAG, "findAllCachedBluetoothDevicesByGroupId: no LocalBluetoothManager");
274             return cachedBluetoothDevices;
275         }
276         CachedBluetoothDevice mainDevice =
277                 localBtMgr.getCachedDeviceManager().getCachedDevicesCopy().stream()
278                         .filter(cachedDevice -> cachedDevice.getGroupId() == deviceGroupId)
279                         .findFirst().orElse(null);
280         if (mainDevice == null) {
281             Log.e(TAG, "findAllCachedBluetoothDevicesByGroupId: groupId = " + deviceGroupId
282                     + ", no main device.");
283             return cachedBluetoothDevices;
284         }
285         cachedBluetoothDevice = mainDevice;
286         cachedBluetoothDevices.add(cachedBluetoothDevice);
287         cachedBluetoothDevices.addAll(cachedBluetoothDevice.getMemberDevice());
288         Log.d(TAG, "findAllCachedBluetoothDevicesByGroupId: groupId = " + deviceGroupId
289                 + " , cachedBluetoothDevice = " + cachedBluetoothDevice
290                 + " , deviceList = " + cachedBluetoothDevices);
291         return cachedBluetoothDevices;
292     }
293 
294     /**
295      * Preloads the values and run the Runnable afterwards.
296      * @param suppliers the value supplier, should be a memoized supplier
297      * @param runnable the runnable to be run after value is preloaded
298      */
preloadAndRun(List<Supplier<?>> suppliers, Runnable runnable)299     public static void preloadAndRun(List<Supplier<?>> suppliers, Runnable runnable) {
300         if (!Flags.enableOffloadBluetoothOperationsToBackgroundThread()) {
301             runnable.run();
302             return;
303         }
304         ThreadUtils.postOnBackgroundThread(() -> {
305             for (Supplier<?> supplier : suppliers) {
306                 Object unused = supplier.get();
307             }
308             ThreadUtils.postOnMainThread(runnable);
309         });
310     }
311 
312     /**
313      * Check if need to block pairing during audio sharing
314      *
315      * @param localBtManager {@link LocalBluetoothManager}
316      * @return if need to block pairing during audio sharing
317      */
shouldBlockPairingInAudioSharing( @onNull LocalBluetoothManager localBtManager)318     public static boolean shouldBlockPairingInAudioSharing(
319             @NonNull LocalBluetoothManager localBtManager) {
320         if (!BluetoothUtils.isBroadcasting(localBtManager)) return false;
321         LocalBluetoothLeBroadcastAssistant assistant =
322                 localBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
323         CachedBluetoothDeviceManager deviceManager = localBtManager.getCachedDeviceManager();
324         List<BluetoothDevice> connectedDevices =
325                 assistant == null ? ImmutableList.of() : assistant.getAllConnectedDevices();
326         Collection<CachedBluetoothDevice> bondedDevices =
327                 deviceManager == null ? ImmutableList.of() : deviceManager.getCachedDevicesCopy();
328         // Block the pairing if there is ongoing audio sharing session and
329         // a) there is already one temp bond sink bonded
330         // or b) there are already two sinks joining the audio sharing
331         return assistant != null && deviceManager != null
332                 && (bondedDevices.stream().anyMatch(
333                         d -> BluetoothUtils.isTemporaryBondDevice(d.getDevice())
334                                 && d.getBondState() == BluetoothDevice.BOND_BONDED)
335                 || connectedDevices.stream().filter(
336                         d -> BluetoothUtils.hasActiveLocalBroadcastSourceForBtDevice(d,
337                                 localBtManager))
338                 .map(d -> BluetoothUtils.getGroupId(deviceManager.findDevice(d))).collect(
339                         Collectors.toSet()).size() >= 2);
340     }
341 
342     /**
343      * Show block pairing dialog during audio sharing
344      * @param context The dialog context
345      * @param dialog The dialog if already exists
346      * @param localBtManager {@link LocalBluetoothManager}
347      * @return The block pairing dialog
348      */
349     @Nullable
showBlockPairingDialog(@onNull Context context, @Nullable AlertDialog dialog, @Nullable LocalBluetoothManager localBtManager)350     static AlertDialog showBlockPairingDialog(@NonNull Context context,
351             @Nullable AlertDialog dialog, @Nullable LocalBluetoothManager localBtManager) {
352         if (!com.android.settingslib.flags.Flags.enableTemporaryBondDevicesUi()) return null;
353         if (dialog != null && dialog.isShowing()) return dialog;
354         if (dialog == null) {
355             AlertDialog.Builder builder = new AlertDialog.Builder(context)
356                     .setNegativeButton(android.R.string.cancel, null)
357                     .setTitle(R.string.audio_sharing_block_pairing_dialog_title)
358                     .setMessage(R.string.audio_sharing_block_pairing_dialog_content);
359             LocalBluetoothLeBroadcast broadcast = localBtManager == null ? null :
360                     localBtManager.getProfileManager().getLeAudioBroadcastProfile();
361             if (broadcast != null) {
362                 builder.setPositiveButton(R.string.audio_sharing_turn_off_button_label,
363                         (dlg, which) -> broadcast.stopLatestBroadcast());
364             }
365             dialog = builder.create();
366         }
367         dialog.show();
368         return dialog;
369     }
370 
371     /** Enables/disables LE Audio profile for the device. */
setLeAudioEnabled( @onNull LocalBluetoothManager manager, @NonNull CachedBluetoothDevice cachedDevice, boolean enable)372     public static void setLeAudioEnabled(
373             @NonNull LocalBluetoothManager manager,
374             @NonNull CachedBluetoothDevice cachedDevice,
375             boolean enable) {
376         List<CachedBluetoothDevice> devices =
377                 List.copyOf(findAllCachedBluetoothDevicesByGroupId(manager, cachedDevice));
378         setLeAudioEnabled(manager, devices, enable);
379     }
380 
381     /** Enables/disables LE Audio profile for the devices in the same csip group. */
setLeAudioEnabled( @onNull LocalBluetoothManager manager, @NonNull List<CachedBluetoothDevice> devicesWithSameGroupId, boolean enable)382     public static void setLeAudioEnabled(
383             @NonNull LocalBluetoothManager manager,
384             @NonNull List<CachedBluetoothDevice> devicesWithSameGroupId,
385             boolean enable) {
386         LocalBluetoothProfileManager profileManager = manager.getProfileManager();
387         LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
388         List<CachedBluetoothDevice> leAudioDevices =
389                 getDevicesWithProfile(devicesWithSameGroupId, leAudioProfile);
390         if (leAudioDevices.isEmpty()) {
391             Log.i(TAG, "Fail to setLeAudioEnabled, no LE Audio profile found.");
392         }
393         boolean dualModeEnabled = SystemProperties.getBoolean(ENABLE_DUAL_MODE_AUDIO, false);
394 
395         if (enable && !dualModeEnabled) {
396             Log.i(TAG, "Disabling classic audio profiles because dual mode is disabled");
397             setProfileEnabledWhenChangingLeAudio(
398                     devicesWithSameGroupId, profileManager.getA2dpProfile(), false);
399             setProfileEnabledWhenChangingLeAudio(
400                     devicesWithSameGroupId, profileManager.getHeadsetProfile(), false);
401         }
402 
403         HearingAidProfile asha = profileManager.getHearingAidProfile();
404         LocalBluetoothLeBroadcastAssistant broadcastAssistant =
405                 profileManager.getLeAudioBroadcastAssistantProfile();
406 
407         for (CachedBluetoothDevice leAudioDevice : leAudioDevices) {
408             Log.d(
409                     TAG,
410                     "device:"
411                             + leAudioDevice.getDevice().getAnonymizedAddress()
412                             + " set LE profile enabled: "
413                             + enable);
414             leAudioProfile.setEnabled(leAudioDevice.getDevice(), enable);
415             if (asha != null) {
416                 asha.setEnabled(leAudioDevice.getDevice(), !enable);
417             }
418             if (broadcastAssistant != null) {
419                 Log.d(
420                         TAG,
421                         "device:"
422                                 + leAudioDevice.getDevice().getAnonymizedAddress()
423                                 + " enable LE broadcast assistant profile: "
424                                 + enable);
425                 broadcastAssistant.setEnabled(leAudioDevice.getDevice(), enable);
426             }
427         }
428 
429         if (!enable && !dualModeEnabled) {
430             Log.i(TAG, "Enabling classic audio profiles because dual mode is disabled");
431             setProfileEnabledWhenChangingLeAudio(
432                     devicesWithSameGroupId, profileManager.getA2dpProfile(), true);
433             setProfileEnabledWhenChangingLeAudio(
434                     devicesWithSameGroupId, profileManager.getHeadsetProfile(), true);
435         }
436     }
437 
getDevicesWithProfile( List<CachedBluetoothDevice> devices, LocalBluetoothProfile profile)438     private static List<CachedBluetoothDevice> getDevicesWithProfile(
439             List<CachedBluetoothDevice> devices, LocalBluetoothProfile profile) {
440         List<CachedBluetoothDevice> devicesWithProfile = new ArrayList<>();
441         for (CachedBluetoothDevice device : devices) {
442             for (LocalBluetoothProfile currentProfile : device.getProfiles()) {
443                 if (currentProfile.toString().equals(profile.toString())) {
444                     devicesWithProfile.add(device);
445                 }
446             }
447         }
448         return devicesWithProfile;
449     }
450 
setProfileEnabledWhenChangingLeAudio( List<CachedBluetoothDevice> devices, @Nullable LocalBluetoothProfile profile, boolean enable)451     private static void setProfileEnabledWhenChangingLeAudio(
452             List<CachedBluetoothDevice> devices,
453             @Nullable LocalBluetoothProfile profile,
454             boolean enable) {
455         if (profile == null) {
456             Log.i(TAG, "profile is null");
457             return;
458         }
459         List<CachedBluetoothDevice> deviceWithProfile = getDevicesWithProfile(devices, profile);
460         Log.d(TAG, "Set " + profile + " enabled:" + enable + " when switching LE Audio");
461         for (CachedBluetoothDevice profileDevice : deviceWithProfile) {
462             if (profile.isEnabled(profileDevice.getDevice()) != enable) {
463                 Log.d(
464                         TAG,
465                         "The "
466                                 + profileDevice.getDevice().getAnonymizedAddress()
467                                 + ":"
468                                 + profile
469                                 + " set to "
470                                 + enable);
471                 profile.setEnabled(profileDevice.getDevice(), enable);
472             } else {
473                 Log.d(
474                         TAG,
475                         "The "
476                                 + profileDevice.getDevice().getAnonymizedAddress()
477                                 + ":"
478                                 + profile
479                                 + " profile is already "
480                                 + enable
481                                 + ". Do nothing.");
482             }
483         }
484     }
485 }
486