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