1 /*
2  * Copyright (C) 2017 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.settings.bluetooth;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothProfile;
21 import android.content.Context;
22 import android.text.TextUtils;
23 
24 import androidx.annotation.VisibleForTesting;
25 import androidx.preference.Preference;
26 import androidx.preference.PreferenceCategory;
27 import androidx.preference.PreferenceFragmentCompat;
28 import androidx.preference.PreferenceScreen;
29 import androidx.preference.SwitchPreference;
30 
31 import com.android.settings.R;
32 import com.android.settingslib.bluetooth.A2dpProfile;
33 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
34 import com.android.settingslib.bluetooth.LocalBluetoothManager;
35 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
36 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
37 import com.android.settingslib.bluetooth.MapProfile;
38 import com.android.settingslib.bluetooth.PanProfile;
39 import com.android.settingslib.bluetooth.PbapServerProfile;
40 import com.android.settingslib.core.lifecycle.Lifecycle;
41 
42 import java.util.List;
43 
44 /**
45  * This class adds switches for toggling the individual profiles that a Bluetooth device
46  * supports, such as "Phone audio", "Media audio", "Contact sharing", etc.
47  */
48 public class BluetoothDetailsProfilesController extends BluetoothDetailsController
49         implements Preference.OnPreferenceClickListener,
50         LocalBluetoothProfileManager.ServiceListener {
51     private static final String KEY_PROFILES_GROUP = "bluetooth_profiles";
52     private static final String KEY_BOTTOM_PREFERENCE = "bottom_preference";
53     private static final int ORDINAL = 99;
54 
55     @VisibleForTesting
56     static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio";
57 
58     private LocalBluetoothManager mManager;
59     private LocalBluetoothProfileManager mProfileManager;
60     private CachedBluetoothDevice mCachedDevice;
61 
62     @VisibleForTesting
63     PreferenceCategory mProfilesContainer;
64 
BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment, LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle)65     public BluetoothDetailsProfilesController(Context context, PreferenceFragmentCompat fragment,
66             LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle) {
67         super(context, fragment, device, lifecycle);
68         mManager = manager;
69         mProfileManager = mManager.getProfileManager();
70         mCachedDevice = device;
71         lifecycle.addObserver(this);
72     }
73 
74     @Override
init(PreferenceScreen screen)75     protected void init(PreferenceScreen screen) {
76         mProfilesContainer = (PreferenceCategory)screen.findPreference(getPreferenceKey());
77         mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
78         // Call refresh here even though it will get called later in onResume, to avoid the
79         // list of switches appearing to "pop" into the page.
80         refresh();
81     }
82 
83     /**
84      * Creates a switch preference for the particular profile.
85      *
86      * @param context The context to use when creating the SwitchPreference
87      * @param profile The profile for which the preference controls.
88      * @return A preference that allows the user to choose whether this profile
89      * will be connected to.
90      */
createProfilePreference(Context context, LocalBluetoothProfile profile)91     private SwitchPreference createProfilePreference(Context context,
92             LocalBluetoothProfile profile) {
93         SwitchPreference pref = new SwitchPreference(context);
94         pref.setKey(profile.toString());
95         pref.setTitle(profile.getNameResource(mCachedDevice.getDevice()));
96         pref.setOnPreferenceClickListener(this);
97         pref.setOrder(profile.getOrdinal());
98         return pref;
99     }
100 
101     /**
102      * Refreshes the state for an existing SwitchPreference for a profile.
103      */
refreshProfilePreference(SwitchPreference profilePref, LocalBluetoothProfile profile)104     private void refreshProfilePreference(SwitchPreference profilePref,
105             LocalBluetoothProfile profile) {
106         BluetoothDevice device = mCachedDevice.getDevice();
107         profilePref.setEnabled(!mCachedDevice.isBusy());
108         if (profile instanceof MapProfile) {
109             profilePref.setChecked(device.getMessageAccessPermission()
110                     == BluetoothDevice.ACCESS_ALLOWED);
111         } else if (profile instanceof PbapServerProfile) {
112             profilePref.setChecked(device.getPhonebookAccessPermission()
113                     == BluetoothDevice.ACCESS_ALLOWED);
114         } else if (profile instanceof PanProfile) {
115             profilePref.setChecked(profile.getConnectionStatus(device) ==
116                     BluetoothProfile.STATE_CONNECTED);
117         } else {
118             profilePref.setChecked(profile.isEnabled(device));
119         }
120 
121         if (profile instanceof A2dpProfile) {
122             A2dpProfile a2dp = (A2dpProfile) profile;
123             SwitchPreference highQualityPref = (SwitchPreference) mProfilesContainer.findPreference(
124                     HIGH_QUALITY_AUDIO_PREF_TAG);
125             if (highQualityPref != null) {
126                 if (a2dp.isEnabled(device) && a2dp.supportsHighQualityAudio(device)) {
127                     highQualityPref.setVisible(true);
128                     highQualityPref.setTitle(a2dp.getHighQualityAudioOptionLabel(device));
129                     highQualityPref.setChecked(a2dp.isHighQualityAudioEnabled(device));
130                     highQualityPref.setEnabled(!mCachedDevice.isBusy());
131                 } else {
132                     highQualityPref.setVisible(false);
133                 }
134             }
135         }
136     }
137 
138     /**
139      * Helper method to enable a profile for a device.
140      */
enableProfile(LocalBluetoothProfile profile)141     private void enableProfile(LocalBluetoothProfile profile) {
142         final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
143         if (profile instanceof PbapServerProfile) {
144             bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
145             // We don't need to do the additional steps below for this profile.
146             return;
147         }
148         if (profile instanceof MapProfile) {
149             bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
150         }
151         profile.setEnabled(bluetoothDevice, true);
152     }
153 
154     /**
155      * Helper method to disable a profile for a device
156      */
disableProfile(LocalBluetoothProfile profile)157     private void disableProfile(LocalBluetoothProfile profile) {
158         final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
159         profile.setEnabled(bluetoothDevice, false);
160         if (profile instanceof MapProfile) {
161             bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED);
162         } else if (profile instanceof PbapServerProfile) {
163             bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
164         }
165     }
166 
167     /**
168      * When the pref for a bluetooth profile is clicked on, we want to toggle the enabled/disabled
169      * state for that profile.
170      */
171     @Override
onPreferenceClick(Preference preference)172     public boolean onPreferenceClick(Preference preference) {
173         LocalBluetoothProfile profile = mProfileManager.getProfileByName(preference.getKey());
174         if (profile == null) {
175             // It might be the PbapServerProfile, which is not stored by name.
176             PbapServerProfile psp = mManager.getProfileManager().getPbapProfile();
177             if (TextUtils.equals(preference.getKey(), psp.toString())) {
178                 profile = psp;
179             } else {
180                 return false;
181             }
182         }
183         SwitchPreference profilePref = (SwitchPreference) preference;
184         if (profilePref.isChecked()) {
185             enableProfile(profile);
186         } else {
187             disableProfile(profile);
188         }
189         refreshProfilePreference(profilePref, profile);
190         return true;
191     }
192 
193 
194     /**
195      * Helper to get the list of connectable and special profiles.
196      */
getProfiles()197     private List<LocalBluetoothProfile> getProfiles() {
198         List<LocalBluetoothProfile> result = mCachedDevice.getConnectableProfiles();
199         final BluetoothDevice device = mCachedDevice.getDevice();
200 
201         final int pbapPermission = device.getPhonebookAccessPermission();
202         // Only provide PBAP cabability if the client device has requested PBAP.
203         if (pbapPermission != BluetoothDevice.ACCESS_UNKNOWN) {
204             final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile();
205             result.add(psp);
206         }
207 
208         final MapProfile mapProfile = mManager.getProfileManager().getMapProfile();
209         final int mapPermission = device.getMessageAccessPermission();
210         if (mapPermission != BluetoothDevice.ACCESS_UNKNOWN) {
211             result.add(mapProfile);
212         }
213 
214         return result;
215     }
216 
217     /**
218      * This is a helper method to be called after adding a Preference for a profile. If that
219      * profile happened to be A2dp and the device supports high quality audio, it will add a
220      * separate preference for controlling whether to actually use high quality audio.
221      *
222      * @param profile the profile just added
223      */
maybeAddHighQualityAudioPref(LocalBluetoothProfile profile)224     private void maybeAddHighQualityAudioPref(LocalBluetoothProfile profile) {
225         if (!(profile instanceof A2dpProfile)) {
226             return;
227         }
228         BluetoothDevice device = mCachedDevice.getDevice();
229         A2dpProfile a2dp = (A2dpProfile) profile;
230         if (a2dp.isProfileReady() && a2dp.supportsHighQualityAudio(device)) {
231             SwitchPreference highQualityAudioPref = new SwitchPreference(
232                     mProfilesContainer.getContext());
233             highQualityAudioPref.setKey(HIGH_QUALITY_AUDIO_PREF_TAG);
234             highQualityAudioPref.setVisible(false);
235             highQualityAudioPref.setOnPreferenceClickListener(clickedPref -> {
236                 boolean enable = ((SwitchPreference) clickedPref).isChecked();
237                 a2dp.setHighQualityAudioEnabled(mCachedDevice.getDevice(), enable);
238                 return true;
239             });
240             mProfilesContainer.addPreference(highQualityAudioPref);
241         }
242     }
243 
244     @Override
onPause()245     public void onPause() {
246         super.onPause();
247         mProfileManager.removeServiceListener(this);
248     }
249 
250     @Override
onResume()251     public void onResume() {
252         super.onResume();
253         mProfileManager.addServiceListener(this);
254     }
255 
256     @Override
onServiceConnected()257     public void onServiceConnected() {
258         refresh();
259     }
260 
261     @Override
onServiceDisconnected()262     public void onServiceDisconnected() {
263         refresh();
264     }
265 
266     /**
267      * Refreshes the state of the switches for all profiles, possibly adding or removing switches as
268      * needed.
269      */
270     @Override
refresh()271     protected void refresh() {
272         for (LocalBluetoothProfile profile : getProfiles()) {
273             if (!profile.isProfileReady()) {
274                 continue;
275             }
276             SwitchPreference pref = mProfilesContainer.findPreference(
277                     profile.toString());
278             if (pref == null) {
279                 pref = createProfilePreference(mProfilesContainer.getContext(), profile);
280                 mProfilesContainer.addPreference(pref);
281                 maybeAddHighQualityAudioPref(profile);
282             }
283             refreshProfilePreference(pref, profile);
284         }
285         for (LocalBluetoothProfile removedProfile : mCachedDevice.getRemovedProfiles()) {
286             final SwitchPreference pref = mProfilesContainer.findPreference(
287                     removedProfile.toString());
288             if (pref != null) {
289                 mProfilesContainer.removePreference(pref);
290             }
291         }
292 
293         Preference preference = mProfilesContainer.findPreference(KEY_BOTTOM_PREFERENCE);
294         if (preference == null) {
295             preference = new Preference(mContext);
296             preference.setLayoutResource(R.layout.preference_bluetooth_profile_category);
297             preference.setEnabled(false);
298             preference.setKey(KEY_BOTTOM_PREFERENCE);
299             preference.setOrder(ORDINAL);
300             preference.setSelectable(false);
301             mProfilesContainer.addPreference(preference);
302         }
303     }
304 
305     @Override
getPreferenceKey()306     public String getPreferenceKey() {
307         return KEY_PROFILES_GROUP;
308     }
309 }