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