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.isConnected() && 159 (mDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE || 160 mDevice.getType() == BluetoothDevice.DEVICE_TYPE_DUAL)) { 161 // Only LE devices support GATT 162 mDeviceGatt = mDevice.connectGatt(getActivity(), true, new GattBatteryCallbacks()); 163 } 164 // Set a broadcast receiver to let us know when the device has been removed 165 final IntentFilter adapterIntentFilter = new IntentFilter(); 166 adapterIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 167 mBroadcastReceiver = new UnpairReceiver(this, mDevice); 168 getActivity().registerReceiver(mBroadcastReceiver, adapterIntentFilter); 169 if (mDevice != null && mDevice.getBondState() == BluetoothDevice.BOND_NONE) { 170 navigateBack(); 171 } 172 } 173 174 @Override onPause()175 public void onPause() { 176 super.onPause(); 177 mHandler.removeCallbacks(mBailoutRunnable); 178 } 179 180 @Override onSaveInstanceState(@onNull Bundle savedInstanceState)181 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { 182 super.onSaveInstanceState(savedInstanceState); 183 savedInstanceState.putBoolean(SAVE_STATE_UNPAIRING, mUnpairing); 184 } 185 186 @Override onStop()187 public void onStop() { 188 super.onStop(); 189 if (mDeviceGatt != null) { 190 mDeviceGatt.close(); 191 } 192 getActivity().unregisterReceiver(mBroadcastReceiver); 193 } 194 195 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)196 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 197 setPreferencesFromResource(R.xml.bluetooth_accessory, null); 198 final PreferenceScreen screen = getPreferenceScreen(); 199 screen.setTitle(mDeviceName); 200 201 mChangeNamePref = findPreference(KEY_CHANGE_NAME); 202 ChangeNameFragment.prepareArgs(mChangeNamePref.getExtras(), mDeviceName, mDeviceImgId); 203 204 mUnpairPref = findPreference(KEY_UNPAIR); 205 updatePrefsForUnpairing(); 206 UnpairConfirmFragment.prepareArgs( 207 mUnpairPref.getExtras(), mDevice, mDeviceName, mDeviceImgId); 208 209 mBatteryPref = findPreference(KEY_BATTERY); 210 mBatteryPref.setVisible(false); 211 } 212 setUnpairing(boolean unpairing)213 public void setUnpairing(boolean unpairing) { 214 mUnpairing = unpairing; 215 updatePrefsForUnpairing(); 216 } 217 updatePrefsForUnpairing()218 private void updatePrefsForUnpairing() { 219 if (mUnpairing) { 220 mUnpairPref.setTitle(R.string.accessory_unpairing); 221 mUnpairPref.setEnabled(false); 222 mChangeNamePref.setEnabled(false); 223 } else { 224 mUnpairPref.setTitle(R.string.accessory_unpair); 225 mUnpairPref.setEnabled(true); 226 mChangeNamePref.setEnabled(true); 227 } 228 } 229 navigateBack()230 private void navigateBack() { 231 // need to post this to avoid recursing in the fragment manager. 232 mHandler.removeCallbacks(mBailoutRunnable); 233 mHandler.post(mBailoutRunnable); 234 } 235 renameDevice(String deviceName)236 private void renameDevice(String deviceName) { 237 mDeviceName = deviceName; 238 if (mDevice != null) { 239 mDevice.setAlias(deviceName); 240 getPreferenceScreen().setTitle(deviceName); 241 setTitle(deviceName); 242 ChangeNameFragment.prepareArgs(mChangeNamePref.getExtras(), mDeviceName, mDeviceImgId); 243 UnpairConfirmFragment.prepareArgs( 244 mUnpairPref.getExtras(), mDevice, mDeviceName, mDeviceImgId); 245 } 246 } 247 248 private class GattBatteryCallbacks extends BluetoothGattCallback { 249 @Override onConnectionStateChange(BluetoothGatt gatt, int status, int newState)250 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 251 if (DEBUG) { 252 Log.d(TAG, "Connection status:" + status + " state:" + newState); 253 } 254 if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) { 255 gatt.discoverServices(); 256 } 257 } 258 259 @Override onServicesDiscovered(BluetoothGatt gatt, int status)260 public void onServicesDiscovered(BluetoothGatt gatt, int status) { 261 if (status != BluetoothGatt.GATT_SUCCESS) { 262 if (DEBUG) { 263 Log.e(TAG, "Service discovery failure on " + gatt); 264 } 265 return; 266 } 267 268 final BluetoothGattService battService = gatt.getService(GATT_BATTERY_SERVICE_UUID); 269 if (battService == null) { 270 if (DEBUG) { 271 Log.d(TAG, "No battery service"); 272 } 273 return; 274 } 275 276 final BluetoothGattCharacteristic battLevel = 277 battService.getCharacteristic(GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID); 278 if (battLevel == null) { 279 if (DEBUG) { 280 Log.d(TAG, "No battery level"); 281 } 282 return; 283 } 284 285 gatt.readCharacteristic(battLevel); 286 } 287 288 @Override onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)289 public void onCharacteristicRead(BluetoothGatt gatt, 290 BluetoothGattCharacteristic characteristic, int status) { 291 if (status != BluetoothGatt.GATT_SUCCESS) { 292 if (DEBUG) { 293 Log.e(TAG, "Read characteristic failure on " + gatt + " " + characteristic); 294 } 295 return; 296 } 297 298 if (GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) { 299 final int batteryLevel = 300 characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); 301 mHandler.post(new Runnable() { 302 @Override 303 public void run() { 304 if (mBatteryPref != null && !mUnpairing) { 305 mBatteryPref.setTitle(getString(R.string.accessory_battery, 306 batteryLevel)); 307 mBatteryPref.setVisible(true); 308 } 309 } 310 }); 311 } 312 } 313 } 314 315 /** 316 * Fragment for changing the name of a bluetooth accessory 317 */ 318 @Keep 319 public static class ChangeNameFragment extends GuidedStepSupportFragment { 320 prepareArgs(@onNull Bundle args, String deviceName, @DrawableRes int deviceImgId)321 public static void prepareArgs(@NonNull Bundle args, String deviceName, 322 @DrawableRes int deviceImgId) { 323 args.putString(ARG_ACCESSORY_NAME, deviceName); 324 args.putInt(ARG_ACCESSORY_ICON_ID, deviceImgId); 325 } 326 327 @Override onStart()328 public void onStart() { 329 super.onStart(); 330 } 331 332 @NonNull 333 @Override onCreateGuidance(Bundle savedInstanceState)334 public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { 335 return new GuidanceStylist.Guidance( 336 getString(R.string.accessory_change_name_title), 337 null, 338 getArguments().getString(ARG_ACCESSORY_NAME), 339 getContext().getDrawable(getArguments().getInt(ARG_ACCESSORY_ICON_ID, 340 R.drawable.ic_qs_bluetooth_not_connected)) 341 ); 342 } 343 344 @Override onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)345 public void onCreateActions(@NonNull List<GuidedAction> actions, 346 Bundle savedInstanceState) { 347 final Context context = getContext(); 348 actions.add(new GuidedAction.Builder(context) 349 .title(getArguments().getString(ARG_ACCESSORY_NAME)) 350 .editable(true) 351 .build()); 352 } 353 354 @Override onGuidedActionEditedAndProceed(GuidedAction action)355 public long onGuidedActionEditedAndProceed(GuidedAction action) { 356 if (!TextUtils.equals(action.getTitle(), 357 getArguments().getString(ARG_ACCESSORY_NAME)) 358 && TextUtils.isGraphic(action.getTitle())) { 359 final BluetoothAccessoryFragment fragment = 360 (BluetoothAccessoryFragment) getTargetFragment(); 361 fragment.renameDevice(action.getTitle().toString()); 362 getFragmentManager().popBackStack(); 363 } 364 return GuidedAction.ACTION_ID_NEXT; 365 } 366 } 367 368 private static class UnpairReceiver extends BroadcastReceiver { 369 370 private final Fragment mFragment; 371 private final BluetoothDevice mDevice; 372 UnpairReceiver(Fragment fragment, BluetoothDevice device)373 public UnpairReceiver(Fragment fragment, BluetoothDevice device) { 374 mFragment = fragment; 375 mDevice = device; 376 } 377 378 @Override onReceive(Context context, Intent intent)379 public void onReceive(Context context, Intent intent) { 380 final BluetoothDevice device = intent 381 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 382 final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 383 BluetoothDevice.BOND_NONE); 384 if (bondState == BluetoothDevice.BOND_NONE && Objects.equals(mDevice, device)) { 385 // Device was removed, bail out of the fragment 386 if (mFragment instanceof BluetoothAccessoryFragment) { 387 ((BluetoothAccessoryFragment) mFragment).navigateBack(); 388 } else if (mFragment instanceof UnpairConfirmFragment) { 389 ((UnpairConfirmFragment) mFragment).navigateBack(); 390 } else { 391 throw new IllegalStateException( 392 "UnpairReceiver attached to wrong fragment class"); 393 } 394 } 395 } 396 } 397 398 public static class UnpairConfirmFragment extends GuidedStepSupportFragment { 399 400 private BluetoothDevice mDevice; 401 private BroadcastReceiver mBroadcastReceiver; 402 private final Handler mHandler = new Handler(); 403 404 private Runnable mBailoutRunnable = new Runnable() { 405 @Override 406 public void run() { 407 if (isResumed() && !getFragmentManager().popBackStackImmediate()) { 408 getActivity().onBackPressed(); 409 } 410 } 411 }; 412 413 private final Runnable mTimeoutRunnable = new Runnable() { 414 @Override 415 public void run() { 416 navigateBack(); 417 } 418 }; 419 prepareArgs(@onNull Bundle args, BluetoothDevice device, String deviceName, @DrawableRes int deviceImgId)420 public static void prepareArgs(@NonNull Bundle args, BluetoothDevice device, 421 String deviceName, @DrawableRes int deviceImgId) { 422 args.putParcelable(ARG_DEVICE, device); 423 args.putString(ARG_ACCESSORY_NAME, deviceName); 424 args.putInt(ARG_ACCESSORY_ICON_ID, deviceImgId); 425 } 426 427 @Override onCreate(Bundle savedInstanceState)428 public void onCreate(Bundle savedInstanceState) { 429 mDevice = getArguments().getParcelable(ARG_DEVICE); 430 super.onCreate(savedInstanceState); 431 } 432 433 @Override onStart()434 public void onStart() { 435 super.onStart(); 436 if (mDevice.getBondState() == BluetoothDevice.BOND_NONE) { 437 navigateBack(); 438 } 439 final IntentFilter adapterIntentFilter = new IntentFilter(); 440 adapterIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 441 mBroadcastReceiver = new UnpairReceiver(this, mDevice); 442 getActivity().registerReceiver(mBroadcastReceiver, adapterIntentFilter); 443 } 444 445 @Override onStop()446 public void onStop() { 447 super.onStop(); 448 getActivity().unregisterReceiver(mBroadcastReceiver); 449 } 450 451 @Override onDestroy()452 public void onDestroy() { 453 super.onDestroy(); 454 mHandler.removeCallbacks(mTimeoutRunnable); 455 mHandler.removeCallbacks(mBailoutRunnable); 456 } 457 458 @NonNull 459 @Override onCreateGuidance(Bundle savedInstanceState)460 public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { 461 return new GuidanceStylist.Guidance( 462 getString(R.string.accessory_unpair), 463 null, 464 getArguments().getString(ARG_ACCESSORY_NAME), 465 getContext().getDrawable(getArguments().getInt(ARG_ACCESSORY_ICON_ID, 466 R.drawable.ic_qs_bluetooth_not_connected)) 467 ); 468 } 469 470 @Override onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)471 public void onCreateActions(@NonNull List<GuidedAction> actions, 472 Bundle savedInstanceState) { 473 final Context context = getContext(); 474 actions.add(new GuidedAction.Builder(context) 475 .clickAction(GuidedAction.ACTION_ID_OK).build()); 476 actions.add(new GuidedAction.Builder(context) 477 .clickAction(GuidedAction.ACTION_ID_CANCEL).build()); 478 } 479 480 @Override onGuidedActionClicked(GuidedAction action)481 public void onGuidedActionClicked(GuidedAction action) { 482 if (action.getId() == GuidedAction.ACTION_ID_OK) { 483 unpairDevice(); 484 } else if (action.getId() == GuidedAction.ACTION_ID_CANCEL) { 485 getFragmentManager().popBackStack(); 486 } else { 487 super.onGuidedActionClicked(action); 488 } 489 } 490 navigateBack()491 private void navigateBack() { 492 // need to post this to avoid recursing in the fragment manager. 493 mHandler.removeCallbacks(mBailoutRunnable); 494 mHandler.post(mBailoutRunnable); 495 } 496 unpairDevice()497 private void unpairDevice() { 498 if (mDevice != null) { 499 int state = mDevice.getBondState(); 500 501 if (state == BluetoothDevice.BOND_BONDING) { 502 mDevice.cancelBondProcess(); 503 } 504 505 if (state != BluetoothDevice.BOND_NONE) { 506 ((BluetoothAccessoryFragment) getTargetFragment()).setUnpairing(true); 507 // Set a timeout, just in case we don't receive the unpair notification we 508 // use to finish the activity 509 mHandler.postDelayed(mTimeoutRunnable, UNPAIR_TIMEOUT); 510 final boolean successful = mDevice.removeBond(); 511 if (successful) { 512 if (DEBUG) { 513 Log.d(TAG, "Bluetooth device successfully unpaired."); 514 } 515 } else { 516 Log.e(TAG, "Failed to unpair Bluetooth Device: " + mDevice.getName()); 517 } 518 } 519 } else { 520 Log.e(TAG, "Bluetooth device not found. Address = " + mDevice.getAddress()); 521 } 522 } 523 } 524 } 525