• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.google.android.tv.btservices.settings;
18 
19 import android.app.FragmentManager;
20 import android.app.admin.DevicePolicyManager;
21 import android.bluetooth.BluetoothDevice;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.os.Looper;
27 import android.os.Message;
28 import android.os.UserHandle;
29 import android.os.UserManager;
30 import android.provider.Settings;
31 import android.text.TextUtils;
32 import android.util.Log;
33 
34 import androidx.leanback.preference.LeanbackPreferenceFragment;
35 import androidx.preference.Preference;
36 import androidx.preference.PreferenceCategory;
37 import androidx.preference.PreferenceGroup;
38 import androidx.preference.PreferenceScreen;
39 
40 import com.android.settingslib.RestrictedLockUtils;
41 import com.android.settingslib.RestrictedLockUtilsInternal;
42 import com.android.settingslib.RestrictedPreference;
43 
44 import com.google.android.tv.btservices.BluetoothUtils;
45 import com.google.android.tv.btservices.Configuration;
46 import com.google.android.tv.btservices.R;
47 import com.google.android.tv.btservices.SettingsUtils;
48 
49 import java.util.ArrayList;
50 import java.util.Arrays;
51 import java.util.Collections;
52 import java.util.HashMap;
53 import java.util.HashSet;
54 import java.util.List;
55 import java.util.Set;
56 import java.util.function.Predicate;
57 
58 public class ConnectedDevicesPreferenceFragment extends LeanbackPreferenceFragment {
59 
60     private static final String TAG = "Atv.DevsPrefFragment";
61 
62     public interface Provider {
isCecEnabled()63         boolean isCecEnabled();
getBluetoothDevices()64         List<BluetoothDevice> getBluetoothDevices();
getBluetoothDeviceProvider()65         BluetoothDeviceProvider getBluetoothDeviceProvider();
66     }
67 
68     private interface BtPreferenceCreator {
create(Context context, BluetoothDeviceProvider provider, BluetoothDevice device)69         Preference create(Context context, BluetoothDeviceProvider provider,
70                 BluetoothDevice device);
71     }
72 
73     static final String KEY_ACCESSORIES = "accessories";
74     static final String KEY_OFFICIAL_REMOTES = "official_remotes";
75     static final String KEY_CONNECTED_DEVICES = "connected_devices";
76     static final String KEY_PAIRED_DEVICES = "paired_devices";
77     static final String KEY_PAIR_REMOTE = "pair_remote";
78     static final String KEY_PAIR_PHONE = "pair_phone";
79     static final String KEY_DEVICE_CONTROL = "device_control";
80     static final String KEY_CEC_TOGGLE = "cec_toggle";
81     static final String KEY_AXEL_TOGGLE = "axel_toggle";
82 
83     private static final Set<String> NON_BT_PREFERENCES =
84             new HashSet<>(Arrays.asList(KEY_PAIR_REMOTE));
85 
86     private static final int PAIR_REMOTE_ORDER = 0;
87     private static final int PAIR_PHONE_ORDER = 1;
88     private static final int CONNECTED_DEVICES_ORDER = 2;
89     private static final int PAIRED_DEVICES_ORDER = 3;
90     private static final int DEVICE_CONTROL_ORDER = 4;
91 
92     // Assuming we won't have that many BT devices connected.
93     private static final int LAST_DEVICE_ORDER = 10000;
94     private static final int UPDATE_DELAY_MS = 500;
95 
96     private PreferenceGroup mPrefGroup;
97     private static final int MSG_POP_DEVICE_FRAGMENT = 1;
98     private final Handler mHandler = new Handler(Looper.getMainLooper()) {
99         @Override
100         public void handleMessage(Message m) {
101             if (m.what == MSG_POP_DEVICE_FRAGMENT) {
102                 popDevicePreferenceImpl((String) m.obj);
103             }
104         }
105     };
106 
107     // Add a local provider proxy to enable custom events on certain actions.
108     private BluetoothDeviceProvider mLocalBluetoothDeviceProvider =
109             new LocalBluetoothDeviceProvider() {
110                 final BluetoothDeviceProvider getHostBluetoothDeviceProvider() {
111                     Provider provider = getProvider();
112                     if (provider != null) {
113                         return provider.getBluetoothDeviceProvider();
114                     }
115                     return null;
116                 }
117 
118                 @Override
119                 public void forgetDevice(BluetoothDevice device) {
120                     popDevicePreference(device);
121                     super.forgetDevice(device);
122                 }
123 
124                 @Override
125                 public void renameDevice(BluetoothDevice device, String newName) {
126                     popDevicePreference(device);
127                     super.renameDevice(device, newName);
128                 }
129             };
130 
newInstance()131     public static ConnectedDevicesPreferenceFragment newInstance() {
132         return new ConnectedDevicesPreferenceFragment();
133     }
134 
findOrCreateCategory(Context context, PreferenceGroup group, String key, int title, boolean defaultShow)135     private static PreferenceCategory findOrCreateCategory(Context context, PreferenceGroup group,
136             String key, int title, boolean defaultShow) {
137         PreferenceCategory category = (PreferenceCategory) group.findPreference(key);
138         if (category == null) {
139             category = new PreferenceCategory(context);
140             category.setKey(key);
141             category.setTitle(title);
142             category.setLayoutResource(R.layout.preference_category_compact_layout);
143             category.setVisible(defaultShow);
144             group.addPreference(category);
145         }
146         return category;
147     }
148 
setComplement(Set<String> a, Set<String> b)149     private static Set<String> setComplement(Set<String> a, Set<String> b) {
150         Set<String> c = new HashSet<>();
151         for (String s : a) {
152             if (b.contains(s)) {
153                 continue;
154             }
155             c.add(s);
156         }
157         return c;
158     }
159 
getBtDevices(PreferenceCategory category)160     private static Set<String> getBtDevices(PreferenceCategory category) {
161         int count = category.getPreferenceCount();
162         Set<String> oldDevices = new HashSet<>();
163         for (int i = 0; i < count; i++) {
164             Preference pref = category.getPreference(i);
165             if (NON_BT_PREFERENCES.contains(pref.getKey())) {
166                 continue;
167             }
168             oldDevices.add(pref.getKey());
169         }
170         return oldDevices;
171     }
172 
updateBtDevicePreference(Context context, BluetoothDeviceProvider provider, BluetoothDevice device, Preference pref)173     private static void updateBtDevicePreference(Context context, BluetoothDeviceProvider provider,
174             BluetoothDevice device, Preference pref) {
175         int batteryLevel = provider.getBatteryLevel(device);
176         pref.setKey(device.getAddress());
177         pref.setTitle(BluetoothUtils.getName(device));
178         if (provider.hasUpgrade(device)) {
179             pref.setSummary(R.string.settings_bt_update_available);
180         } else {
181             if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
182                 if (provider.isBatteryLow(device)) {
183                     pref.setSummary(R.string.settings_bt_battery_low_warning);
184                 } else {
185                     pref.setSummary(context.getString(R.string.settings_remote_battery_level,
186                         String.valueOf(batteryLevel)));
187                 }
188             } else {
189                 pref.setSummary(null);
190             }
191         }
192         pref.setIcon(BluetoothUtils.getIcon(context, device));
193     }
194 
createConnectedBtPreference(Context context, BluetoothDeviceProvider provider, BluetoothDevice device)195     private static Preference createConnectedBtPreference(Context context,
196             BluetoothDeviceProvider provider, BluetoothDevice device) {
197         Preference pref = new Preference(context);
198         pref.setKey(device.getAddress());
199         pref.setLayoutResource(R.layout.preference_item_layout);
200         updateBtDevicePreference(context, provider, device, pref);
201         pref.setVisible(true);
202         pref.setSelectable(true);
203         BluetoothDevicePreferenceFragment.buildArgs(pref.getExtras(), device);
204         RestrictedLockUtils.EnforcedAdmin admin =
205                 RestrictedLockUtilsInternal.checkIfRestrictionEnforced(context,
206                         UserManager.DISALLOW_CONFIG_BLUETOOTH, UserHandle.myUserId());
207         if (admin == null) {
208             pref.setFragment(BluetoothDevicePreferenceFragment.class.getCanonicalName());
209         } else {
210             Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(context, admin);
211             intent.putExtra(DevicePolicyManager.EXTRA_RESTRICTION,
212                     UserManager.DISALLOW_CONFIG_BLUETOOTH);
213             pref.setIntent(intent);
214         }
215 
216         return pref;
217     }
218 
createPairedBtPreference(Context context, BluetoothDeviceProvider provider, BluetoothDevice device)219     private static Preference createPairedBtPreference(Context context,
220             BluetoothDeviceProvider provider, BluetoothDevice device) {
221         Preference pref = createConnectedBtPreference(context, provider, device);
222         // Do not show any metadata.
223         pref.setSummary(null);
224         return pref;
225     }
226 
getProvider()227     private Provider getProvider() {
228         return getActivity() instanceof Provider ? (Provider) getActivity() : null;
229     }
230 
getBluetoothDeviceProvider()231     public BluetoothDeviceProvider getBluetoothDeviceProvider() {
232         return mLocalBluetoothDeviceProvider;
233     }
234 
updateBtDevices(Context context, PreferenceCategory category, Predicate<BluetoothDevice> filterFunc, BtPreferenceCreator prefCreator)235     private int updateBtDevices(Context context, PreferenceCategory category,
236             Predicate<BluetoothDevice> filterFunc, BtPreferenceCreator prefCreator) {
237         Provider stateProvider = getProvider();
238         BluetoothDeviceProvider btProvider = getBluetoothDeviceProvider();
239         if (stateProvider == null) {
240             return 0;
241         }
242 
243         Set<String> oldDevices = getBtDevices(category);
244         Set<String> currentDevices = new HashSet<>();
245         List<BluetoothDevice> btDevices = stateProvider.getBluetoothDevices();
246         HashMap<String, BluetoothDevice> addressToDevice = new HashMap<>();
247         for (BluetoothDevice device : btDevices) {
248             if (!filterFunc.test(device)) {
249                 continue;
250             }
251             addressToDevice.put(device.getAddress(), device);
252             currentDevices.add(device.getAddress());
253         }
254 
255         final Set<String> lostDevices = setComplement(oldDevices, currentDevices);
256         final Set<String> newDevices = setComplement(currentDevices, oldDevices);
257         final Set<String> updatingDevices = setComplement(oldDevices, lostDevices);
258         for (String s : lostDevices) {
259             Preference pref = category.findPreference(s);
260             category.removePreference(pref);
261         }
262 
263         int count = updatingDevices.size();
264         for (String s : newDevices) {
265             BluetoothDevice device = addressToDevice.get(s);
266             Preference pref = prefCreator.create(context, btProvider, device);
267             pref.setOrder(count++);
268             category.addPreference(pref);
269             if (!BluetoothUtils.isConnected(device)) {
270                 popDevicePreference(device);
271             }
272         }
273 
274         for (String s : updatingDevices) {
275             BluetoothDevice device = addressToDevice.get(s);
276             Preference pref = category.findPreference(s);
277             pref.setVisible(true);
278             updateBtDevicePreference(context, btProvider, device, pref);
279             if (!BluetoothUtils.isConnected(device)) {
280                 popDevicePreference(device);
281             }
282         }
283 
284         // Lexicographically order the devices
285         final List<String> allDevices = new ArrayList<>(currentDevices);
286         Collections.sort(allDevices);
287         for (int i = 0; i < allDevices.size(); i++) {
288             category.findPreference(allDevices.get(i)).setOrder(i);
289         }
290         return allDevices.size();
291     }
292 
updatePairedDevices()293     public void updatePairedDevices() {
294         Context context = getContext();
295         if (context == null) {
296             return;
297         }
298         PreferenceCategory category = findOrCreateCategory(context, mPrefGroup, KEY_PAIRED_DEVICES,
299                 R.string.settings_devices_paired, true);
300         category.setOrder(PAIRED_DEVICES_ORDER);
301         category.setOrderingAsAdded(true);
302         int updatedDevices = updateBtDevices(context, category, BluetoothUtils::isBonded,
303                 ConnectedDevicesPreferenceFragment::createPairedBtPreference);
304         category.setVisible(updatedDevices > 0);
305     }
306 
updateConnectedDevices()307     public void updateConnectedDevices() {
308         Context context = getContext();
309         if (context == null) {
310             return;
311         }
312         PreferenceCategory category = findOrCreateCategory(context, mPrefGroup,
313                 KEY_CONNECTED_DEVICES, R.string.settings_devices_connected, false);
314         category.setOrder(CONNECTED_DEVICES_ORDER);
315 
316         Provider stateProvider = getActivity() instanceof Provider ? (Provider) getActivity() :
317                 null;
318 
319         int updatedDevices = updateBtDevices(context, category, BluetoothUtils::isConnected,
320                 ConnectedDevicesPreferenceFragment::createConnectedBtPreference);
321         category.setVisible(updatedDevices > 0);
322     }
323 
isCecSettingsEnabled(Context context)324     protected static boolean isCecSettingsEnabled(Context context) {
325         return Configuration.get(context).isEnabled(R.bool.cec_settings_enabled);
326     }
327 
isAxelSettingsEnabled(Context context)328     protected static boolean isAxelSettingsEnabled(Context context) {
329         return Configuration.get(context).isEnabled(R.bool.axel_settings_enabled);
330     }
331 
updateCec(Context context)332     public void updateCec(Context context) {
333         if (!isCecSettingsEnabled(context)) {
334             return;
335         }
336 
337         PreferenceCategory category = findOrCreateCategory(context, mPrefGroup,
338                 KEY_DEVICE_CONTROL, R.string.settings_devices_control, true);
339         category.setOrder(DEVICE_CONTROL_ORDER);
340         Preference cecPref = category.findPreference(KEY_CEC_TOGGLE);
341         if (cecPref == null) {
342             cecPref = new Preference(context);
343             cecPref.setKey(KEY_CEC_TOGGLE);
344             cecPref.setTitle(R.string.settings_hdmi_cec);
345             cecPref.setSummary(R.string.settings_enabled);
346             final String cecPrefClassName = CecPreferenceFragment.class.getCanonicalName();
347             cecPref.setFragment(cecPrefClassName);
348             category.addPreference(cecPref);
349         }
350         Provider stateProvider = getActivity() instanceof Provider ? (Provider) getActivity() :
351                 null;
352         if (stateProvider != null && !stateProvider.isCecEnabled()) {
353             cecPref.setSummary(R.string.settings_disabled);
354         } else {
355             cecPref.setSummary(R.string.settings_enabled);
356         }
357     }
358 
updateAll(Context context)359     private void updateAll(Context context) {
360         updateConnectedDevices();
361         updatePairedDevices();
362         updateCec(context);
363     }
364 
popDevicePreference(BluetoothDevice device)365     private void popDevicePreference(BluetoothDevice device) {
366         mHandler.removeMessages(MSG_POP_DEVICE_FRAGMENT);
367         Message msg = mHandler.obtainMessage(MSG_POP_DEVICE_FRAGMENT, device.getAddress());
368         // We need to add a delay to make sure we are not popping the top fragment and updating the
369         // fragment underneath at the same time.
370         mHandler.sendMessageDelayed(msg, UPDATE_DELAY_MS);
371     }
372 
popDevicePreferenceImpl(String address)373     private void popDevicePreferenceImpl(String address) {
374         FragmentManager fragmentManager = getFragmentManager();
375         if (fragmentManager == null) {
376             Log.w(TAG, "popDevicePreferenceImpl: fragmentManager is null");
377             return;
378         }
379         boolean shouldPop = false;
380         for (android.app.Fragment frag : fragmentManager.getFragments()) {
381             if (frag instanceof BluetoothDevicePreferenceFragment) {
382                 BluetoothDevice device = ((BluetoothDevicePreferenceFragment) frag).getDevice();
383                 shouldPop = device != null && TextUtils.equals(device.getAddress(), address);
384                 break;
385             }
386         }
387         if (shouldPop) {
388             fragmentManager.popBackStackImmediate();
389         }
390     }
391 
392     @Override
onResume()393     public void onResume() {
394         super.onResume();
395         // Delay the update to avoid jank when the panel is sliding in.
396         mHandler.postDelayed(() -> updateAll(getPreferenceManager().getContext()), UPDATE_DELAY_MS);
397     }
398 
399     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)400     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
401         final Context preferenceContext = getPreferenceManager().getContext();
402         mPrefGroup = getPreferenceManager().createPreferenceScreen(preferenceContext);
403         mPrefGroup.setTitle(R.string.connected_devices_pref_title);
404         mPrefGroup.setOrderingAsAdded(false);
405 
406         RestrictedLockUtils.EnforcedAdmin admin =
407                 RestrictedLockUtilsInternal.checkIfRestrictionEnforced(getPreferenceManager().getContext(),
408                         UserManager.DISALLOW_CONFIG_BLUETOOTH, UserHandle.myUserId());
409         Preference pairRemotePref = mPrefGroup.findPreference(KEY_PAIR_REMOTE);
410 
411         if (pairRemotePref == null) {
412             pairRemotePref = new Preference(preferenceContext);
413             pairRemotePref.setLayoutResource(R.layout.preference_item_layout);
414             pairRemotePref.setKey(KEY_PAIR_REMOTE);
415             pairRemotePref.setTitle(R.string.settings_pair_remote);
416             pairRemotePref.setIcon(R.drawable.ic_baseline_add_24dp);
417             pairRemotePref.setOrder(PAIR_REMOTE_ORDER);
418             mPrefGroup.addPreference(pairRemotePref);
419         }
420         if (admin != null) {
421             Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(preferenceContext,
422                     admin);
423             intent.putExtra(DevicePolicyManager.EXTRA_RESTRICTION,
424                     UserManager.DISALLOW_CONFIG_BLUETOOTH);
425             pairRemotePref.setIntent(intent);
426         }
427         setPreferenceScreen((PreferenceScreen) mPrefGroup);
428     }
429 
430     @Override
onPreferenceTreeClick(Preference preference)431     public boolean onPreferenceTreeClick(Preference preference) {
432         final String key = preference.getKey();
433         if (KEY_PAIR_REMOTE.equals(key) && preference.getIntent() == null) {
434             SettingsUtils.sendPairingIntent(getContext(), null);
435         }
436         return super.onPreferenceTreeClick(preference);
437     }
438 }
439