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