• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 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.bluetooth;
18 
19 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
20 
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.BluetoothProfile;
23 import android.car.drivingstate.CarUxRestrictions;
24 import android.content.Context;
25 import android.os.UserManager;
26 
27 import androidx.annotation.VisibleForTesting;
28 import androidx.preference.PreferenceGroup;
29 
30 import com.android.car.settings.R;
31 import com.android.car.settings.common.CarUxRestrictionsHelper;
32 import com.android.car.settings.common.FragmentController;
33 import com.android.car.settings.common.MultiActionPreference;
34 import com.android.car.settings.common.ToggleButtonActionItem;
35 import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
36 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
37 import com.android.settingslib.bluetooth.LocalBluetoothManager;
38 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
39 
40 import java.util.Set;
41 
42 /**
43  * Displays a list of bonded (paired) Bluetooth devices. Clicking on a device launch the device
44  * details page. Additional buttons to will connect/disconnect from the device, toggle phone calls,
45  * and toggle media audio.
46  *
47  * <p>
48  * Moreover, these buttons' availability and enable/disable status are controlled by UX restriction
49  * and user restriction. Specifically,
50  * <ul>
51  * <li>{@code BLUETOOTH_BUTTON}: always available and enabled.
52  * <li>{@code PHONE_BUTTON}: available when the device has {@code BluetoothProfile.HEADSET_CLIENT}
53  * and {@code CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP} is not set. Disabled but clickable when
54  * {@link UserManager.DISALLOW_CONFIG_BLUETOOTH} is set.
55  * <li>{@code MEDIA_BUTTON}: available when the device has {@code BluetoothProfile.A2DP_SINK} and
56  * {@code CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP} is not set; Disabled but clickable when
57  * {@link UserManager.DISALLOW_CONFIG_BLUETOOTH} is set.
58  * </ul>
59  *
60  * <p>
61  * Note: when button is disabled, it will still be shown as available. When the button is disabled
62  * because of {@link UserManager.DISALLOW_CONFIG_BLUETOOTH} is set by DevicePolicyManager, click
63  * on the button will show action disabled by admin dialog.
64  *
65  * <p>
66  * Device detail page will not be launched when UX retriction is set. It can still be launched
67  * when there is {@link UserManager.DISALLOW_CONFIG_BLUETOOTH} restriction. However, individual
68  * profile's toggle switch will be disabled - when clicked, shows action disabled by admin dialog.
69  */
70 public class BluetoothBondedDevicesPreferenceController extends
71         BluetoothDevicesGroupPreferenceController implements
72         BluetoothDevicePreference.UpdateToggleButtonListener {
73 
74     private static final MultiActionPreference.ActionItem BLUETOOTH_BUTTON =
75             MultiActionPreference.ActionItem.ACTION_ITEM1;
76     private static final MultiActionPreference.ActionItem PHONE_BUTTON =
77             MultiActionPreference.ActionItem.ACTION_ITEM2;
78     private static final MultiActionPreference.ActionItem MEDIA_BUTTON =
79             MultiActionPreference.ActionItem.ACTION_ITEM3;
80 
81     private final BluetoothDeviceFilter.Filter mBondedDeviceTypeFilter =
82             new BondedDeviceTypeFilter();
83     private boolean mShowDeviceDetails = true;
84     private boolean mHasUxRestriction;
85 
BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)86     public BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey,
87             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
88         super(context, preferenceKey, fragmentController, uxRestrictions);
89     }
90 
91     @VisibleForTesting
BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, LocalBluetoothManager localBluetoothManager, UserManager userManager)92     BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey,
93             FragmentController fragmentController, CarUxRestrictions uxRestrictions,
94             LocalBluetoothManager localBluetoothManager, UserManager userManager) {
95         super(context, preferenceKey, fragmentController, uxRestrictions, localBluetoothManager,
96                 userManager);
97     }
98 
99     @Override
getDeviceFilter()100     protected BluetoothDeviceFilter.Filter getDeviceFilter() {
101         return mBondedDeviceTypeFilter;
102     }
103 
104     @Override
createDevicePreference(CachedBluetoothDevice cachedDevice)105     protected BluetoothDevicePreference createDevicePreference(CachedBluetoothDevice cachedDevice) {
106         BluetoothDevicePreference pref = super.createDevicePreference(cachedDevice);
107         ToggleButtonActionItem bluetoothItem = pref.getActionItem(BLUETOOTH_BUTTON);
108         ToggleButtonActionItem phoneItem = pref.getActionItem(PHONE_BUTTON);
109         ToggleButtonActionItem mediaItem = pref.getActionItem(MEDIA_BUTTON);
110 
111         bluetoothItem.setVisible(true);
112         phoneItem.setVisible(true);
113         mediaItem.setVisible(true);
114 
115         bluetoothItem.setContentDescription(getContext(),
116                 R.string.bluetooth_bonded_bluetooth_toggle_content_description);
117         phoneItem.setContentDescription(getContext(),
118                 R.string.bluetooth_bonded_phone_toggle_content_description);
119         mediaItem.setContentDescription(getContext(),
120                 R.string.bluetooth_bonded_media_toggle_content_description);
121 
122         pref.setToggleButtonUpdateListener(this);
123         mHasUxRestriction = hasNoSetupUxRestriction();
124         setButtonsCheckedAndListeners(pref);
125         return pref;
126     }
127 
128     @Override
onDeviceClicked(CachedBluetoothDevice cachedDevice)129     protected void onDeviceClicked(CachedBluetoothDevice cachedDevice) {
130         if (mShowDeviceDetails) {
131             getFragmentController().launchFragment(
132                     BluetoothDeviceDetailsFragment.newInstance(cachedDevice));
133         }
134     }
135 
136     @Override
onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState)137     public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
138         refreshUi();
139     }
140 
141     @Override
updateState(PreferenceGroup preferenceGroup)142     protected void updateState(PreferenceGroup preferenceGroup) {
143         super.updateState(preferenceGroup);
144         updateActionAvailability(preferenceGroup);
145     }
146 
147     @Override
updateToggleButtonState(BluetoothDevicePreference preference)148     public void updateToggleButtonState(BluetoothDevicePreference preference) {
149         updateActionAvailability(preference);
150     }
151 
updateActionAvailability(PreferenceGroup group)152     private void updateActionAvailability(PreferenceGroup group) {
153         for (int i = 0; i < group.getPreferenceCount(); i++) {
154             BluetoothDevicePreference preference =
155                     (BluetoothDevicePreference) group.getPreference(i);
156             updateActionAvailability(preference);
157         }
158     }
159 
updateActionAvailability(BluetoothDevicePreference preference)160     private void updateActionAvailability(BluetoothDevicePreference preference) {
161         mHasUxRestriction = hasNoSetupUxRestriction();
162         if (!mHasUxRestriction) {
163             setButtonsCheckedAndListeners(preference);
164         } else {
165             updatePhoneActionItemAvailability(preference, /* isAvailable= */ false);
166             updateMediaActionItemAvailability(preference, /* isAvailable= */ false);
167         }
168         mShowDeviceDetails = !mHasUxRestriction;
169     }
170 
toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice)171     private void toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice) {
172         if (connect) {
173             cachedDevice.connect();
174         } else if (cachedDevice.isConnected()) {
175             cachedDevice.disconnect();
176         }
177     }
178 
setButtonsCheckedAndListeners(BluetoothDevicePreference preference)179     private void setButtonsCheckedAndListeners(BluetoothDevicePreference preference) {
180         CachedBluetoothDevice cachedDevice = preference.getCachedDevice();
181 
182         // If device is currently attempting to connect/disconnect, disable further actions
183         if (cachedDevice.isBusy()) {
184             disableAllActionItems(preference);
185             // There is a case where on creation the cached device will try to automatically connect
186             // but does not report itself as busy yet. This ensures that the bluetooth button state
187             // is correct (should be checked in either connecting or disconnecting states).
188             preference.getActionItem(BLUETOOTH_BUTTON).setChecked(true);
189             return;
190         }
191 
192         LocalBluetoothProfile phoneProfile = null;
193         LocalBluetoothProfile mediaProfile = null;
194         for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
195             if (profile.getProfileId() == BluetoothProfile.HEADSET_CLIENT) {
196                 phoneProfile = profile;
197             } else if (profile.getProfileId() == BluetoothProfile.A2DP_SINK) {
198                 mediaProfile = profile;
199             }
200         }
201         LocalBluetoothProfile finalPhoneProfile = phoneProfile;
202         LocalBluetoothProfile finalMediaProfile = mediaProfile;
203         boolean isConnected = cachedDevice.isConnected();
204 
205         // Setup up bluetooth button
206         updateBluetoothActionItemAvailability(preference);
207         ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON);
208         bluetoothItem.setChecked(isConnected);
209         bluetoothItem.setOnClickListener(
210                 isChecked -> {
211                     if (cachedDevice.isBusy()) {
212                         return;
213                     }
214                     // If trying to connect and both phone and media are disabled, connecting will
215                     // always fail. In this case force both profiles on.
216                     if (isChecked && finalPhoneProfile != null && finalMediaProfile != null
217                             && !finalPhoneProfile.isEnabled(cachedDevice.getDevice())
218                             && !finalMediaProfile.isEnabled(cachedDevice.getDevice())) {
219                         finalPhoneProfile.setEnabled(cachedDevice.getDevice(), true);
220                         finalMediaProfile.setEnabled(cachedDevice.getDevice(), true);
221                     }
222                     toggleBluetoothConnectivity(isChecked, cachedDevice);
223                 });
224 
225         if (phoneProfile == null || !isConnected || mHasUxRestriction) {
226             // Disable phone button
227             updatePhoneActionItemAvailability(preference, /* isAvailable= */ false);
228         } else {
229             // Enable phone button
230             ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON);
231             updatePhoneActionItemAvailability(preference, /* isAvailable= */ true);
232             boolean phoneEnabled = phoneProfile.isEnabled(cachedDevice.getDevice());
233 
234             if (hasDisallowConfigRestriction()) {
235                 phoneItem.setOnClickWhileDisabledListener(p -> BluetoothUtils
236                         .onClickWhileDisabled(getContext(), getFragmentController()));
237             }
238             phoneItem.setOnClickListener(isChecked ->
239                     finalPhoneProfile.setEnabled(cachedDevice.getDevice(), isChecked));
240             phoneItem.setChecked(phoneEnabled);
241         }
242 
243         if (mediaProfile == null || !isConnected || mHasUxRestriction) {
244             // Disable media button
245             updateMediaActionItemAvailability(preference, /* isAvailable= */ false);
246         } else {
247             // Enable media button
248             ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON);
249             updateMediaActionItemAvailability(preference, /* isAvailable= */ true);
250             boolean mediaEnabled = mediaProfile.isEnabled(cachedDevice.getDevice());
251 
252             if (hasDisallowConfigRestriction()) {
253                 mediaItem.setOnClickWhileDisabledListener(p -> BluetoothUtils
254                         .onClickWhileDisabled(getContext(), getFragmentController()));
255             }
256             mediaItem.setOnClickListener(isChecked ->
257                     finalMediaProfile.setEnabled(cachedDevice.getDevice(), isChecked));
258             mediaItem.setChecked(mediaEnabled);
259         }
260     }
261 
updateBluetoothActionItemAvailability(BluetoothDevicePreference preference)262     private void updateBluetoothActionItemAvailability(BluetoothDevicePreference preference) {
263         // Run on main thread because recyclerview may still be computing layout
264         getContext().getMainExecutor().execute(() -> {
265             ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON);
266             bluetoothItem.setEnabled(true);
267             bluetoothItem.setDrawable(getContext(), R.drawable.ic_bluetooth_button);
268         });
269     }
270 
updatePhoneActionItemAvailability(BluetoothDevicePreference preference, boolean isAvailable)271     private void updatePhoneActionItemAvailability(BluetoothDevicePreference preference,
272             boolean isAvailable) {
273         // Run on main thread because recyclerview may still be computing layout
274         getContext().getMainExecutor().execute(() -> {
275             ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON);
276             phoneItem.setEnabled(isAvailable && !hasDisallowConfigRestriction());
277             phoneItem.setDrawable(getContext(), isAvailable
278                     ? R.drawable.ic_bluetooth_phone : R.drawable.ic_bluetooth_phone_unavailable);
279             phoneItem.setRestricted(!isAvailable && mHasUxRestriction);
280         });
281     }
282 
updateMediaActionItemAvailability(BluetoothDevicePreference preference, boolean isAvailable)283     private void updateMediaActionItemAvailability(BluetoothDevicePreference preference,
284             boolean isAvailable) {
285         // Run on main thread because recyclerview may still be computing layout
286         getContext().getMainExecutor().execute(() -> {
287             ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON);
288             mediaItem.setEnabled(isAvailable && !hasDisallowConfigRestriction());
289             mediaItem.setDrawable(getContext(), isAvailable
290                     ? R.drawable.ic_bluetooth_media : R.drawable.ic_bluetooth_media_unavailable);
291             mediaItem.setRestricted(!isAvailable && mHasUxRestriction);
292         });
293     }
294 
disableAllActionItems(BluetoothDevicePreference preference)295     private void disableAllActionItems(BluetoothDevicePreference preference) {
296         // Run on main thread because recyclerview may still be computing layout
297         getContext().getMainExecutor().execute(() -> {
298             preference.getActionItem(BLUETOOTH_BUTTON).setEnabled(false);
299             preference.getActionItem(PHONE_BUTTON).setEnabled(false);
300             preference.getActionItem(MEDIA_BUTTON).setEnabled(false);
301         });
302     }
303 
hasDisallowConfigRestriction()304     private boolean hasDisallowConfigRestriction() {
305         return getUserManager().hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH);
306     }
307 
hasNoSetupUxRestriction()308     private boolean hasNoSetupUxRestriction() {
309         return CarUxRestrictionsHelper.isNoSetup(getUxRestrictions());
310     }
311 
312     /** Filter that matches only bonded devices with specific device types. */
313     //TODO(b/198339129): Use BluetoothDeviceFilter.BONDED_DEVICE_FILTER
314     private class BondedDeviceTypeFilter implements BluetoothDeviceFilter.Filter {
315         @Override
matches(BluetoothDevice device)316         public boolean matches(BluetoothDevice device) {
317             Set<BluetoothDevice> bondedDevices = mBluetoothManager.getBluetoothAdapter()
318                     .getBondedDevices();
319             return bondedDevices != null && bondedDevices.contains(device);
320         }
321     }
322 }
323