• 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         pref.getActionItem(BLUETOOTH_BUTTON).setVisible(true);
108         pref.getActionItem(PHONE_BUTTON).setVisible(true);
109         pref.getActionItem(MEDIA_BUTTON).setVisible(true);
110         pref.setToggleButtonUpdateListener(this);
111         mHasUxRestriction = hasNoSetupUxRestriction();
112         setButtonsCheckedAndListeners(pref);
113         return pref;
114     }
115 
116     @Override
onDeviceClicked(CachedBluetoothDevice cachedDevice)117     protected void onDeviceClicked(CachedBluetoothDevice cachedDevice) {
118         if (mShowDeviceDetails) {
119             getFragmentController().launchFragment(
120                     BluetoothDeviceDetailsFragment.newInstance(cachedDevice));
121         }
122     }
123 
124     @Override
onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState)125     public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
126         refreshUi();
127     }
128 
129     @Override
updateState(PreferenceGroup preferenceGroup)130     protected void updateState(PreferenceGroup preferenceGroup) {
131         super.updateState(preferenceGroup);
132         updateActionAvailability(preferenceGroup);
133     }
134 
135     @Override
updateToggleButtonState(BluetoothDevicePreference preference)136     public void updateToggleButtonState(BluetoothDevicePreference preference) {
137         updateActionAvailability(preference);
138     }
139 
updateActionAvailability(PreferenceGroup group)140     private void updateActionAvailability(PreferenceGroup group) {
141         for (int i = 0; i < group.getPreferenceCount(); i++) {
142             BluetoothDevicePreference preference =
143                     (BluetoothDevicePreference) group.getPreference(i);
144             updateActionAvailability(preference);
145         }
146     }
147 
updateActionAvailability(BluetoothDevicePreference preference)148     private void updateActionAvailability(BluetoothDevicePreference preference) {
149         mHasUxRestriction = hasNoSetupUxRestriction();
150         if (!mHasUxRestriction) {
151             setButtonsCheckedAndListeners(preference);
152         } else {
153             updatePhoneActionItemAvailability(preference, /* isAvailable= */ false);
154             updateMediaActionItemAvailability(preference, /* isAvailable= */ false);
155         }
156         mShowDeviceDetails = !mHasUxRestriction;
157     }
158 
toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice)159     private void toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice) {
160         if (connect) {
161             cachedDevice.connect();
162         } else if (cachedDevice.isConnected()) {
163             cachedDevice.disconnect();
164         }
165     }
166 
setButtonsCheckedAndListeners(BluetoothDevicePreference preference)167     private void setButtonsCheckedAndListeners(BluetoothDevicePreference preference) {
168         CachedBluetoothDevice cachedDevice = preference.getCachedDevice();
169 
170         // If device is currently attempting to connect/disconnect, disable further actions
171         if (cachedDevice.isBusy()) {
172             disableAllActionItems(preference);
173             // There is a case where on creation the cached device will try to automatically connect
174             // but does not report itself as busy yet. This ensures that the bluetooth button state
175             // is correct (should be checked in either connecting or disconnecting states).
176             preference.getActionItem(BLUETOOTH_BUTTON).setChecked(true);
177             return;
178         }
179 
180         LocalBluetoothProfile phoneProfile = null;
181         LocalBluetoothProfile mediaProfile = null;
182         for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
183             if (profile.getProfileId() == BluetoothProfile.HEADSET_CLIENT) {
184                 phoneProfile = profile;
185             } else if (profile.getProfileId() == BluetoothProfile.A2DP_SINK) {
186                 mediaProfile = profile;
187             }
188         }
189         LocalBluetoothProfile finalPhoneProfile = phoneProfile;
190         LocalBluetoothProfile finalMediaProfile = mediaProfile;
191         boolean isConnected = cachedDevice.isConnected();
192 
193         // Setup up bluetooth button
194         updateBluetoothActionItemAvailability(preference);
195         ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON);
196         bluetoothItem.setChecked(isConnected);
197         bluetoothItem.setOnClickListener(
198                 isChecked -> {
199                     if (cachedDevice.isBusy()) {
200                         return;
201                     }
202                     // If trying to connect and both phone and media are disabled, connecting will
203                     // always fail. In this case force both profiles on.
204                     if (isChecked && finalPhoneProfile != null && finalMediaProfile != null
205                             && !finalPhoneProfile.isEnabled(cachedDevice.getDevice())
206                             && !finalMediaProfile.isEnabled(cachedDevice.getDevice())) {
207                         finalPhoneProfile.setEnabled(cachedDevice.getDevice(), true);
208                         finalMediaProfile.setEnabled(cachedDevice.getDevice(), true);
209                     }
210                     toggleBluetoothConnectivity(isChecked, cachedDevice);
211                 });
212 
213         if (phoneProfile == null || !isConnected || mHasUxRestriction) {
214             // Disable phone button
215             updatePhoneActionItemAvailability(preference, /* isAvailable= */ false);
216         } else {
217             // Enable phone button
218             ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON);
219             updatePhoneActionItemAvailability(preference, /* isAvailable= */ true);
220             boolean phoneEnabled = phoneProfile.isEnabled(cachedDevice.getDevice());
221 
222             if (hasDisallowConfigRestriction()) {
223                 phoneItem.setOnClickWhileDisabledListener(p -> BluetoothUtils
224                         .onClickWhileDisabled(getContext(), getFragmentController()));
225             }
226             phoneItem.setOnClickListener(isChecked ->
227                     finalPhoneProfile.setEnabled(cachedDevice.getDevice(), isChecked));
228             phoneItem.setChecked(phoneEnabled);
229         }
230 
231         if (mediaProfile == null || !isConnected || mHasUxRestriction) {
232             // Disable media button
233             updateMediaActionItemAvailability(preference, /* isAvailable= */ false);
234         } else {
235             // Enable media button
236             ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON);
237             updateMediaActionItemAvailability(preference, /* isAvailable= */ true);
238             boolean mediaEnabled = mediaProfile.isEnabled(cachedDevice.getDevice());
239 
240             if (hasDisallowConfigRestriction()) {
241                 mediaItem.setOnClickWhileDisabledListener(p -> BluetoothUtils
242                         .onClickWhileDisabled(getContext(), getFragmentController()));
243             }
244             mediaItem.setOnClickListener(isChecked ->
245                     finalMediaProfile.setEnabled(cachedDevice.getDevice(), isChecked));
246             mediaItem.setChecked(mediaEnabled);
247         }
248     }
249 
updateBluetoothActionItemAvailability(BluetoothDevicePreference preference)250     private void updateBluetoothActionItemAvailability(BluetoothDevicePreference preference) {
251         // Run on main thread because recyclerview may still be computing layout
252         getContext().getMainExecutor().execute(() -> {
253             ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON);
254             bluetoothItem.setEnabled(true);
255             bluetoothItem.setDrawable(getContext(), R.drawable.ic_bluetooth_button);
256         });
257     }
258 
updatePhoneActionItemAvailability(BluetoothDevicePreference preference, boolean isAvailable)259     private void updatePhoneActionItemAvailability(BluetoothDevicePreference preference,
260             boolean isAvailable) {
261         // Run on main thread because recyclerview may still be computing layout
262         getContext().getMainExecutor().execute(() -> {
263             ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON);
264             phoneItem.setEnabled(isAvailable && !hasDisallowConfigRestriction());
265             phoneItem.setDrawable(getContext(), isAvailable
266                     ? R.drawable.ic_bluetooth_phone : R.drawable.ic_bluetooth_phone_unavailable);
267             phoneItem.setRestricted(!isAvailable && mHasUxRestriction);
268         });
269     }
270 
updateMediaActionItemAvailability(BluetoothDevicePreference preference, boolean isAvailable)271     private void updateMediaActionItemAvailability(BluetoothDevicePreference preference,
272             boolean isAvailable) {
273         // Run on main thread because recyclerview may still be computing layout
274         getContext().getMainExecutor().execute(() -> {
275             ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON);
276             mediaItem.setEnabled(isAvailable && !hasDisallowConfigRestriction());
277             mediaItem.setDrawable(getContext(), isAvailable
278                     ? R.drawable.ic_bluetooth_media : R.drawable.ic_bluetooth_media_unavailable);
279             mediaItem.setRestricted(!isAvailable && mHasUxRestriction);
280         });
281     }
282 
disableAllActionItems(BluetoothDevicePreference preference)283     private void disableAllActionItems(BluetoothDevicePreference preference) {
284         // Run on main thread because recyclerview may still be computing layout
285         getContext().getMainExecutor().execute(() -> {
286             preference.getActionItem(BLUETOOTH_BUTTON).setEnabled(false);
287             preference.getActionItem(PHONE_BUTTON).setEnabled(false);
288             preference.getActionItem(MEDIA_BUTTON).setEnabled(false);
289         });
290     }
291 
hasDisallowConfigRestriction()292     private boolean hasDisallowConfigRestriction() {
293         return getUserManager().hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH);
294     }
295 
hasNoSetupUxRestriction()296     private boolean hasNoSetupUxRestriction() {
297         return CarUxRestrictionsHelper.isNoSetup(getUxRestrictions());
298     }
299 
300     /** Filter that matches only bonded devices with specific device types. */
301     //TODO(b/198339129): Use BluetoothDeviceFilter.BONDED_DEVICE_FILTER
302     private class BondedDeviceTypeFilter implements BluetoothDeviceFilter.Filter {
303         @Override
matches(BluetoothDevice device)304         public boolean matches(BluetoothDevice device) {
305             Set<BluetoothDevice> bondedDevices = mBluetoothManager.getBluetoothAdapter()
306                     .getBondedDevices();
307             return bondedDevices != null && bondedDevices.contains(device);
308         }
309     }
310 }
311