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.UserHandle;
32 import android.provider.Settings;
33 import android.util.Log;
34 import android.widget.Toast;
35 
36 import androidx.annotation.VisibleForTesting;
37 import androidx.appcompat.app.AlertDialog;
38 
39 import com.android.settings.R;
40 import com.android.settings.flags.Flags;
41 import com.android.settings.overlay.FeatureFactory;
42 import com.android.settingslib.bluetooth.BluetoothUtils;
43 import com.android.settingslib.bluetooth.BluetoothUtils.ErrorListener;
44 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
45 import com.android.settingslib.bluetooth.LocalBluetoothManager;
46 import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback;
47 import com.android.settingslib.utils.ThreadUtils;
48 
49 import com.google.common.base.Supplier;
50 
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.concurrent.ExecutionException;
54 import java.util.concurrent.FutureTask;
55 
56 /**
57  * Utils is a helper class that contains constants for various
58  * Android resource IDs, debug logging flags, and static methods
59  * for creating dialogs.
60  */
61 public final class Utils {
62 
63     private static final String TAG = "BluetoothUtils";
64 
65     static final boolean V = BluetoothUtils.V; // verbose logging
66     static final boolean D = BluetoothUtils.D;  // regular logging
67 
Utils()68     private Utils() {
69     }
70 
getConnectionStateSummary(int connectionState)71     public static int getConnectionStateSummary(int connectionState) {
72         switch (connectionState) {
73             case BluetoothProfile.STATE_CONNECTED:
74                 return com.android.settingslib.R.string.bluetooth_connected;
75             case BluetoothProfile.STATE_CONNECTING:
76                 return com.android.settingslib.R.string.bluetooth_connecting;
77             case BluetoothProfile.STATE_DISCONNECTED:
78                 return com.android.settingslib.R.string.bluetooth_disconnected;
79             case BluetoothProfile.STATE_DISCONNECTING:
80                 return com.android.settingslib.R.string.bluetooth_disconnecting;
81             default:
82                 return 0;
83         }
84     }
85 
86     // Create (or recycle existing) and show disconnect dialog.
showDisconnectDialog(Context context, AlertDialog dialog, DialogInterface.OnClickListener disconnectListener, CharSequence title, CharSequence message)87     static AlertDialog showDisconnectDialog(Context context,
88             AlertDialog dialog,
89             DialogInterface.OnClickListener disconnectListener,
90             CharSequence title, CharSequence message) {
91         if (dialog == null) {
92             dialog = new AlertDialog.Builder(context)
93                     .setPositiveButton(android.R.string.ok, disconnectListener)
94                     .setNegativeButton(android.R.string.cancel, null)
95                     .create();
96         } else {
97             if (dialog.isShowing()) {
98                 dialog.dismiss();
99             }
100             // use disconnectListener for the correct profile(s)
101             CharSequence okText = context.getText(android.R.string.ok);
102             dialog.setButton(DialogInterface.BUTTON_POSITIVE,
103                     okText, disconnectListener);
104         }
105         dialog.setTitle(title);
106         dialog.setMessage(message);
107         dialog.show();
108         return dialog;
109     }
110 
111     @VisibleForTesting
showConnectingError(Context context, String name, LocalBluetoothManager manager)112     static void showConnectingError(Context context, String name, LocalBluetoothManager manager) {
113         FeatureFactory.getFeatureFactory().getMetricsFeatureProvider().visible(context,
114                 SettingsEnums.PAGE_UNKNOWN, SettingsEnums.ACTION_SETTINGS_BLUETOOTH_CONNECT_ERROR,
115                 0);
116         showError(context, name, R.string.bluetooth_connecting_error_message, manager);
117     }
118 
showError(Context context, String name, int messageResId)119     static void showError(Context context, String name, int messageResId) {
120         showError(context, name, messageResId, getLocalBtManager(context));
121     }
122 
showError(Context context, String name, int messageResId, LocalBluetoothManager manager)123     private static void showError(Context context, String name, int messageResId,
124             LocalBluetoothManager manager) {
125         String message = context.getString(messageResId, name);
126         Context activity = manager.getForegroundActivity();
127         if (manager.isForegroundActivity()) {
128             try {
129                 new AlertDialog.Builder(activity)
130                         .setTitle(R.string.bluetooth_error_title)
131                         .setMessage(message)
132                         .setPositiveButton(android.R.string.ok, null)
133                         .show();
134             } catch (Exception e) {
135                 Log.e(TAG, "Cannot show error dialog.", e);
136             }
137         } else {
138             Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
139         }
140     }
141 
getLocalBtManager(Context context)142     public static LocalBluetoothManager getLocalBtManager(Context context) {
143         return LocalBluetoothManager.getInstance(context, mOnInitCallback);
144     }
145 
146     /**
147      * Obtains a {@link LocalBluetoothManager}.
148      *
149      * To avoid StrictMode ThreadPolicy violation, will get it in another thread.
150      */
getLocalBluetoothManager(Context context)151     public static LocalBluetoothManager getLocalBluetoothManager(Context context) {
152         final FutureTask<LocalBluetoothManager> localBtManagerFutureTask = new FutureTask<>(
153                 // Avoid StrictMode ThreadPolicy violation
154                 () -> getLocalBtManager(context));
155         try {
156             localBtManagerFutureTask.run();
157             return localBtManagerFutureTask.get();
158         } catch (InterruptedException | ExecutionException e) {
159             Log.w(TAG, "Error getting LocalBluetoothManager.", e);
160             return null;
161         }
162     }
163 
createRemoteName(Context context, BluetoothDevice device)164     public static String createRemoteName(Context context, BluetoothDevice device) {
165         String mRemoteName = device != null ? device.getAlias() : null;
166 
167         if (mRemoteName == null) {
168             mRemoteName = context.getString(R.string.unknown);
169         }
170         return mRemoteName;
171     }
172 
173     private static final ErrorListener mErrorListener = new ErrorListener() {
174         @Override
175         public void onShowError(Context context, String name, int messageResId) {
176             showError(context, name, messageResId);
177         }
178     };
179 
180     private static final BluetoothManagerCallback mOnInitCallback = new BluetoothManagerCallback() {
181         @Override
182         public void onBluetoothManagerInitialized(Context appContext,
183                 LocalBluetoothManager bluetoothManager) {
184             BluetoothUtils.setErrorListener(mErrorListener);
185         }
186     };
187 
isBluetoothScanningEnabled(Context context)188     public static boolean isBluetoothScanningEnabled(Context context) {
189         return Settings.Global.getInt(context.getContentResolver(),
190                 Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, 0) == 1;
191     }
192 
193     /**
194      * Returns the Bluetooth Package name
195      */
findBluetoothPackageName(Context context)196     public static String findBluetoothPackageName(Context context)
197             throws NameNotFoundException {
198         // this activity will always be in the package where the rest of Bluetooth lives
199         final String sentinelActivity = "com.android.bluetooth.opp.BluetoothOppLauncherActivity";
200         PackageManager packageManager = context.createContextAsUser(UserHandle.SYSTEM, 0)
201                 .getPackageManager();
202         String[] allPackages = packageManager.getPackagesForUid(BLUETOOTH_UID);
203         String matchedPackage = null;
204         for (String candidatePackage : allPackages) {
205             PackageInfo packageInfo;
206             try {
207                 packageInfo =
208                         packageManager.getPackageInfo(
209                                 candidatePackage,
210                                 PackageManager.GET_ACTIVITIES
211                                         | PackageManager.MATCH_ANY_USER
212                                         | PackageManager.MATCH_UNINSTALLED_PACKAGES
213                                         | PackageManager.MATCH_DISABLED_COMPONENTS);
214             } catch (NameNotFoundException e) {
215                 // rethrow
216                 throw e;
217             }
218             if (packageInfo.activities == null) {
219                 continue;
220             }
221             for (ActivityInfo activity : packageInfo.activities) {
222                 if (sentinelActivity.equals(activity.name)) {
223                     if (matchedPackage == null) {
224                         matchedPackage = candidatePackage;
225                     } else {
226                         throw new NameNotFoundException("multiple main bluetooth packages found");
227                     }
228                 }
229             }
230         }
231         if (matchedPackage != null) {
232             return matchedPackage;
233         }
234         throw new NameNotFoundException("Could not find main bluetooth package");
235     }
236 
237     /**
238      * Returns all cachedBluetoothDevices with the same groupId.
239      * @param cachedBluetoothDevice The main cachedBluetoothDevice.
240      * @return all cachedBluetoothDevices with the same groupId.
241      */
getAllOfCachedBluetoothDevices( LocalBluetoothManager localBtMgr, CachedBluetoothDevice cachedBluetoothDevice)242     public static List<CachedBluetoothDevice> getAllOfCachedBluetoothDevices(
243             LocalBluetoothManager localBtMgr,
244             CachedBluetoothDevice cachedBluetoothDevice) {
245         List<CachedBluetoothDevice> cachedBluetoothDevices = new ArrayList<>();
246         if (cachedBluetoothDevice == null) {
247             Log.e(TAG, "getAllOfCachedBluetoothDevices: no cachedBluetoothDevice");
248             return cachedBluetoothDevices;
249         }
250         int deviceGroupId = cachedBluetoothDevice.getGroupId();
251         if (deviceGroupId == BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
252             cachedBluetoothDevices.add(cachedBluetoothDevice);
253             return cachedBluetoothDevices;
254         }
255 
256         if (localBtMgr == null) {
257             Log.e(TAG, "getAllOfCachedBluetoothDevices: no LocalBluetoothManager");
258             return cachedBluetoothDevices;
259         }
260         CachedBluetoothDevice mainDevice =
261                 localBtMgr.getCachedDeviceManager().getCachedDevicesCopy().stream()
262                         .filter(cachedDevice -> cachedDevice.getGroupId() == deviceGroupId)
263                         .findFirst().orElse(null);
264         if (mainDevice == null) {
265             Log.e(TAG, "getAllOfCachedBluetoothDevices: groupId = " + deviceGroupId
266                     + ", no main device.");
267             return cachedBluetoothDevices;
268         }
269         cachedBluetoothDevice = mainDevice;
270         cachedBluetoothDevices.add(cachedBluetoothDevice);
271         for (CachedBluetoothDevice member : cachedBluetoothDevice.getMemberDevice()) {
272             cachedBluetoothDevices.add(member);
273         }
274         Log.d(TAG, "getAllOfCachedBluetoothDevices: groupId = " + deviceGroupId
275                 + " , cachedBluetoothDevice = " + cachedBluetoothDevice
276                 + " , deviceList = " + cachedBluetoothDevices);
277         return cachedBluetoothDevices;
278     }
279 
280     /**
281      * Preloads the values and run the Runnable afterwards.
282      * @param suppliers the value supplier, should be a memoized supplier
283      * @param runnable the runnable to be run after value is preloaded
284      */
preloadAndRun(List<Supplier<?>> suppliers, Runnable runnable)285     public static void preloadAndRun(List<Supplier<?>> suppliers, Runnable runnable) {
286         if (!Flags.enableOffloadBluetoothOperationsToBackgroundThread()) {
287             runnable.run();
288             return;
289         }
290         ThreadUtils.postOnBackgroundThread(() -> {
291             for (Supplier<?> supplier : suppliers) {
292                 supplier.get();
293             }
294             ThreadUtils.postOnMainThread(runnable);
295         });
296     }
297 }
298