• 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.BluetoothProfile;
22 import android.car.drivingstate.CarUxRestrictions;
23 import android.content.Context;
24 import android.os.UserManager;
25 
26 import androidx.annotation.VisibleForTesting;
27 import androidx.preference.PreferenceGroup;
28 
29 import com.android.car.settings.R;
30 import com.android.car.settings.common.CarUxRestrictionsHelper;
31 import com.android.car.settings.common.FragmentController;
32 import com.android.car.settings.common.MultiActionPreference;
33 import com.android.car.settings.common.ToggleButtonActionItem;
34 import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
35 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
36 import com.android.settingslib.bluetooth.LocalBluetoothManager;
37 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
38 
39 
40 /**
41  * Displays a list of bonded (paired) Bluetooth devices. Clicking on a device launch the device
42  * details page. Additional buttons to will connect/disconnect from the device, toggle phone calls,
43  * and toggle media audio.
44  */
45 public class BluetoothBondedDevicesPreferenceController extends
46         BluetoothDevicesGroupPreferenceController implements
47         BluetoothDevicePreference.UpdateToggleButtonListener {
48 
49     private static final MultiActionPreference.ActionItem BLUETOOTH_BUTTON =
50             MultiActionPreference.ActionItem.ACTION_ITEM1;
51     private static final MultiActionPreference.ActionItem PHONE_BUTTON =
52             MultiActionPreference.ActionItem.ACTION_ITEM2;
53     private static final MultiActionPreference.ActionItem MEDIA_BUTTON =
54             MultiActionPreference.ActionItem.ACTION_ITEM3;
55 
56     private boolean mShowDeviceDetails = true;
57 
BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)58     public BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey,
59             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
60         super(context, preferenceKey, fragmentController, uxRestrictions);
61     }
62 
63     @VisibleForTesting
BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, LocalBluetoothManager localBluetoothManager, UserManager userManager)64     BluetoothBondedDevicesPreferenceController(Context context, String preferenceKey,
65             FragmentController fragmentController, CarUxRestrictions uxRestrictions,
66             LocalBluetoothManager localBluetoothManager, UserManager userManager) {
67         super(context, preferenceKey, fragmentController, uxRestrictions, localBluetoothManager,
68                 userManager);
69     }
70 
71     @Override
getDeviceFilter()72     protected BluetoothDeviceFilter.Filter getDeviceFilter() {
73         return BluetoothDeviceFilter.BONDED_DEVICE_FILTER;
74     }
75 
76     @Override
createDevicePreference(CachedBluetoothDevice cachedDevice)77     protected BluetoothDevicePreference createDevicePreference(CachedBluetoothDevice cachedDevice) {
78         BluetoothDevicePreference pref = super.createDevicePreference(cachedDevice);
79         pref.getActionItem(BLUETOOTH_BUTTON).setVisible(true);
80         pref.getActionItem(PHONE_BUTTON).setVisible(true);
81         pref.getActionItem(MEDIA_BUTTON).setVisible(true);
82         pref.setToggleButtonUpdateListener(this);
83         updateBluetoothActionItemAvailability(pref);
84         updateActionAvailability(pref, true);
85 
86         return pref;
87     }
88 
89     @Override
onDeviceClicked(CachedBluetoothDevice cachedDevice)90     protected void onDeviceClicked(CachedBluetoothDevice cachedDevice) {
91         if (mShowDeviceDetails) {
92             getFragmentController().launchFragment(
93                     BluetoothDeviceDetailsFragment.newInstance(cachedDevice));
94         }
95     }
96 
97     @Override
onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState)98     public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
99         refreshUi();
100     }
101 
102     @Override
updateState(PreferenceGroup preferenceGroup)103     protected void updateState(PreferenceGroup preferenceGroup) {
104         super.updateState(preferenceGroup);
105 
106         boolean hasUserRestriction = getUserManager()
107                 .hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH);
108         updateActionAvailability(preferenceGroup, hasUserRestriction);
109     }
110 
111     @Override
onApplyUxRestrictions(CarUxRestrictions uxRestrictions)112     protected void onApplyUxRestrictions(CarUxRestrictions uxRestrictions) {
113         super.onApplyUxRestrictions(uxRestrictions);
114 
115         if (CarUxRestrictionsHelper.isNoSetup(uxRestrictions)) {
116             updateActionAvailability(getPreference(), /* isRestricted= */ true);
117         }
118     }
119 
120     @Override
updateToggleButtonState(BluetoothDevicePreference preference)121     public void updateToggleButtonState(BluetoothDevicePreference preference) {
122         boolean hasUserRestriction = getUserManager()
123                 .hasUserRestriction(DISALLOW_CONFIG_BLUETOOTH);
124         updateActionAvailability(preference, hasUserRestriction);
125     }
126 
updateActionAvailability(PreferenceGroup group, boolean isRestricted)127     private void updateActionAvailability(PreferenceGroup group, boolean isRestricted) {
128         for (int i = 0; i < group.getPreferenceCount(); i++) {
129             BluetoothDevicePreference preference =
130                     (BluetoothDevicePreference) group.getPreference(i);
131             updateActionAvailability(preference, isRestricted);
132         }
133     }
134 
updateActionAvailability(BluetoothDevicePreference preference, boolean isRestricted)135     private void updateActionAvailability(BluetoothDevicePreference preference,
136             boolean isRestricted) {
137         if (!isRestricted) {
138             setButtonsCheckedAndListeners(preference);
139             mShowDeviceDetails = true;
140         } else {
141             updatePhoneActionItemAvailability(preference, true);
142             updateMediaActionItemAvailability(preference, true);
143             mShowDeviceDetails = false;
144         }
145     }
146 
toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice)147     private void toggleBluetoothConnectivity(boolean connect, CachedBluetoothDevice cachedDevice) {
148         if (connect) {
149             cachedDevice.connect();
150         } else if (cachedDevice.isConnected()) {
151             cachedDevice.disconnect();
152         }
153     }
154 
setButtonsCheckedAndListeners(BluetoothDevicePreference preference)155     private void setButtonsCheckedAndListeners(BluetoothDevicePreference preference) {
156         CachedBluetoothDevice cachedDevice = preference.getCachedDevice();
157 
158         // If device is currently attempting to connect/disconnect, disable further actions
159         if (cachedDevice.isBusy()) {
160             disableAllActionItems(preference);
161             // There is a case where on creation the cached device will try to automatically connect
162             // but does not report itself as busy yet. This ensures that the bluetooth button state
163             // is correct (should be checked in either connecting or disconnecting states).
164             preference.getActionItem(BLUETOOTH_BUTTON).setChecked(true);
165             return;
166         }
167 
168         LocalBluetoothProfile phoneProfile = null;
169         LocalBluetoothProfile mediaProfile = null;
170         for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
171             if (profile.getProfileId() == BluetoothProfile.HEADSET_CLIENT) {
172                 phoneProfile = profile;
173             } else if (profile.getProfileId() == BluetoothProfile.A2DP_SINK) {
174                 mediaProfile = profile;
175             }
176         }
177         LocalBluetoothProfile finalPhoneProfile = phoneProfile;
178         LocalBluetoothProfile finalMediaProfile = mediaProfile;
179         boolean isConnected = cachedDevice.isConnected();
180 
181         // Setup up bluetooth button
182         updateBluetoothActionItemAvailability(preference);
183         ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON);
184         bluetoothItem.setChecked(isConnected);
185         bluetoothItem.setOnClickListener(
186                 isChecked -> {
187                     if (cachedDevice.isBusy()) {
188                         return;
189                     }
190                     // If trying to connect and both phone and media are disabled, connecting will
191                     // always fail. In this case force both profiles on.
192                     if (isChecked && finalPhoneProfile != null && finalMediaProfile != null
193                             && !finalPhoneProfile.isEnabled(cachedDevice.getDevice())
194                             && !finalMediaProfile.isEnabled(cachedDevice.getDevice())) {
195                         finalPhoneProfile.setEnabled(cachedDevice.getDevice(), true);
196                         finalMediaProfile.setEnabled(cachedDevice.getDevice(), true);
197                     }
198                     toggleBluetoothConnectivity(isChecked, cachedDevice);
199                 });
200 
201         if (phoneProfile == null || !isConnected) {
202             // Disable phone button
203             updatePhoneActionItemAvailability(preference, true);
204         } else {
205             // Enable phone button
206             updatePhoneActionItemAvailability(preference, false);
207             ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON);
208             boolean phoneEnabled = phoneProfile.isEnabled(cachedDevice.getDevice());
209             phoneItem.setChecked(phoneEnabled);
210             phoneItem.setOnClickListener(isChecked ->
211                     finalPhoneProfile.setEnabled(cachedDevice.getDevice(), isChecked));
212         }
213 
214         if (mediaProfile == null || !isConnected) {
215             // Disable media button
216             updateMediaActionItemAvailability(preference, true);
217         } else {
218             // Enable media button
219             updateMediaActionItemAvailability(preference, false);
220             ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON);
221             boolean mediaEnabled = mediaProfile.isEnabled(cachedDevice.getDevice());
222             mediaItem.setChecked(mediaEnabled);
223             mediaItem.setOnClickListener(isChecked ->
224                     finalMediaProfile.setEnabled(cachedDevice.getDevice(), isChecked));
225         }
226     }
227 
updateBluetoothActionItemAvailability(BluetoothDevicePreference preference)228     private void updateBluetoothActionItemAvailability(BluetoothDevicePreference preference) {
229         // Run on main thread because recyclerview may still be computing layout
230         getContext().getMainExecutor().execute(() -> {
231             ToggleButtonActionItem bluetoothItem = preference.getActionItem(BLUETOOTH_BUTTON);
232             bluetoothItem.setEnabled(true);
233             bluetoothItem.setDrawable(getContext(), R.drawable.ic_bluetooth_button);
234         });
235     }
236 
updatePhoneActionItemAvailability(BluetoothDevicePreference preference, boolean isRestricted)237     private void updatePhoneActionItemAvailability(BluetoothDevicePreference preference,
238             boolean isRestricted) {
239         // Run on main thread because recyclerview may still be computing layout
240         getContext().getMainExecutor().execute(() -> {
241             ToggleButtonActionItem phoneItem = preference.getActionItem(PHONE_BUTTON);
242             phoneItem.setEnabled(!isRestricted);
243             phoneItem.setDrawable(getContext(), isRestricted
244                     ? R.drawable.ic_bluetooth_phone_unavailable : R.drawable.ic_bluetooth_phone);
245         });
246     }
247 
updateMediaActionItemAvailability(BluetoothDevicePreference preference, boolean isRestricted)248     private void updateMediaActionItemAvailability(BluetoothDevicePreference preference,
249             boolean isRestricted) {
250         // Run on main thread because recyclerview may still be computing layout
251         getContext().getMainExecutor().execute(() -> {
252             ToggleButtonActionItem mediaItem = preference.getActionItem(MEDIA_BUTTON);
253             mediaItem.setEnabled(!isRestricted);
254             mediaItem.setDrawable(getContext(), isRestricted
255                     ? R.drawable.ic_bluetooth_media_unavailable : R.drawable.ic_bluetooth_media);
256         });
257     }
258 
disableAllActionItems(BluetoothDevicePreference preference)259     private void disableAllActionItems(BluetoothDevicePreference preference) {
260         // Run on main thread because recyclerview may still be computing layout
261         getContext().getMainExecutor().execute(() -> {
262             preference.getActionItem(BLUETOOTH_BUTTON).setEnabled(false);
263             preference.getActionItem(PHONE_BUTTON).setEnabled(false);
264             preference.getActionItem(MEDIA_BUTTON).setEnabled(false);
265         });
266     }
267 }
268