1 /* 2 * Copyright (C) 2015 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 import android.app.Fragment; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothGatt; 23 import android.bluetooth.BluetoothGattCallback; 24 import android.bluetooth.BluetoothGattCharacteristic; 25 import android.bluetooth.BluetoothGattService; 26 import android.content.BroadcastReceiver; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.IntentFilter; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.text.TextUtils; 33 import android.util.Log; 34 35 import androidx.annotation.DrawableRes; 36 import androidx.annotation.Keep; 37 import androidx.annotation.NonNull; 38 import androidx.leanback.app.GuidedStepFragment; 39 import androidx.leanback.widget.GuidanceStylist; 40 import androidx.leanback.widget.GuidedAction; 41 import androidx.preference.Preference; 42 import androidx.preference.PreferenceScreen; 43 44 import com.android.internal.logging.nano.MetricsProto; 45 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 46 import com.android.tv.settings.R; 47 import com.android.tv.settings.SettingsPreferenceFragment; 48 49 import java.util.List; 50 import java.util.Objects; 51 import java.util.Set; 52 import java.util.UUID; 53 54 /** 55 * The screen in TV settings that let's users rename or unpair a bluetooth device. 56 */ 57 @Keep 58 public class BluetoothAccessoryFragment extends SettingsPreferenceFragment { 59 60 private static final boolean DEBUG = false; 61 private static final String TAG = "BluetoothAccessoryFrag"; 62 63 private static final UUID GATT_BATTERY_SERVICE_UUID = 64 UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb"); 65 private static final UUID GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID = 66 UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb"); 67 68 private static final String KEY_CHANGE_NAME = "changeName"; 69 private static final String KEY_UNPAIR = "unpair"; 70 private static final String KEY_BATTERY = "battery"; 71 72 private static final String SAVE_STATE_UNPAIRING = "BluetoothAccessoryActivity.unpairing"; 73 74 private static final int UNPAIR_TIMEOUT = 5000; 75 76 private static final String ARG_DEVICE = "device"; 77 private static final String ARG_ACCESSORY_ADDRESS = "accessory_address"; 78 private static final String ARG_ACCESSORY_NAME = "accessory_name"; 79 private static final String ARG_ACCESSORY_ICON_ID = "accessory_icon_res"; 80 81 private BluetoothDevice mDevice; 82 private BluetoothGatt mDeviceGatt; 83 private String mDeviceAddress; 84 private String mDeviceName; 85 private @DrawableRes int mDeviceImgId; 86 private boolean mUnpairing; 87 private Preference mChangeNamePref; 88 private Preference mUnpairPref; 89 private Preference mBatteryPref; 90 91 private final Handler mHandler = new Handler(); 92 private Runnable mBailoutRunnable = new Runnable() { 93 @Override 94 public void run() { 95 if (isResumed() && !getFragmentManager().popBackStackImmediate()) { 96 getActivity().onBackPressed(); 97 } 98 } 99 }; 100 101 // Broadcast Receiver for Bluetooth related events 102 private BroadcastReceiver mBroadcastReceiver; 103 newInstance(String deviceAddress, String deviceName, int deviceImgId)104 public static BluetoothAccessoryFragment newInstance(String deviceAddress, String deviceName, 105 int deviceImgId) { 106 final Bundle b = new Bundle(3); 107 prepareArgs(b, deviceAddress, deviceName, deviceImgId); 108 final BluetoothAccessoryFragment f = new BluetoothAccessoryFragment(); 109 f.setArguments(b); 110 return f; 111 } 112 prepareArgs(Bundle b, String deviceAddress, String deviceName, int deviceImgId)113 public static void prepareArgs(Bundle b, String deviceAddress, String deviceName, 114 int deviceImgId) { 115 b.putString(ARG_ACCESSORY_ADDRESS, deviceAddress); 116 b.putString(ARG_ACCESSORY_NAME, deviceName); 117 b.putInt(ARG_ACCESSORY_ICON_ID, deviceImgId); 118 } 119 120 @Override onCreate(Bundle savedInstanceState)121 public void onCreate(Bundle savedInstanceState) { 122 Bundle bundle = getArguments(); 123 if (bundle != null) { 124 mDeviceAddress = bundle.getString(ARG_ACCESSORY_ADDRESS); 125 mDeviceName = bundle.getString(ARG_ACCESSORY_NAME); 126 mDeviceImgId = bundle.getInt(ARG_ACCESSORY_ICON_ID); 127 } else { 128 mDeviceName = getString(R.string.accessory_options); 129 mDeviceImgId = R.drawable.ic_qs_bluetooth_not_connected; 130 } 131 132 133 mUnpairing = savedInstanceState != null 134 && savedInstanceState.getBoolean(SAVE_STATE_UNPAIRING); 135 136 BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); 137 if (btAdapter != null) { 138 final Set<BluetoothDevice> bondedDevices = btAdapter.getBondedDevices(); 139 if (bondedDevices != null) { 140 for (BluetoothDevice device : bondedDevices) { 141 if (mDeviceAddress.equals(device.getAddress())) { 142 mDevice = device; 143 break; 144 } 145 } 146 } 147 } 148 149 if (mDevice == null) { 150 navigateBack(); 151 } 152 153 super.onCreate(savedInstanceState); 154 } 155 156 @Override onStart()157 public void onStart() { 158 super.onStart(); 159 if (mDevice != null && 160 (mDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE || 161 mDevice.getType() == BluetoothDevice.DEVICE_TYPE_DUAL)) { 162 // Only LE devices support GATT 163 mDeviceGatt = mDevice.connectGatt(getActivity(), true, new GattBatteryCallbacks()); 164 } 165 // Set a broadcast receiver to let us know when the device has been removed 166 final IntentFilter adapterIntentFilter = new IntentFilter(); 167 adapterIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 168 mBroadcastReceiver = new UnpairReceiver(this, mDevice); 169 getActivity().registerReceiver(mBroadcastReceiver, adapterIntentFilter); 170 if (mDevice != null && mDevice.getBondState() == BluetoothDevice.BOND_NONE) { 171 navigateBack(); 172 } 173 } 174 175 @Override onPause()176 public void onPause() { 177 super.onPause(); 178 mHandler.removeCallbacks(mBailoutRunnable); 179 } 180 181 @Override onSaveInstanceState(@onNull Bundle savedInstanceState)182 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { 183 super.onSaveInstanceState(savedInstanceState); 184 savedInstanceState.putBoolean(SAVE_STATE_UNPAIRING, mUnpairing); 185 } 186 187 @Override onStop()188 public void onStop() { 189 super.onStop(); 190 if (mDeviceGatt != null) { 191 mDeviceGatt.close(); 192 } 193 getActivity().unregisterReceiver(mBroadcastReceiver); 194 } 195 196 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)197 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 198 setPreferencesFromResource(R.xml.bluetooth_accessory, null); 199 final PreferenceScreen screen = getPreferenceScreen(); 200 screen.setTitle(mDeviceName); 201 202 mChangeNamePref = findPreference(KEY_CHANGE_NAME); 203 ChangeNameFragment.prepareArgs(mChangeNamePref.getExtras(), mDeviceName, mDeviceImgId); 204 205 mUnpairPref = findPreference(KEY_UNPAIR); 206 updatePrefsForUnpairing(); 207 UnpairConfirmFragment.prepareArgs( 208 mUnpairPref.getExtras(), mDevice, mDeviceName, mDeviceImgId); 209 210 mBatteryPref = findPreference(KEY_BATTERY); 211 mBatteryPref.setVisible(false); 212 } 213 setUnpairing(boolean unpairing)214 public void setUnpairing(boolean unpairing) { 215 mUnpairing = unpairing; 216 updatePrefsForUnpairing(); 217 } 218 updatePrefsForUnpairing()219 private void updatePrefsForUnpairing() { 220 if (mUnpairing) { 221 mUnpairPref.setTitle(R.string.accessory_unpairing); 222 mUnpairPref.setEnabled(false); 223 mChangeNamePref.setEnabled(false); 224 } else { 225 mUnpairPref.setTitle(R.string.accessory_unpair); 226 mUnpairPref.setEnabled(true); 227 mChangeNamePref.setEnabled(true); 228 } 229 } 230 navigateBack()231 private void navigateBack() { 232 // need to post this to avoid recursing in the fragment manager. 233 mHandler.removeCallbacks(mBailoutRunnable); 234 mHandler.post(mBailoutRunnable); 235 } 236 renameDevice(String deviceName)237 private void renameDevice(String deviceName) { 238 mDeviceName = deviceName; 239 if (mDevice != null) { 240 mDevice.setAlias(deviceName); 241 getPreferenceScreen().setTitle(deviceName); 242 setTitle(deviceName); 243 ChangeNameFragment.prepareArgs(mChangeNamePref.getExtras(), mDeviceName, mDeviceImgId); 244 UnpairConfirmFragment.prepareArgs( 245 mUnpairPref.getExtras(), mDevice, mDeviceName, mDeviceImgId); 246 } 247 } 248 249 private class GattBatteryCallbacks extends BluetoothGattCallback { 250 @Override onConnectionStateChange(BluetoothGatt gatt, int status, int newState)251 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 252 if (DEBUG) { 253 Log.d(TAG, "Connection status:" + status + " state:" + newState); 254 } 255 if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) { 256 gatt.discoverServices(); 257 } 258 } 259 260 @Override onServicesDiscovered(BluetoothGatt gatt, int status)261 public void onServicesDiscovered(BluetoothGatt gatt, int status) { 262 if (status != BluetoothGatt.GATT_SUCCESS) { 263 if (DEBUG) { 264 Log.e(TAG, "Service discovery failure on " + gatt); 265 } 266 return; 267 } 268 269 final BluetoothGattService battService = gatt.getService(GATT_BATTERY_SERVICE_UUID); 270 if (battService == null) { 271 if (DEBUG) { 272 Log.d(TAG, "No battery service"); 273 } 274 return; 275 } 276 277 final BluetoothGattCharacteristic battLevel = 278 battService.getCharacteristic(GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID); 279 if (battLevel == null) { 280 if (DEBUG) { 281 Log.d(TAG, "No battery level"); 282 } 283 return; 284 } 285 286 gatt.readCharacteristic(battLevel); 287 } 288 289 @Override onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)290 public void onCharacteristicRead(BluetoothGatt gatt, 291 BluetoothGattCharacteristic characteristic, int status) { 292 if (status != BluetoothGatt.GATT_SUCCESS) { 293 if (DEBUG) { 294 Log.e(TAG, "Read characteristic failure on " + gatt + " " + characteristic); 295 } 296 return; 297 } 298 299 if (GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) { 300 final int batteryLevel = 301 characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); 302 mHandler.post(new Runnable() { 303 @Override 304 public void run() { 305 if (mBatteryPref != null && !mUnpairing) { 306 mBatteryPref.setTitle(getString(R.string.accessory_battery, 307 batteryLevel)); 308 mBatteryPref.setVisible(true); 309 } 310 } 311 }); 312 } 313 } 314 } 315 316 /** 317 * Fragment for changing the name of a bluetooth accessory 318 */ 319 @Keep 320 public static class ChangeNameFragment extends GuidedStepFragment { 321 322 private final MetricsFeatureProvider mMetricsFeatureProvider = new MetricsFeatureProvider(); 323 prepareArgs(@onNull Bundle args, String deviceName, @DrawableRes int deviceImgId)324 public static void prepareArgs(@NonNull Bundle args, String deviceName, 325 @DrawableRes int deviceImgId) { 326 args.putString(ARG_ACCESSORY_NAME, deviceName); 327 args.putInt(ARG_ACCESSORY_ICON_ID, deviceImgId); 328 } 329 330 @Override onStart()331 public void onStart() { 332 super.onStart(); 333 mMetricsFeatureProvider.action(getContext(), 334 MetricsProto.MetricsEvent.ACTION_BLUETOOTH_RENAME); 335 } 336 337 @NonNull 338 @Override onCreateGuidance(Bundle savedInstanceState)339 public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { 340 return new GuidanceStylist.Guidance( 341 getString(R.string.accessory_change_name_title), 342 null, 343 getArguments().getString(ARG_ACCESSORY_NAME), 344 getContext().getDrawable(getArguments().getInt(ARG_ACCESSORY_ICON_ID, 345 R.drawable.ic_qs_bluetooth_not_connected)) 346 ); 347 } 348 349 @Override onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)350 public void onCreateActions(@NonNull List<GuidedAction> actions, 351 Bundle savedInstanceState) { 352 final Context context = getContext(); 353 actions.add(new GuidedAction.Builder(context) 354 .title(getArguments().getString(ARG_ACCESSORY_NAME)) 355 .editable(true) 356 .build()); 357 } 358 359 @Override onGuidedActionEditedAndProceed(GuidedAction action)360 public long onGuidedActionEditedAndProceed(GuidedAction action) { 361 if (!TextUtils.equals(action.getTitle(), 362 getArguments().getString(ARG_ACCESSORY_NAME)) 363 && TextUtils.isGraphic(action.getTitle())) { 364 final BluetoothAccessoryFragment fragment = 365 (BluetoothAccessoryFragment) getTargetFragment(); 366 fragment.renameDevice(action.getTitle().toString()); 367 getFragmentManager().popBackStack(); 368 } 369 return GuidedAction.ACTION_ID_NEXT; 370 } 371 } 372 373 private static class UnpairReceiver extends BroadcastReceiver { 374 375 private final Fragment mFragment; 376 private final BluetoothDevice mDevice; 377 UnpairReceiver(Fragment fragment, BluetoothDevice device)378 public UnpairReceiver(Fragment fragment, BluetoothDevice device) { 379 mFragment = fragment; 380 mDevice = device; 381 } 382 383 @Override onReceive(Context context, Intent intent)384 public void onReceive(Context context, Intent intent) { 385 final BluetoothDevice device = intent 386 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 387 final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 388 BluetoothDevice.BOND_NONE); 389 if (bondState == BluetoothDevice.BOND_NONE && Objects.equals(mDevice, device)) { 390 // Device was removed, bail out of the fragment 391 if (mFragment instanceof BluetoothAccessoryFragment) { 392 ((BluetoothAccessoryFragment) mFragment).navigateBack(); 393 } else if (mFragment instanceof UnpairConfirmFragment) { 394 ((UnpairConfirmFragment) mFragment).navigateBack(); 395 } else { 396 throw new IllegalStateException( 397 "UnpairReceiver attached to wrong fragment class"); 398 } 399 } 400 } 401 } 402 403 public static class UnpairConfirmFragment extends GuidedStepFragment { 404 405 private BluetoothDevice mDevice; 406 private BroadcastReceiver mBroadcastReceiver; 407 private final Handler mHandler = new Handler(); 408 private final MetricsFeatureProvider mMetricsFeatureProvider = 409 new MetricsFeatureProvider(); 410 411 private Runnable mBailoutRunnable = new Runnable() { 412 @Override 413 public void run() { 414 if (isResumed() && !getFragmentManager().popBackStackImmediate()) { 415 getActivity().onBackPressed(); 416 } 417 } 418 }; 419 420 private final Runnable mTimeoutRunnable = new Runnable() { 421 @Override 422 public void run() { 423 navigateBack(); 424 } 425 }; 426 prepareArgs(@onNull Bundle args, BluetoothDevice device, String deviceName, @DrawableRes int deviceImgId)427 public static void prepareArgs(@NonNull Bundle args, BluetoothDevice device, 428 String deviceName, @DrawableRes int deviceImgId) { 429 args.putParcelable(ARG_DEVICE, device); 430 args.putString(ARG_ACCESSORY_NAME, deviceName); 431 args.putInt(ARG_ACCESSORY_ICON_ID, deviceImgId); 432 } 433 434 @Override onCreate(Bundle savedInstanceState)435 public void onCreate(Bundle savedInstanceState) { 436 mDevice = getArguments().getParcelable(ARG_DEVICE); 437 super.onCreate(savedInstanceState); 438 } 439 440 @Override onStart()441 public void onStart() { 442 super.onStart(); 443 if (mDevice.getBondState() == BluetoothDevice.BOND_NONE) { 444 navigateBack(); 445 } 446 final IntentFilter adapterIntentFilter = new IntentFilter(); 447 adapterIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 448 mBroadcastReceiver = new UnpairReceiver(this, mDevice); 449 getActivity().registerReceiver(mBroadcastReceiver, adapterIntentFilter); 450 mMetricsFeatureProvider.action(getContext(), 451 MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_PAIRED_DEVICE_FORGET); 452 } 453 454 @Override onStop()455 public void onStop() { 456 super.onStop(); 457 getActivity().unregisterReceiver(mBroadcastReceiver); 458 } 459 460 @Override onDestroy()461 public void onDestroy() { 462 super.onDestroy(); 463 mHandler.removeCallbacks(mTimeoutRunnable); 464 mHandler.removeCallbacks(mBailoutRunnable); 465 } 466 467 @NonNull 468 @Override onCreateGuidance(Bundle savedInstanceState)469 public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { 470 return new GuidanceStylist.Guidance( 471 getString(R.string.accessory_unpair), 472 null, 473 getArguments().getString(ARG_ACCESSORY_NAME), 474 getContext().getDrawable(getArguments().getInt(ARG_ACCESSORY_ICON_ID, 475 R.drawable.ic_qs_bluetooth_not_connected)) 476 ); 477 } 478 479 @Override onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)480 public void onCreateActions(@NonNull List<GuidedAction> actions, 481 Bundle savedInstanceState) { 482 final Context context = getContext(); 483 actions.add(new GuidedAction.Builder(context) 484 .clickAction(GuidedAction.ACTION_ID_OK).build()); 485 actions.add(new GuidedAction.Builder(context) 486 .clickAction(GuidedAction.ACTION_ID_CANCEL).build()); 487 } 488 489 @Override onGuidedActionClicked(GuidedAction action)490 public void onGuidedActionClicked(GuidedAction action) { 491 if (action.getId() == GuidedAction.ACTION_ID_OK) { 492 unpairDevice(); 493 } else if (action.getId() == GuidedAction.ACTION_ID_CANCEL) { 494 getFragmentManager().popBackStack(); 495 } else { 496 super.onGuidedActionClicked(action); 497 } 498 } 499 navigateBack()500 private void navigateBack() { 501 // need to post this to avoid recursing in the fragment manager. 502 mHandler.removeCallbacks(mBailoutRunnable); 503 mHandler.post(mBailoutRunnable); 504 } 505 unpairDevice()506 private void unpairDevice() { 507 if (mDevice != null) { 508 int state = mDevice.getBondState(); 509 510 if (state == BluetoothDevice.BOND_BONDING) { 511 mDevice.cancelBondProcess(); 512 } 513 514 if (state != BluetoothDevice.BOND_NONE) { 515 ((BluetoothAccessoryFragment) getTargetFragment()).setUnpairing(true); 516 // Set a timeout, just in case we don't receive the unpair notification we 517 // use to finish the activity 518 mHandler.postDelayed(mTimeoutRunnable, UNPAIR_TIMEOUT); 519 final boolean successful = mDevice.removeBond(); 520 if (successful) { 521 if (DEBUG) { 522 Log.d(TAG, "Bluetooth device successfully unpaired."); 523 } 524 } else { 525 Log.e(TAG, "Failed to unpair Bluetooth Device: " + mDevice.getName()); 526 } 527 } 528 } else { 529 Log.e(TAG, "Bluetooth device not found. Address = " + mDevice.getAddress()); 530 } 531 } 532 } 533 534 @Override getMetricsCategory()535 public int getMetricsCategory() { 536 return MetricsProto.MetricsEvent.DIALOG_BLUETOOTH_PAIRED_DEVICE_PROFILE; 537 } 538 } 539