• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.car.settings.qc;
18 
19 import static android.os.UserManager.DISALLOW_BLUETOOTH;
20 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
21 
22 import static com.android.car.qc.QCItem.QC_ACTION_TOGGLE_STATE;
23 import static com.android.car.qc.QCItem.QC_TYPE_ACTION_TOGGLE;
24 import static com.android.car.settings.qc.QCUtils.getActionDisabledDialogIntent;
25 
26 import android.app.PendingIntent;
27 import android.bluetooth.BluetoothAdapter;
28 import android.bluetooth.BluetoothClass;
29 import android.bluetooth.BluetoothDevice;
30 import android.bluetooth.BluetoothProfile;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.pm.PackageManager;
34 import android.graphics.drawable.Icon;
35 import android.net.Uri;
36 import android.os.Bundle;
37 
38 import androidx.annotation.DrawableRes;
39 import androidx.annotation.VisibleForTesting;
40 
41 import com.android.car.qc.QCActionItem;
42 import com.android.car.qc.QCItem;
43 import com.android.car.qc.QCList;
44 import com.android.car.qc.QCRow;
45 import com.android.car.settings.R;
46 import com.android.car.settings.common.Logger;
47 import com.android.car.settings.enterprise.EnterpriseUtils;
48 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
49 import com.android.settingslib.bluetooth.HidProfile;
50 import com.android.settingslib.bluetooth.LocalBluetoothManager;
51 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
52 
53 import java.util.ArrayList;
54 import java.util.Collection;
55 import java.util.Comparator;
56 import java.util.List;
57 import java.util.Set;
58 
59 /**
60  * QCItem for showing paired bluetooth devices.
61  */
62 public class PairedBluetoothDevices extends SettingsQCItem {
63     @VisibleForTesting
64     static final String EXTRA_DEVICE_KEY = "BT_EXTRA_DEVICE_KEY";
65     @VisibleForTesting
66     static final String EXTRA_BUTTON_TYPE = "BT_EXTRA_BUTTON_TYPE";
67     @VisibleForTesting
68     static final String BLUETOOTH_BUTTON = "BLUETOOTH_BUTTON";
69     @VisibleForTesting
70     static final String PHONE_BUTTON = "PHONE_BUTTON";
71     @VisibleForTesting
72     static final String MEDIA_BUTTON = "MEDIA_BUTTON";
73     private static final Logger LOG = new Logger(PairedBluetoothDevices.class);
74 
75     private final LocalBluetoothManager mBluetoothManager;
76     private final int mDeviceLimit;
77 
PairedBluetoothDevices(Context context)78     public PairedBluetoothDevices(Context context) {
79         super(context);
80         mBluetoothManager = LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null);
81         mDeviceLimit = context.getResources().getInteger(
82                 R.integer.config_qc_bluetooth_device_limit);
83     }
84 
85     @Override
getQCItem()86     QCItem getQCItem() {
87         if (!getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
88                 || EnterpriseUtils.hasUserRestrictionByDpm(getContext(), DISALLOW_BLUETOOTH)
89                 || mDeviceLimit == 0) {
90             return null;
91         }
92 
93         QCList.Builder listBuilder = new QCList.Builder();
94 
95         if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
96             listBuilder.addRow(new QCRow.Builder()
97                     .setIcon(Icon.createWithResource(getContext(),
98                             R.drawable.ic_settings_bluetooth_disabled))
99                     .setTitle(getContext().getString(R.string.qc_bluetooth_off_devices_info))
100                     .build());
101             return listBuilder.build();
102         }
103 
104         Collection<CachedBluetoothDevice> cachedDevices =
105                 mBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy();
106 
107         //TODO(b/198339129): Use BluetoothDeviceFilter.BONDED_DEVICE_FILTER
108         Set<BluetoothDevice> bondedDevices = mBluetoothManager.getBluetoothAdapter()
109                 .getBondedDevices();
110 
111         List<CachedBluetoothDevice> filteredDevices = new ArrayList<>();
112         for (CachedBluetoothDevice cachedDevice : cachedDevices) {
113             if (bondedDevices != null && bondedDevices.contains(cachedDevice.getDevice())) {
114                 filteredDevices.add(cachedDevice);
115             }
116         }
117         filteredDevices.sort(Comparator.naturalOrder());
118 
119         if (filteredDevices.isEmpty()) {
120             listBuilder.addRow(new QCRow.Builder()
121                     .setIcon(Icon.createWithResource(getContext(),
122                             R.drawable.ic_settings_bluetooth))
123                     .setTitle(getContext().getString(R.string.qc_bluetooth_on_no_devices_info))
124                     .build());
125             return listBuilder.build();
126         }
127 
128         int i = 0;
129         int deviceLimit = mDeviceLimit >= 0 ? Math.min(mDeviceLimit, filteredDevices.size())
130                 : filteredDevices.size();
131         for (int j = 0; j < deviceLimit; j++) {
132             CachedBluetoothDevice cachedDevice = filteredDevices.get(j);
133             listBuilder.addRow(new QCRow.Builder()
134                     .setTitle(cachedDevice.getName())
135                     .setSubtitle(cachedDevice.getCarConnectionSummary(/* shortSummary= */ true))
136                     .setIcon(Icon.createWithResource(getContext(), getIconRes(cachedDevice)))
137                     .addEndItem(createBluetoothButton(cachedDevice, i++))
138                     .addEndItem(createPhoneButton(cachedDevice, i++))
139                     .addEndItem(createMediaButton(cachedDevice, i++))
140                     .build()
141             );
142         }
143 
144         return listBuilder.build();
145     }
146 
147     @Override
getUri()148     Uri getUri() {
149         return SettingsQCRegistry.PAIRED_BLUETOOTH_DEVICES_URI;
150     }
151 
152     @Override
onNotifyChange(Intent intent)153     void onNotifyChange(Intent intent) {
154         String deviceKey = intent.getStringExtra(EXTRA_DEVICE_KEY);
155         if (deviceKey == null) {
156             return;
157         }
158         CachedBluetoothDevice device = null;
159         Collection<CachedBluetoothDevice> cachedDevices =
160                 mBluetoothManager.getCachedDeviceManager().getCachedDevicesCopy();
161         for (CachedBluetoothDevice cachedDevice : cachedDevices) {
162             if (cachedDevice.getAddress().equals(deviceKey)) {
163                 device = cachedDevice;
164                 break;
165             }
166         }
167         if (device == null) {
168             return;
169         }
170 
171         String buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE);
172         boolean newState = intent.getBooleanExtra(QC_ACTION_TOGGLE_STATE, true);
173         if (BLUETOOTH_BUTTON.equals(buttonType)) {
174             if (newState) {
175                 LocalBluetoothProfile phoneProfile = getProfile(device,
176                         BluetoothProfile.HEADSET_CLIENT);
177                 LocalBluetoothProfile mediaProfile = getProfile(device, BluetoothProfile.A2DP_SINK);
178                 // If trying to connect and both phone and media are disabled, connecting will
179                 // always fail. In this case force both profiles on.
180                 if (phoneProfile != null && mediaProfile != null
181                         && !phoneProfile.isEnabled(device.getDevice())
182                         && !mediaProfile.isEnabled(device.getDevice())) {
183                     phoneProfile.setEnabled(device.getDevice(), true);
184                     mediaProfile.setEnabled(device.getDevice(), true);
185                 }
186                 device.connect();
187             } else if (device.isConnected()) {
188                 device.disconnect();
189             }
190         } else if (PHONE_BUTTON.equals(buttonType)) {
191             LocalBluetoothProfile profile = getProfile(device, BluetoothProfile.HEADSET_CLIENT);
192             if (profile != null) {
193                 profile.setEnabled(device.getDevice(), newState);
194             }
195         } else if (MEDIA_BUTTON.equals(buttonType)) {
196             LocalBluetoothProfile profile = getProfile(device, BluetoothProfile.A2DP_SINK);
197             if (profile != null) {
198                 profile.setEnabled(device.getDevice(), newState);
199             }
200         } else {
201             LOG.d("Unknown button type: " + buttonType);
202         }
203     }
204 
205     @Override
getBackgroundWorkerClass()206     Class getBackgroundWorkerClass() {
207         return PairedBluetoothDevicesWorker.class;
208     }
209 
210     @DrawableRes
getIconRes(CachedBluetoothDevice device)211     private int getIconRes(CachedBluetoothDevice device) {
212         BluetoothClass btClass = device.getBtClass();
213         if (btClass != null) {
214             switch (btClass.getMajorDeviceClass()) {
215                 case BluetoothClass.Device.Major.COMPUTER:
216                     return com.android.internal.R.drawable.ic_bt_laptop;
217                 case BluetoothClass.Device.Major.PHONE:
218                     return com.android.internal.R.drawable.ic_phone;
219                 case BluetoothClass.Device.Major.PERIPHERAL:
220                     return HidProfile.getHidClassDrawable(btClass);
221                 case BluetoothClass.Device.Major.IMAGING:
222                     return com.android.internal.R.drawable.ic_settings_print;
223                 default:
224                     // unrecognized device class; continue
225             }
226         }
227 
228         List<LocalBluetoothProfile> profiles = device.getProfiles();
229         for (LocalBluetoothProfile profile : profiles) {
230             int resId = profile.getDrawableResource(btClass);
231             if (resId != 0) {
232                 return resId;
233             }
234         }
235         if (btClass != null) {
236             if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
237                 return com.android.internal.R.drawable.ic_bt_headset_hfp;
238             }
239             if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
240                 return com.android.internal.R.drawable.ic_bt_headphones_a2dp;
241             }
242         }
243         return com.android.internal.R.drawable.ic_settings_bluetooth;
244     }
245 
createBluetoothButton(CachedBluetoothDevice device, int requestCode)246     private QCActionItem createBluetoothButton(CachedBluetoothDevice device, int requestCode) {
247         return createBluetoothDeviceToggle(device, requestCode, BLUETOOTH_BUTTON,
248                 Icon.createWithResource(getContext(), R.drawable.ic_qc_bluetooth), true,
249                 !device.isBusy(), false, device.isConnected());
250     }
251 
createPhoneButton(CachedBluetoothDevice device, int requestCode)252     private QCActionItem createPhoneButton(CachedBluetoothDevice device, int requestCode) {
253         BluetoothProfileToggleState phoneState = getBluetoothProfileToggleState(device,
254                 BluetoothProfile.HEADSET_CLIENT);
255         int iconRes = phoneState.mIsAvailable ? R.drawable.ic_qc_bluetooth_phone
256                 : R.drawable.ic_qc_bluetooth_phone_unavailable;
257         return createBluetoothDeviceToggle(device, requestCode, PHONE_BUTTON,
258                 Icon.createWithResource(getContext(), iconRes),
259                 phoneState.mIsAvailable, phoneState.mIsEnabled,
260                 phoneState.mIsClickableWhileDisabled, phoneState.mIsChecked);
261     }
262 
createMediaButton(CachedBluetoothDevice device, int requestCode)263     private QCActionItem createMediaButton(CachedBluetoothDevice device, int requestCode) {
264         BluetoothProfileToggleState mediaState = getBluetoothProfileToggleState(device,
265                 BluetoothProfile.A2DP_SINK);
266         int iconRes = mediaState.mIsAvailable ? R.drawable.ic_qc_bluetooth_media
267                 : R.drawable.ic_qc_bluetooth_media_unavailable;
268         return createBluetoothDeviceToggle(device, requestCode, MEDIA_BUTTON,
269                 Icon.createWithResource(getContext(), iconRes),
270                 mediaState.mIsAvailable, mediaState.mIsEnabled,
271                 mediaState.mIsClickableWhileDisabled, mediaState.mIsChecked);
272     }
273 
createBluetoothDeviceToggle(CachedBluetoothDevice device, int requestCode, String buttonType, Icon icon, boolean available, boolean enabled, boolean clickableWhileDisabled, boolean checked)274     private QCActionItem createBluetoothDeviceToggle(CachedBluetoothDevice device, int requestCode,
275             String buttonType, Icon icon, boolean available, boolean enabled,
276             boolean clickableWhileDisabled, boolean checked) {
277         Bundle extras = new Bundle();
278         extras.putString(EXTRA_BUTTON_TYPE, buttonType);
279         extras.putString(EXTRA_DEVICE_KEY, device.getAddress());
280         PendingIntent action = getBroadcastIntent(extras, requestCode);
281 
282         return new QCActionItem.Builder(QC_TYPE_ACTION_TOGGLE)
283                 .setAvailable(available)
284                 .setChecked(checked)
285                 .setEnabled(enabled)
286                 .setClickableWhileDisabled(clickableWhileDisabled)
287                 .setAction(action)
288                 .setDisabledClickAction(getActionDisabledDialogIntent(getContext(),
289                         DISALLOW_CONFIG_BLUETOOTH))
290                 .setIcon(icon)
291                 .build();
292     }
293 
getProfile(CachedBluetoothDevice device, int profileId)294     private LocalBluetoothProfile getProfile(CachedBluetoothDevice device, int profileId) {
295         for (LocalBluetoothProfile profile : device.getProfiles()) {
296             if (profile.getProfileId() == profileId) {
297                 return profile;
298             }
299         }
300         return null;
301     }
302 
getBluetoothProfileToggleState(CachedBluetoothDevice device, int profileId)303     private BluetoothProfileToggleState getBluetoothProfileToggleState(CachedBluetoothDevice device,
304             int profileId) {
305         LocalBluetoothProfile profile = getProfile(device, profileId);
306         if (!device.isConnected() || profile == null) {
307             return new BluetoothProfileToggleState(false, false, false, false);
308         }
309         boolean hasUmRestrictions = EnterpriseUtils.hasUserRestrictionByUm(getContext(),
310                 DISALLOW_CONFIG_BLUETOOTH);
311         boolean hasDpmRestrictions = EnterpriseUtils.hasUserRestrictionByDpm(getContext(),
312                 DISALLOW_CONFIG_BLUETOOTH);
313         return new BluetoothProfileToggleState(true, !hasDpmRestrictions && !hasUmRestrictions
314                 && !device.isBusy(), hasDpmRestrictions, profile.isEnabled(device.getDevice()));
315     }
316 
317     private static class BluetoothProfileToggleState {
318         final boolean mIsAvailable;
319         final boolean mIsEnabled;
320         final boolean mIsClickableWhileDisabled;
321         final boolean mIsChecked;
322 
BluetoothProfileToggleState(boolean isAvailable, boolean isEnabled, boolean isClickableWhileDisabled, boolean isChecked)323         BluetoothProfileToggleState(boolean isAvailable, boolean isEnabled,
324                 boolean isClickableWhileDisabled, boolean isChecked) {
325             mIsAvailable = isAvailable;
326             mIsEnabled = isEnabled;
327             mIsClickableWhileDisabled = isClickableWhileDisabled;
328             mIsChecked = isChecked;
329         }
330     }
331 }
332