1 /*
2  * Copyright (C) 2011 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.BluetoothAdapter;
20 import android.bluetooth.BluetoothDevice;
21 import android.os.Bundle;
22 import android.os.SystemProperties;
23 import android.text.BidiFormatter;
24 import android.util.Log;
25 
26 import androidx.annotation.VisibleForTesting;
27 import androidx.preference.Preference;
28 import androidx.preference.PreferenceCategory;
29 import androidx.preference.PreferenceGroup;
30 
31 import com.android.settings.R;
32 import com.android.settings.dashboard.RestrictedDashboardFragment;
33 import com.android.settingslib.bluetooth.BluetoothCallback;
34 import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
35 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
36 import com.android.settingslib.bluetooth.LocalBluetoothManager;
37 
38 import java.util.ArrayList;
39 import java.util.Collection;
40 import java.util.HashMap;
41 import java.util.List;
42 
43 /**
44  * Parent class for settings fragments that contain a list of Bluetooth
45  * devices.
46  *
47  * @see DevicePickerFragment
48  */
49 // TODO: Refactor this fragment
50 public abstract class DeviceListPreferenceFragment extends
51         RestrictedDashboardFragment implements BluetoothCallback {
52 
53     private static final String TAG = "DeviceListPreferenceFragment";
54 
55     private static final String KEY_BT_SCAN = "bt_scan";
56 
57     // Copied from BluetoothDeviceNoNamePreferenceController.java
58     private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
59             "persist.bluetooth.showdeviceswithoutnames";
60 
61     private BluetoothDeviceFilter.Filter mFilter;
62 
63     @VisibleForTesting
64     boolean mScanEnabled;
65 
66     BluetoothDevice mSelectedDevice;
67 
68     BluetoothAdapter mBluetoothAdapter;
69     LocalBluetoothManager mLocalManager;
70 
71     @VisibleForTesting
72     PreferenceGroup mDeviceListGroup;
73 
74     final HashMap<CachedBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap =
75             new HashMap<>();
76     final List<BluetoothDevice> mSelectedList = new ArrayList<>();
77 
78     boolean mShowDevicesWithoutNames;
79 
DeviceListPreferenceFragment(String restrictedKey)80     DeviceListPreferenceFragment(String restrictedKey) {
81         super(restrictedKey);
82         mFilter = BluetoothDeviceFilter.ALL_FILTER;
83     }
84 
setFilter(BluetoothDeviceFilter.Filter filter)85     final void setFilter(BluetoothDeviceFilter.Filter filter) {
86         mFilter = filter;
87     }
88 
setFilter(int filterType)89     final void setFilter(int filterType) {
90         mFilter = BluetoothDeviceFilter.getFilter(filterType);
91     }
92 
93     @Override
onCreate(Bundle savedInstanceState)94     public void onCreate(Bundle savedInstanceState) {
95         super.onCreate(savedInstanceState);
96 
97         mLocalManager = Utils.getLocalBtManager(getActivity());
98         if (mLocalManager == null) {
99             Log.e(TAG, "Bluetooth is not supported on this device");
100             return;
101         }
102         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
103         mShowDevicesWithoutNames = SystemProperties.getBoolean(
104                 BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false);
105 
106         initPreferencesFromPreferenceScreen();
107 
108         mDeviceListGroup = (PreferenceCategory) findPreference(getDeviceListKey());
109     }
110 
111     /** find and update preference that already existed in preference screen */
initPreferencesFromPreferenceScreen()112     abstract void initPreferencesFromPreferenceScreen();
113 
114     @Override
onStart()115     public void onStart() {
116         super.onStart();
117         if (mLocalManager == null || isUiRestricted()) return;
118 
119         mLocalManager.setForegroundActivity(getActivity());
120         mLocalManager.getEventManager().registerCallback(this);
121     }
122 
123     @Override
onStop()124     public void onStop() {
125         super.onStop();
126         if (mLocalManager == null || isUiRestricted()) {
127             return;
128         }
129 
130         removeAllDevices();
131         mLocalManager.setForegroundActivity(null);
132         mLocalManager.getEventManager().unregisterCallback(this);
133     }
134 
removeAllDevices()135     void removeAllDevices() {
136         mDevicePreferenceMap.clear();
137         mDeviceListGroup.removeAll();
138     }
139 
addCachedDevices()140     void addCachedDevices() {
141         Collection<CachedBluetoothDevice> cachedDevices =
142                 mLocalManager.getCachedDeviceManager().getCachedDevicesCopy();
143         for (CachedBluetoothDevice cachedDevice : cachedDevices) {
144             onDeviceAdded(cachedDevice);
145         }
146     }
147 
148     @Override
onPreferenceTreeClick(Preference preference)149     public boolean onPreferenceTreeClick(Preference preference) {
150         if (KEY_BT_SCAN.equals(preference.getKey())) {
151             startScanning();
152             return true;
153         }
154 
155         if (preference instanceof BluetoothDevicePreference) {
156             BluetoothDevicePreference btPreference = (BluetoothDevicePreference) preference;
157             CachedBluetoothDevice device = btPreference.getCachedDevice();
158             mSelectedDevice = device.getDevice();
159             mSelectedList.add(mSelectedDevice);
160             onDevicePreferenceClick(btPreference);
161             return true;
162         }
163 
164         return super.onPreferenceTreeClick(preference);
165     }
166 
onDevicePreferenceClick(BluetoothDevicePreference btPreference)167     void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {
168         btPreference.onClicked();
169     }
170 
171     @Override
onDeviceAdded(CachedBluetoothDevice cachedDevice)172     public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
173         if (mDevicePreferenceMap.get(cachedDevice) != null) {
174             return;
175         }
176 
177         // Prevent updates while the list shows one of the state messages
178         if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) return;
179 
180         if (mFilter.matches(cachedDevice.getDevice())) {
181             createDevicePreference(cachedDevice);
182         }
183     }
184 
createDevicePreference(CachedBluetoothDevice cachedDevice)185     void createDevicePreference(CachedBluetoothDevice cachedDevice) {
186         if (mDeviceListGroup == null) {
187             Log.w(TAG, "Trying to create a device preference before the list group/category "
188                     + "exists!");
189             return;
190         }
191 
192         String key = cachedDevice.getDevice().getAddress();
193         BluetoothDevicePreference preference = (BluetoothDevicePreference) getCachedPreference(key);
194 
195         if (preference == null) {
196             preference = new BluetoothDevicePreference(getPrefContext(), cachedDevice,
197                     mShowDevicesWithoutNames, BluetoothDevicePreference.SortType.TYPE_FIFO);
198             preference.setKey(key);
199             //Set hideSecondTarget is true if it's bonded device.
200             preference.hideSecondTarget(true);
201             mDeviceListGroup.addPreference(preference);
202         }
203 
204         initDevicePreference(preference);
205         mDevicePreferenceMap.put(cachedDevice, preference);
206     }
207 
initDevicePreference(BluetoothDevicePreference preference)208     protected void initDevicePreference(BluetoothDevicePreference preference) {
209         // Does nothing by default
210     }
211 
212     @VisibleForTesting
updateFooterPreference(Preference myDevicePreference)213     void updateFooterPreference(Preference myDevicePreference) {
214         final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
215 
216         myDevicePreference.setTitle(getString(
217                 R.string.bluetooth_footer_mac_message,
218                 bidiFormatter.unicodeWrap(mBluetoothAdapter.getAddress())));
219     }
220 
221     @Override
onDeviceDeleted(CachedBluetoothDevice cachedDevice)222     public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
223         BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice);
224         if (preference != null) {
225             mDeviceListGroup.removePreference(preference);
226         }
227     }
228 
229     @VisibleForTesting
enableScanning()230     void enableScanning() {
231         // BluetoothAdapter already handles repeated scan requests
232         if (!mScanEnabled) {
233             startScanning();
234             mScanEnabled = true;
235         }
236     }
237 
238     @VisibleForTesting
disableScanning()239     void disableScanning() {
240         if (mScanEnabled) {
241             stopScanning();
242             mScanEnabled = false;
243         }
244     }
245 
246     @Override
onScanningStateChanged(boolean started)247     public void onScanningStateChanged(boolean started) {
248         if (!started && mScanEnabled) {
249             startScanning();
250         }
251     }
252 
253     /**
254      * Add bluetooth device preferences to {@code preferenceGroup} which satisfy the {@code filter}
255      *
256      * This method will also (1) set the title for {@code preferenceGroup} and (2) change the
257      * default preferenceGroup and filter
258      * @param preferenceGroup
259      * @param titleId
260      * @param filter
261      * @param addCachedDevices
262      */
addDeviceCategory(PreferenceGroup preferenceGroup, int titleId, BluetoothDeviceFilter.Filter filter, boolean addCachedDevices)263     public void addDeviceCategory(PreferenceGroup preferenceGroup, int titleId,
264             BluetoothDeviceFilter.Filter filter, boolean addCachedDevices) {
265         cacheRemoveAllPrefs(preferenceGroup);
266         preferenceGroup.setTitle(titleId);
267         mDeviceListGroup = preferenceGroup;
268         if (addCachedDevices) {
269             // Don't show bonded devices when screen turned back on
270             setFilter(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER);
271             addCachedDevices();
272         }
273         setFilter(filter);
274         preferenceGroup.setEnabled(true);
275         removeCachedPrefs(preferenceGroup);
276     }
277 
278     /**
279      * Return the key of the {@link PreferenceGroup} that contains the bluetooth devices
280      */
getDeviceListKey()281     public abstract String getDeviceListKey();
282 
shouldShowDevicesWithoutNames()283     public boolean shouldShowDevicesWithoutNames() {
284         return mShowDevicesWithoutNames;
285     }
286 
startScanning()287     void startScanning() {
288         if (!mBluetoothAdapter.isDiscovering()) {
289             mBluetoothAdapter.startDiscovery();
290         }
291     }
292 
stopScanning()293     void stopScanning() {
294         if (mBluetoothAdapter.isDiscovering()) {
295             mBluetoothAdapter.cancelDiscovery();
296         }
297     }
298 }
299