1 /* 2 * Copyright (C) 2020 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.tv.settings.accessories; 18 19 20 import static android.app.PendingIntent.FLAG_IMMUTABLE; 21 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; 22 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND; 23 24 import static com.android.tv.settings.accessories.AddAccessoryActivity.ACTION_CONNECT_INPUT; 25 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.ACTION_FIND_MY_REMOTE; 26 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.ACTION_TOGGLE_CHANGED; 27 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.ACTIVE_AUDIO_OUTPUT; 28 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.BLUETOOTH_ON; 29 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.EXTRA_TOGGLE_STATE; 30 import static com.android.tv.settings.accessories.ConnectedDevicesSliceBroadcastReceiver.EXTRA_TOGGLE_TYPE; 31 import static com.android.tv.settings.accessories.ConnectedDevicesSliceUtils.EXTRAS_SLICE_URI; 32 import static com.android.tv.settings.accessories.ConnectedDevicesSliceUtils.FIND_MY_REMOTE_PHYSICAL_BUTTON_ENABLED_SETTING; 33 import static com.android.tv.settings.accessories.ConnectedDevicesSliceUtils.isFindMyRemoteButtonEnabled; 34 35 import android.app.PendingIntent; 36 import android.app.admin.DevicePolicyManager; 37 import android.app.tvsettings.TvSettingsEnums; 38 import android.bluetooth.BluetoothDevice; 39 import android.content.ComponentName; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.ServiceConnection; 43 import android.content.pm.ResolveInfo; 44 import android.net.Uri; 45 import android.os.Bundle; 46 import android.os.Handler; 47 import android.os.IBinder; 48 import android.os.Looper; 49 import android.os.StrictMode; 50 import android.os.UserHandle; 51 import android.os.UserManager; 52 import android.provider.Settings; 53 import android.text.TextUtils; 54 import android.util.ArrayMap; 55 import android.util.Log; 56 57 import androidx.annotation.IntegerRes; 58 import androidx.core.graphics.drawable.IconCompat; 59 import androidx.slice.Slice; 60 import androidx.slice.SliceProvider; 61 62 import com.android.settingslib.RestrictedLockUtils; 63 import com.android.settingslib.RestrictedLockUtilsInternal; 64 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 65 import com.android.settingslib.media.flags.Flags; 66 import com.android.tv.settings.R; 67 import com.android.tv.twopanelsettings.slices.builders.PreferenceSliceBuilder; 68 import com.android.tv.twopanelsettings.slices.builders.PreferenceSliceBuilder.RowBuilder; 69 70 import java.util.ArrayList; 71 import java.util.Collections; 72 import java.util.HashMap; 73 import java.util.HashSet; 74 import java.util.List; 75 import java.util.Map; 76 import java.util.Set; 77 78 /** The SliceProvider for "connected devices" settings */ 79 public class ConnectedDevicesSliceProvider extends SliceProvider implements 80 BluetoothDeviceProvider.Listener { 81 82 private static final String TAG = "ConnectedDevices"; 83 private static final boolean DEBUG = false; 84 private static final boolean DISCONNECT_PREFERENCE_ENABLED = false; 85 private static final int ACTIVE_AUDIO_OUTPUT_INTENT_REQUEST_CODE = 9; 86 private final Map<Uri, Integer> mPinnedUris = new ArrayMap<>(); 87 private final Handler mHandler = new Handler(Looper.getMainLooper()); 88 89 private boolean mBtDeviceServiceBound; 90 private BluetoothDevicesService.LocalBinder mBtDeviceServiceBinder; 91 92 private final BluetoothDeviceProvider mLocalBluetoothDeviceProvider = 93 new LocalBluetoothDeviceProvider() { 94 @Override 95 BluetoothDeviceProvider getHostBluetoothDeviceProvider() { 96 return getBluetoothDeviceProvider(); 97 } 98 }; 99 100 private final ServiceConnection mBtDeviceServiceConnection = 101 new SimplifiedConnection() { 102 103 @Override 104 public void onServiceConnected(ComponentName className, IBinder service) { 105 mBtDeviceServiceBinder = (BluetoothDevicesService.LocalBinder) service; 106 mBtDeviceServiceBinder.addListener(ConnectedDevicesSliceProvider.this); 107 getContext().getContentResolver() 108 .notifyChange(ConnectedDevicesSliceUtils.GENERAL_SLICE_URI, null); 109 } 110 111 @Override 112 protected void cleanUp() { 113 if (mBtDeviceServiceBinder != null) { 114 mBtDeviceServiceBinder.removeListener(ConnectedDevicesSliceProvider.this); 115 } 116 mBtDeviceServiceBinder = null; 117 } 118 }; 119 120 static final String KEY_BLUETOOTH_TOGGLE = "bluetooth_toggle"; 121 static final String KEY_PAIR_REMOTE = "pair_remote"; 122 static final String KEY_ACCESSORIES = "accessories"; 123 static final String KEY_OFFICIAL_REMOTES_CATEGORY = "official_remotes_category"; 124 // Preference key for the remote bundled with the device (Bluetooth based) 125 static final String KEY_OFFICIAL_REMOTE = "official_remote"; 126 // Preference key for the remote bundled with the device (IR based) 127 static final String KEY_IR = "ir"; 128 static final String KEY_CONNECT = "connect"; 129 static final String KEY_DISCONNECT = "disconnect"; 130 static final String KEY_RENAME = "rename"; 131 static final String KEY_FORGET = "forget"; 132 static final String KEY_EXTRAS_DEVICE = "extra_devices"; 133 static final String KEY_BLUETOOTH_DEVICE_INFO = "bluetooth_device_info"; 134 static final String KEY_FIND_MY_REMOTE_TOGGLE = "fmr_toggle"; 135 static final String KEY_TOGGLE_ACTIVE_AUDIO_OUTPUT = "toggle_active_audio_output"; 136 137 static final int YES = R.string.general_action_yes; 138 static final int NO = R.string.general_action_no; 139 static final int[] YES_NO_ARGS = {YES, NO}; 140 141 @Override onCreateSliceProvider()142 public boolean onCreateSliceProvider() { 143 return true; 144 } 145 146 @Override onCreatePermissionRequest(Uri sliceUri, String callingPackage)147 public PendingIntent onCreatePermissionRequest(Uri sliceUri, String callingPackage) { 148 final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS); 149 final PendingIntent noOpIntent = PendingIntent.getActivity( 150 getContext(), 0, settingsIntent, PendingIntent.FLAG_IMMUTABLE); 151 return noOpIntent; 152 } 153 154 @Override onSlicePinned(Uri sliceUri)155 public void onSlicePinned(Uri sliceUri) { 156 mHandler.post(() -> { 157 if (DEBUG) { 158 Log.d(TAG, "Slice pinned: " + sliceUri); 159 } 160 Context context = getContext(); 161 if (!mBtDeviceServiceBound && context.bindService( 162 new Intent(context, AccessoryUtils.getBluetoothDeviceServiceClass()), 163 mBtDeviceServiceConnection, 164 Context.BIND_AUTO_CREATE)) { 165 mBtDeviceServiceBound = true; 166 } 167 if (!mPinnedUris.containsKey(sliceUri)) { 168 mPinnedUris.put(sliceUri, 0); 169 } 170 mPinnedUris.put(sliceUri, mPinnedUris.get(sliceUri) + 1); 171 }); 172 } 173 174 @Override onBindSlice(Uri sliceUri)175 public Slice onBindSlice(Uri sliceUri) { 176 if (DEBUG) { 177 Log.d(TAG, "onBindSlice: " + sliceUri); 178 } 179 if (getBluetoothDevices().isEmpty()) { 180 sliceUri = ConnectedDevicesSliceUtils.GENERAL_SLICE_URI; 181 } 182 StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); 183 try { 184 // Prevent StrictMode from throwing on access to shared preferences. 185 StrictMode.setThreadPolicy( 186 new StrictMode.ThreadPolicy.Builder(oldPolicy).permitDiskReads().build()); 187 if (ConnectedDevicesSliceUtils.isGeneralPath(sliceUri)) { 188 return createGeneralSlice(sliceUri); 189 } else if (ConnectedDevicesSliceUtils.isBluetoothDevicePath(sliceUri)) { 190 return createBluetoothDeviceSlice(sliceUri); 191 } else if (ConnectedDevicesSliceUtils.isFindMyRemotePath(sliceUri)) { 192 return createFindMyRemoteSlice(sliceUri); 193 } 194 } finally { 195 StrictMode.setThreadPolicy(oldPolicy); 196 } 197 return null; 198 } 199 200 @Override onSliceUnpinned(Uri sliceUri)201 public void onSliceUnpinned(Uri sliceUri) { 202 mHandler.post(() -> { 203 if (DEBUG) { 204 Log.d(TAG, "Slice unpinned: " + sliceUri); 205 } 206 Context context = getContext(); 207 // If at this point there is only one slice pinned, we need to unbind the service as 208 // there won't be any slice pinned after handleSliceUnpinned is called. 209 if (mPinnedUris.containsKey(sliceUri)) { 210 int newCount = mPinnedUris.get(sliceUri) - 1; 211 mPinnedUris.put(sliceUri, newCount); 212 if (newCount == 0) { 213 mPinnedUris.remove(sliceUri); 214 } 215 } 216 if (mPinnedUris.isEmpty() && mBtDeviceServiceBound) { 217 context.unbindService(mBtDeviceServiceConnection); 218 mBtDeviceServiceBound = false; 219 } 220 }); 221 } 222 223 // BluetoothDeviceProvider.Listener implementation 224 @Override onDeviceUpdated(BluetoothDevice device)225 public void onDeviceUpdated(BluetoothDevice device) { 226 getContext().getContentResolver() 227 .notifyChange(ConnectedDevicesSliceUtils.GENERAL_SLICE_URI, null); 228 notifyDeviceSlice(device); 229 } 230 231 // The initial slice in the Connected Device flow. createGeneralSlice(Uri sliceUri)232 private Slice createGeneralSlice(Uri sliceUri) { 233 PreferenceSliceBuilder psb = new PreferenceSliceBuilder(getContext(), sliceUri); 234 psb.addScreenTitle( 235 new RowBuilder() 236 .setTitle(getString(R.string.connected_devices_slice_pref_title)) 237 .setPageId(TvSettingsEnums.CONNECTED_SLICE)); 238 updateBluetoothToggle(psb); 239 updatePairingButton(psb); 240 updateConnectedDevices(psb); 241 updateOfficialRemoteSettings(psb); 242 updateFmr(psb); 243 return psb.build(); 244 } 245 246 // The slice page that shows detail information of a particular device. createBluetoothDeviceSlice(Uri sliceUri)247 private Slice createBluetoothDeviceSlice(Uri sliceUri) { 248 Context context = getContext(); 249 String deviceAddr = ConnectedDevicesSliceUtils.getDeviceAddr(sliceUri); 250 BluetoothDevice device = BluetoothDevicesService.findDevice(deviceAddr); 251 CachedBluetoothDevice cachedDevice = 252 AccessoryUtils.getCachedBluetoothDevice(getContext(), device); 253 String deviceName = ""; 254 if (device != null) { 255 deviceName = AccessoryUtils.getLocalName(device); 256 } 257 258 PreferenceSliceBuilder psb = new PreferenceSliceBuilder(getContext(), sliceUri); 259 psb.addScreenTitle( 260 new RowBuilder() 261 .setTitle(deviceName) 262 .setPageId(TvSettingsEnums.CONNECTED_SLICE_DEVICE_ENTRY)); 263 264 Bundle extras; 265 Intent i; 266 // Update "Use for TV audio". 267 // Set as active audio output device only connected devices that have audio capabilities 268 if (Flags.enableTvMediaOutputDialog() 269 && cachedDevice != null && !cachedDevice.isBusy() 270 && AccessoryUtils.isConnected(device) && cachedDevice.isConnected() 271 && (AccessoryUtils.isBluetoothHeadset(device) 272 || AccessoryUtils.hasAudioProfile(cachedDevice))) { 273 boolean isActive = AccessoryUtils.isActiveAudioOutput(device); 274 275 Intent intent = new Intent(ACTION_TOGGLE_CHANGED); 276 intent.setClass(context, ConnectedDevicesSliceBroadcastReceiver.class); 277 intent.putExtra(EXTRA_TOGGLE_TYPE, ACTIVE_AUDIO_OUTPUT); 278 intent.putExtra(EXTRA_TOGGLE_STATE, !isActive); 279 intent.putExtra(KEY_EXTRAS_DEVICE, device); 280 281 PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 282 ACTIVE_AUDIO_OUTPUT_INTENT_REQUEST_CODE, intent, 283 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 284 285 // Update set/unset active audio output preference 286 RowBuilder activeAudioOutputPref = new RowBuilder() 287 .setKey(KEY_TOGGLE_ACTIVE_AUDIO_OUTPUT) 288 .setTitle(getString(R.string.bluetooth_toggle_active_audio_output_title)) 289 .setActionId( 290 TvSettingsEnums.CONNECTED_SLICE_DEVICE_ENTRY_TOGGLE_ACTIVE_AUDIO_OUTPUT) 291 .addSwitch(pendingIntent, 292 context.getText(R.string.bluetooth_toggle_active_audio_output_title), 293 isActive); 294 295 psb.addPreference(activeAudioOutputPref); 296 } 297 298 // Update "connect/disconnect preference" 299 if (cachedDevice != null && !cachedDevice.isBusy()) { 300 // Whether the device is actually connected from CachedBluetoothDevice's perspective. 301 boolean isConnected = AccessoryUtils.isConnected(device) && cachedDevice.isConnected(); 302 303 if (!isConnected || showDisconnectButton(device, context)) { 304 RowBuilder connectionActionPref = new RowBuilder() 305 .setKey(isConnected ? KEY_DISCONNECT : KEY_CONNECT) 306 .setTitle(getString((isConnected 307 ? R.string.bluetooth_disconnect_action_title 308 : R.string.bluetooth_connect_action_title))); 309 extras = new Bundle(); 310 i = new Intent(context, BluetoothActionActivity.class); 311 BluetoothActionFragment.prepareArgs( 312 extras, 313 isConnected ? KEY_DISCONNECT : KEY_CONNECT, 314 R.drawable.ic_baseline_bluetooth_searching_large, 315 isConnected 316 ? R.string.bluetooth_disconnect_confirm 317 : R.string.bluetooth_connect_confirm, 318 0, 319 YES_NO_ARGS, 320 deviceName, 321 isConnected ? 1 /* default to NO (index 1) */ : 0 /* default to YES */ 322 ); 323 i.putExtras(extras); 324 i.putExtra(KEY_EXTRAS_DEVICE, device); 325 PendingIntent pendingIntent = PendingIntent.getActivity( 326 context, 3, i, 327 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 328 Intent followUpIntent = 329 new Intent(context, ConnectedDevicesSliceBroadcastReceiver.class); 330 followUpIntent.putExtra(EXTRAS_SLICE_URI, sliceUri.toString()); 331 PendingIntent followupIntent = PendingIntent.getBroadcast( 332 context, 4, followUpIntent, 333 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 334 connectionActionPref.setPendingIntent(pendingIntent); 335 connectionActionPref.setFollowupPendingIntent(followupIntent); 336 psb.addPreference(connectionActionPref); 337 } 338 } 339 340 // Update "rename preference". 341 RowBuilder renamePref = new RowBuilder() 342 .setKey(KEY_RENAME) 343 .setTitle(getString(R.string.bluetooth_rename_action_title)) 344 .setActionId(TvSettingsEnums.CONNECTED_SLICE_DEVICE_ENTRY_RENAME); 345 extras = new Bundle(); 346 BluetoothActionFragment.prepareArgs( 347 extras, 348 KEY_RENAME, 349 R.drawable.ic_baseline_bluetooth_searching_large, 350 R.string.bluetooth_rename, 351 0, 352 null, 353 deviceName, 354 BluetoothActionFragment.DEFAULT_CHOICE_UNDEFINED 355 ); 356 i = new Intent(context, BluetoothActionActivity.class); 357 i.putExtra(KEY_EXTRAS_DEVICE, device); 358 i.putExtras(extras); 359 PendingIntent renamePendingIntent = PendingIntent.getActivity( 360 context, 5, i, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 361 362 Intent followUpIntent = new Intent(context, ConnectedDevicesSliceBroadcastReceiver.class); 363 followUpIntent.putExtra(EXTRAS_SLICE_URI, sliceUri.toString()); 364 PendingIntent renameFollowupIntent = PendingIntent.getBroadcast( 365 context, 6, followUpIntent, 366 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 367 renamePref.setFollowupPendingIntent(renameFollowupIntent); 368 renamePref.setPendingIntent(renamePendingIntent); 369 psb.addPreference(renamePref); 370 371 // Update "forget preference". 372 RowBuilder forgetPref = new RowBuilder() 373 .setKey(KEY_FORGET) 374 .setTitle(getString(R.string.bluetooth_forget_action_title)) 375 .setActionId(TvSettingsEnums.CONNECTED_SLICE_DEVICE_ENTRY_FORGET); 376 extras = new Bundle(); 377 i = new Intent(context, BluetoothActionActivity.class); 378 BluetoothActionFragment.prepareArgs( 379 extras, 380 KEY_FORGET, 381 R.drawable.ic_baseline_bluetooth_searching_large, 382 R.string.bluetooth_forget_confirm, 383 0, 384 YES_NO_ARGS, 385 deviceName, 386 1 /* default to NO (index 1) */ 387 ); 388 i.putExtras(extras); 389 i.putExtra(KEY_EXTRAS_DEVICE, device); 390 PendingIntent disconnectPendingIntent = PendingIntent.getActivity( 391 context, 7, i, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 392 followUpIntent = new Intent(context, ConnectedDevicesSliceBroadcastReceiver.class); 393 followUpIntent.putExtra(EXTRAS_SLICE_URI, sliceUri.toString()); 394 PendingIntent forgetFollowupIntent = PendingIntent.getBroadcast( 395 context, 8, followUpIntent, 396 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); 397 forgetPref.setPendingIntent(disconnectPendingIntent); 398 forgetPref.setFollowupPendingIntent(forgetFollowupIntent); 399 psb.addPreference(forgetPref); 400 401 // Update "bluetooth device info preference". 402 RowBuilder infoPref = new RowBuilder() 403 .setKey(KEY_BLUETOOTH_DEVICE_INFO) 404 .setIcon(IconCompat.createWithResource(context, R.drawable.ic_baseline_info_24dp)); 405 406 infoPref.addInfoItem(getString(R.string.bluetooth_serial_number_label), deviceAddr); 407 psb.addPreference(infoPref); 408 return psb.build(); 409 } 410 updateBluetoothToggle(PreferenceSliceBuilder psb)411 private void updateBluetoothToggle(PreferenceSliceBuilder psb) { 412 if (showBluetoothToggle()) { 413 Intent bluetoothToggleIntent; 414 if (AccessoryUtils.isBluetoothEnabled()) { 415 bluetoothToggleIntent = new Intent(getContext(), BluetoothActionActivity.class); 416 Bundle extras = new Bundle(); 417 BluetoothActionFragment.prepareArgs( 418 extras, 419 KEY_BLUETOOTH_TOGGLE, 420 R.drawable.ic_baseline_bluetooth_searching_large, 421 R.string.bluetooth_toggle_confirmation_dialog_title, 422 R.string.bluetooth_toggle_confirmation_dialog_summary, 423 YES_NO_ARGS, 424 null, 425 0 /* default to YES */ 426 ); 427 bluetoothToggleIntent.putExtras(extras); 428 } else { 429 bluetoothToggleIntent = new Intent(ACTION_TOGGLE_CHANGED); 430 bluetoothToggleIntent.setClass( 431 getContext(), ConnectedDevicesSliceBroadcastReceiver.class); 432 bluetoothToggleIntent.putExtra(EXTRA_TOGGLE_TYPE, BLUETOOTH_ON); 433 } 434 psb.addPreference( 435 new RowBuilder() 436 .setKey(KEY_BLUETOOTH_TOGGLE) 437 .setIcon(IconCompat.createWithResource( 438 getContext(), R.drawable.ic_bluetooth_raw)) 439 .setIconNeedsToBeProcessed(true) 440 .setTitle(getString(R.string.bluetooth_toggle_title)) 441 .addSwitch( 442 AccessoryUtils.isBluetoothEnabled() 443 ? PendingIntent.getActivity( 444 getContext(), 1, bluetoothToggleIntent, 445 PendingIntent.FLAG_IMMUTABLE) 446 : PendingIntent.getBroadcast( 447 getContext(), 2, bluetoothToggleIntent, 448 PendingIntent.FLAG_IMMUTABLE), 449 AccessoryUtils.isBluetoothEnabled()) 450 ); 451 } 452 } 453 updatePairingButton(PreferenceSliceBuilder psb)454 private void updatePairingButton(PreferenceSliceBuilder psb) { 455 RestrictedLockUtils.EnforcedAdmin admin = 456 RestrictedLockUtilsInternal.checkIfRestrictionEnforced(getContext(), 457 UserManager.DISALLOW_CONFIG_BLUETOOTH, UserHandle.myUserId()); 458 if (AccessoryUtils.isBluetoothEnabled()) { 459 PendingIntent pendingIntent; 460 if (admin == null) { 461 Intent i = new Intent(ACTION_CONNECT_INPUT).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 462 pendingIntent = PendingIntent 463 .getActivity(getContext(), 3, i, PendingIntent.FLAG_IMMUTABLE); 464 } else { 465 Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(getContext(), 466 admin); 467 intent.putExtra(DevicePolicyManager.EXTRA_RESTRICTION, 468 UserManager.DISALLOW_CONFIG_BLUETOOTH); 469 pendingIntent = PendingIntent.getActivity(getContext(), 0, intent, 470 PendingIntent.FLAG_IMMUTABLE); 471 } 472 psb.addPreference(new RowBuilder() 473 .setKey(KEY_PAIR_REMOTE) 474 .setTitle(getString(R.string.bluetooth_pair_accessory)) 475 .setActionId(TvSettingsEnums.CONNECTED_SLICE_CONNECT_NEW_DEVICES) 476 .setIcon(IconCompat.createWithResource(getContext(), 477 R.drawable.ic_baseline_add_24dp)) 478 .setIconNeedsToBeProcessed(true) 479 .setPendingIntent(pendingIntent) 480 ); 481 } 482 } 483 updateConnectedDevices(PreferenceSliceBuilder psb)484 private void updateConnectedDevices(PreferenceSliceBuilder psb) { 485 // Overall BT devices maps 486 HashMap<String, BluetoothDevice> addressToDevice = new HashMap<>(); 487 // Sets for BT devices that are not official remotes: 488 // - activeAccessories: they are considered connected from both BluetoothDevice and 489 // CachedBluetoothDevice's perceptive. 490 // - inactiveAccessories: they are considered connected from BluetoothDevice's perceptive 491 // but disconnected from CachedBluetoothDevice's perceptive. They can be easily 492 // reconnected. 493 // - bondedAccessories: they are considered merely bonded but not connected from 494 // BluetoothDevice's perceptive. 495 Set<String> activeAccessories = new HashSet<>(); 496 Set<String> inactiveAccessories = new HashSet<>(); 497 Set<String> bondedAccessories = new HashSet<>(); 498 499 // Bucketing all BT devices 500 for (BluetoothDevice device : getBluetoothDevices()) { 501 CachedBluetoothDevice cachedDevice = 502 AccessoryUtils.getCachedBluetoothDevice(getContext(), device); 503 if (!AccessoryUtils.isKnownDevice(getContext(), device)) { 504 if (AccessoryUtils.isConnected(device)) { 505 addressToDevice.put(device.getAddress(), device); 506 if (cachedDevice != null && cachedDevice.isConnected()) { 507 activeAccessories.add(device.getAddress()); 508 } else { 509 inactiveAccessories.add(device.getAddress()); 510 } 511 } else if (AccessoryUtils.isBonded(device)) { 512 addressToDevice.put(device.getAddress(), device); 513 bondedAccessories.add(device.getAddress()); 514 } 515 } 516 } 517 518 // "Accessories" category 519 if (activeAccessories.size() + inactiveAccessories.size() + bondedAccessories.size() 520 > 0) { 521 psb.addPreferenceCategory(new RowBuilder() 522 .setTitle(getContext().getString(R.string.bluetooth_known_devices_category)) 523 .setKey(KEY_ACCESSORIES)); 524 // Add accessories following the ranking of: active, inactive, bonded. 525 createAndAddBtDeviceSlicePreferenceFromSet(psb, activeAccessories, addressToDevice); 526 createAndAddBtDeviceSlicePreferenceFromSet(psb, inactiveAccessories, addressToDevice); 527 createAndAddBtDeviceSlicePreferenceFromSet(psb, bondedAccessories, addressToDevice); 528 } 529 } 530 updateOfficialRemoteSettings(PreferenceSliceBuilder psb)531 private void updateOfficialRemoteSettings(PreferenceSliceBuilder psb) { 532 String officialRemoteSettingsUri = 533 getString(R.string.bluetooth_official_remote_entry_slice_uri); 534 String irSettingsUri = 535 getString(R.string.bluetooth_ir_entry_slice_uri); 536 boolean isOfficialRemoteSettingsUriValid = isSliceProviderValid(officialRemoteSettingsUri); 537 boolean isIrSettingsUriValid = isSliceProviderValid(irSettingsUri); 538 if (isOfficialRemoteSettingsUriValid || isIrSettingsUriValid) { 539 psb.addPreferenceCategory(new RowBuilder() 540 .setKey(KEY_OFFICIAL_REMOTES_CATEGORY) 541 .setTitle(getString(R.string.bluetooth_official_remote_category))); 542 } 543 if (isIrSettingsUriValid) { 544 psb.addPreference(new RowBuilder() 545 .setKey(KEY_IR) 546 .setTitle(getString(R.string.bluetooth_ir_entry_title)) 547 .setSubtitle(getString(R.string.bluetooth_ir_entry_subtitle)) 548 .setTargetSliceUri(irSettingsUri)); 549 } 550 if (isOfficialRemoteSettingsUriValid) { 551 psb.addPreference(new RowBuilder() 552 .setKey(KEY_OFFICIAL_REMOTE) 553 .setTitle(getString(R.string.bluetooth_official_remote_entry_title)) 554 .setTargetSliceUri(officialRemoteSettingsUri)); 555 } 556 } 557 updateFmr(PreferenceSliceBuilder psb)558 private void updateFmr(PreferenceSliceBuilder psb) { 559 List<ResolveInfo> receivers = getContext().getPackageManager().queryBroadcastReceivers( 560 new Intent(ACTION_FIND_MY_REMOTE), 0); 561 if (receivers.isEmpty()) { 562 return; 563 } 564 565 psb.addPreference(new RowBuilder() 566 .setKey(KEY_FIND_MY_REMOTE_TOGGLE) 567 .setTitle(getString(R.string.settings_find_my_remote_title)) 568 .setSubtitle(getString(R.string.settings_find_my_remote_description)) 569 .setTargetSliceUri(ConnectedDevicesSliceUtils.FIND_MY_REMOTE_SLICE_URI.toString())); 570 } 571 createAndAddBtDeviceSlicePreferenceFromSet( PreferenceSliceBuilder psb, Set<String> addresses, HashMap<String, BluetoothDevice> addressesToBtDeviceMap)572 private void createAndAddBtDeviceSlicePreferenceFromSet( 573 PreferenceSliceBuilder psb, 574 Set<String> addresses, 575 HashMap<String, BluetoothDevice> addressesToBtDeviceMap) { 576 if (psb == null || addresses == null || addresses.isEmpty() 577 || addressesToBtDeviceMap == null || addressesToBtDeviceMap.isEmpty()) { 578 return; 579 } 580 final List<String> devicesAddressesList = new ArrayList<>(addresses); 581 Collections.sort(devicesAddressesList); 582 for (String deviceAddr : devicesAddressesList) { 583 psb.addPreference( 584 createBtDeviceSlicePreference( 585 getContext(), 586 addressesToBtDeviceMap.get(deviceAddr))); 587 } 588 } 589 createBtDeviceSlicePreference( Context context, BluetoothDevice device)590 private PreferenceSliceBuilder.RowBuilder createBtDeviceSlicePreference( 591 Context context, BluetoothDevice device) { 592 PreferenceSliceBuilder.RowBuilder pref = new PreferenceSliceBuilder.RowBuilder(); 593 boolean isConnected = AccessoryUtils.isConnected(device) 594 && AccessoryUtils.getCachedBluetoothDevice(getContext(), device) != null 595 && AccessoryUtils.getCachedBluetoothDevice(getContext(), device).isConnected(); 596 pref.setKey(device.getAddress()); 597 pref.setTitle(AccessoryUtils.getLocalName(device)); 598 pref.setSubtitle( 599 isConnected 600 ? getString(R.string.bluetooth_connected_status) 601 : getString(R.string.bluetooth_disconnected_status)); 602 pref.setIcon(IconCompat.createWithResource( 603 context, AccessoriesFragment.getImageIdForDevice(device, true))); 604 pref.setIconNeedsToBeProcessed(true); 605 606 RestrictedLockUtils.EnforcedAdmin admin = 607 RestrictedLockUtilsInternal.checkIfRestrictionEnforced(getContext(), 608 UserManager.DISALLOW_CONFIG_BLUETOOTH, UserHandle.myUserId()); 609 if (admin == null) { 610 Uri targetSliceUri = ConnectedDevicesSliceUtils 611 .getDeviceUri(device.getAddress(), device.getAlias()); 612 pref.setTargetSliceUri(targetSliceUri.toString()); 613 } else { 614 Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(getContext(), 615 admin); 616 intent.putExtra(DevicePolicyManager.EXTRA_RESTRICTION, 617 UserManager.DISALLOW_CONFIG_BLUETOOTH); 618 pref.setPendingIntent(PendingIntent.getActivity(getContext(), 0, intent, 619 PendingIntent.FLAG_IMMUTABLE)); 620 } 621 return pref; 622 } 623 getBluetoothDevices()624 private List<BluetoothDevice> getBluetoothDevices() { 625 if (mBtDeviceServiceBinder != null) { 626 return mBtDeviceServiceBinder.getDevices(); 627 } 628 return new ArrayList<>(); 629 } 630 getBluetoothDeviceProvider()631 private BluetoothDeviceProvider getBluetoothDeviceProvider() { 632 return mBtDeviceServiceBinder; 633 } 634 notifyDeviceSlice(BluetoothDevice device)635 private void notifyDeviceSlice(BluetoothDevice device) { 636 String addr = device.getAddress(); 637 mHandler.post(() -> { 638 if (device != null) { 639 getContext().getContentResolver().notifyChange( 640 ConnectedDevicesSliceUtils.getDeviceUri(addr, device.getAlias()), null); 641 } 642 }); 643 } 644 showBluetoothToggle()645 private boolean showBluetoothToggle() { 646 return getContext().getResources().getBoolean(R.bool.show_bluetooth_toggle); 647 } 648 getString(@ntegerRes int resId)649 private String getString(@IntegerRes int resId) { 650 return getContext().getString(resId); 651 } 652 isSliceProviderValid(String uri)653 private boolean isSliceProviderValid(String uri) { 654 return !TextUtils.isEmpty(uri) 655 && ConnectedDevicesSliceUtils.isSliceProviderValid(getContext(), uri); 656 } 657 showDisconnectButton(BluetoothDevice device, Context context)658 private boolean showDisconnectButton(BluetoothDevice device, Context context) { 659 if (DISCONNECT_PREFERENCE_ENABLED) { 660 return true; 661 } 662 return !AccessoryUtils.isRemoteClass(device) 663 && !AccessoryUtils.isKnownDevice(context, device); 664 } 665 createFindMyRemoteSlice(Uri sliceUri)666 private Slice createFindMyRemoteSlice(Uri sliceUri) { 667 Context context = getContext(); 668 final PreferenceSliceBuilder psb = new PreferenceSliceBuilder(context, sliceUri); 669 psb.addScreenTitle(new RowBuilder() 670 .setTitle(getString(R.string.settings_find_my_remote_title)) 671 .setSubtitle(getString(R.string.find_my_remote_slice_description))); 672 673 if (context.getResources().getBoolean(R.bool.config_find_my_remote_integration_enabled)) { 674 boolean isButtonEnabled = isFindMyRemoteButtonEnabled(context); 675 Intent intent = new Intent(ACTION_TOGGLE_CHANGED); 676 intent.putExtra(EXTRA_TOGGLE_TYPE, FIND_MY_REMOTE_PHYSICAL_BUTTON_ENABLED_SETTING); 677 intent.putExtra(EXTRA_TOGGLE_STATE, !isButtonEnabled); 678 intent.setClass(context, ConnectedDevicesSliceBroadcastReceiver.class); 679 psb.addPreference(new RowBuilder() 680 .setKey(FIND_MY_REMOTE_PHYSICAL_BUTTON_ENABLED_SETTING) 681 .setTitle(getString(R.string.find_my_remote_integration_title)) 682 .setSubtitle(getString(R.string.find_my_remote_integration_hint)) 683 .addSwitch( 684 PendingIntent.getBroadcast( 685 context, 0, intent, FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT), 686 !isButtonEnabled)); 687 } 688 689 PendingIntent pendingIntent = PendingIntent.getBroadcast( 690 context, 0, 691 new Intent(context, ConnectedDevicesSliceBroadcastReceiver.class) 692 .setAction(ACTION_FIND_MY_REMOTE) 693 .setFlags(FLAG_RECEIVER_FOREGROUND), 694 FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT); 695 696 psb.addPreference(new RowBuilder() 697 .setKey(ACTION_FIND_MY_REMOTE) 698 .setTitle(getString(R.string.find_my_remote_play_sound)) 699 .setPendingIntent(pendingIntent) 700 .setIcon(IconCompat.createWithResource(context, R.drawable.ic_play_arrow)) 701 .setIconNeedsToBeProcessed(true)); 702 return psb.build(); 703 } 704 } 705