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.support.annotation.DrawableRes; 32 import android.support.annotation.NonNull; 33 import android.support.v17.leanback.app.GuidedStepFragment; 34 import android.support.v17.leanback.widget.GuidanceStylist; 35 import android.support.v17.leanback.widget.GuidedAction; 36 import android.support.v17.preference.LeanbackPreferenceFragment; 37 import android.support.v7.preference.Preference; 38 import android.support.v7.preference.PreferenceScreen; 39 import android.util.Log; 40 41 import com.android.tv.settings.R; 42 43 import java.util.List; 44 import java.util.Set; 45 import java.util.UUID; 46 47 public class BluetoothAccessoryFragment extends LeanbackPreferenceFragment { 48 49 private static final boolean DEBUG = false; 50 private static final String TAG = "BluetoothAccessoryFrag"; 51 52 private static final UUID GATT_BATTERY_SERVICE_UUID = 53 UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb"); 54 private static final UUID GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID = 55 UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb"); 56 57 private static final String SAVE_STATE_UNPAIRING = "BluetoothAccessoryActivity.unpairing"; 58 59 private static final int UNPAIR_TIMEOUT = 5000; 60 61 private static final String ARG_ACCESSORY_ADDRESS = "accessory_address"; 62 private static final String ARG_ACCESSORY_NAME = "accessory_name"; 63 private static final String ARG_ACCESSORY_ICON_ID = "accessory_icon_res"; 64 65 private BluetoothDevice mDevice; 66 private BluetoothGatt mDeviceGatt; 67 private String mDeviceAddress; 68 private String mDeviceName; 69 private @DrawableRes int mDeviceImgId; 70 private boolean mUnpairing; 71 private Preference mUnpairPref; 72 private Preference mBatteryPref; 73 74 private final Handler mHandler = new Handler(); 75 private Runnable mBailoutRunnable = new Runnable() { 76 @Override 77 public void run() { 78 if (isResumed() && !getFragmentManager().popBackStackImmediate()) { 79 getActivity().onBackPressed(); 80 } 81 } 82 }; 83 84 // Broadcast Receiver for Bluetooth related events 85 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 86 @Override 87 public void onReceive(Context context, Intent intent) { 88 BluetoothDevice device = intent 89 .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 90 if (mUnpairing) { 91 if (mDevice.equals(device)) { 92 // Done removing device, finish the activity 93 mMsgHandler.removeCallbacks(mTimeoutRunnable); 94 navigateBack(); 95 } 96 } 97 } 98 }; 99 100 // Internal message handler 101 private final Handler mMsgHandler = new Handler(); 102 103 private final Runnable mTimeoutRunnable = new Runnable() { 104 @Override 105 public void run() { 106 navigateBack(); 107 } 108 }; 109 newInstance(String deviceAddress, String deviceName, int deviceImgId)110 public static BluetoothAccessoryFragment newInstance(String deviceAddress, String deviceName, 111 int deviceImgId) { 112 final Bundle b = new Bundle(3); 113 prepareArgs(b, deviceAddress, deviceName, deviceImgId); 114 final BluetoothAccessoryFragment f = new BluetoothAccessoryFragment(); 115 f.setArguments(b); 116 return f; 117 } 118 prepareArgs(Bundle b, String deviceAddress, String deviceName, int deviceImgId)119 public static void prepareArgs(Bundle b, String deviceAddress, String deviceName, 120 int deviceImgId) { 121 b.putString(ARG_ACCESSORY_ADDRESS, deviceAddress); 122 b.putString(ARG_ACCESSORY_NAME, deviceName); 123 b.putInt(ARG_ACCESSORY_ICON_ID, deviceImgId); 124 } 125 126 @Override onCreate(Bundle savedInstanceState)127 public void onCreate(Bundle savedInstanceState) { 128 Bundle bundle = getArguments(); 129 if (bundle != null) { 130 mDeviceAddress = bundle.getString(ARG_ACCESSORY_ADDRESS); 131 mDeviceName = bundle.getString(ARG_ACCESSORY_NAME); 132 mDeviceImgId = bundle.getInt(ARG_ACCESSORY_ICON_ID); 133 } else { 134 mDeviceName = getString(R.string.accessory_options); 135 mDeviceImgId = R.drawable.ic_qs_bluetooth_not_connected; 136 } 137 138 139 mUnpairing = savedInstanceState != null 140 && savedInstanceState.getBoolean(SAVE_STATE_UNPAIRING); 141 142 BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); 143 if (btAdapter != null) { 144 Set<BluetoothDevice> bondedDevices = btAdapter.getBondedDevices(); 145 for (BluetoothDevice device : bondedDevices) { 146 if (mDeviceAddress.equals(device.getAddress())) { 147 mDevice = device; 148 break; 149 } 150 } 151 } 152 153 if (mDevice == null) { 154 navigateBack(); 155 } 156 157 super.onCreate(savedInstanceState); 158 } 159 160 @Override onStart()161 public void onStart() { 162 super.onStart(); 163 if (mDevice != null && 164 (mDevice.getType() == BluetoothDevice.DEVICE_TYPE_LE || 165 mDevice.getType() == BluetoothDevice.DEVICE_TYPE_DUAL)) { 166 // Only LE devices support GATT 167 mDeviceGatt = mDevice.connectGatt(getActivity(), true, new GattBatteryCallbacks()); 168 } 169 // Set a broadcast receiver to let us know when the device has been removed 170 IntentFilter adapterIntentFilter = new IntentFilter(); 171 adapterIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 172 getActivity().registerReceiver(mBroadcastReceiver, adapterIntentFilter); 173 if (mDevice != null && mDevice.getBondState() == BluetoothDevice.BOND_NONE) { 174 mMsgHandler.removeCallbacks(mTimeoutRunnable); 175 navigateBack(); 176 } 177 } 178 179 @Override onPause()180 public void onPause() { 181 super.onPause(); 182 mHandler.removeCallbacks(mBailoutRunnable); 183 } 184 185 @Override onSaveInstanceState(@onNull Bundle savedInstanceState)186 public void onSaveInstanceState(@NonNull Bundle savedInstanceState) { 187 super.onSaveInstanceState(savedInstanceState); 188 savedInstanceState.putBoolean(SAVE_STATE_UNPAIRING, mUnpairing); 189 } 190 191 @Override onStop()192 public void onStop() { 193 super.onStop(); 194 if (mDeviceGatt != null) { 195 mDeviceGatt.close(); 196 } 197 getActivity().unregisterReceiver(mBroadcastReceiver); 198 } 199 200 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)201 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 202 final Context themedContext = getPreferenceManager().getContext(); 203 final PreferenceScreen screen = 204 getPreferenceManager().createPreferenceScreen(themedContext); 205 screen.setTitle(mDeviceName); 206 207 mUnpairPref = new Preference(themedContext); 208 updateUnpairPref(mUnpairPref); 209 mUnpairPref.setFragment(UnpairConfirmFragment.class.getName()); 210 UnpairConfirmFragment.prepareArgs(mUnpairPref.getExtras(), mDeviceName, mDeviceImgId); 211 screen.addPreference(mUnpairPref); 212 213 mBatteryPref = new Preference(themedContext); 214 screen.addPreference(mBatteryPref); 215 mBatteryPref.setVisible(false); 216 217 setPreferenceScreen(screen); 218 } 219 updateUnpairPref(Preference pref)220 private void updateUnpairPref(Preference pref) { 221 if (mUnpairing) { 222 pref.setTitle(R.string.accessory_unpairing); 223 pref.setEnabled(false); 224 } else { 225 pref.setTitle(R.string.accessory_unpair); 226 pref.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 unpairDevice()236 private void unpairDevice() { 237 if (mDevice != null) { 238 int state = mDevice.getBondState(); 239 240 if (state == BluetoothDevice.BOND_BONDING) { 241 mDevice.cancelBondProcess(); 242 } 243 244 if (state != BluetoothDevice.BOND_NONE) { 245 mUnpairing = true; 246 // Set a timeout, just in case we don't receive the unpair notification we 247 // use to finish the activity 248 mMsgHandler.postDelayed(mTimeoutRunnable, UNPAIR_TIMEOUT); 249 final boolean successful = mDevice.removeBond(); 250 if (successful) { 251 if (DEBUG) { 252 Log.d(TAG, "Bluetooth device successfully unpaired."); 253 } 254 // set the dialog to a waiting state 255 if (mUnpairPref != null) { 256 updateUnpairPref(mUnpairPref); 257 } 258 } else { 259 Log.e(TAG, "Failed to unpair Bluetooth Device: " + mDevice.getName()); 260 } 261 } 262 } else { 263 Log.e(TAG, "Bluetooth device not found. Address = " + mDeviceAddress); 264 } 265 } 266 267 private class GattBatteryCallbacks extends BluetoothGattCallback { 268 @Override onConnectionStateChange(BluetoothGatt gatt, int status, int newState)269 public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { 270 if (DEBUG) { 271 Log.d(TAG, "Connection status:" + status + " state:" + newState); 272 } 273 if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) { 274 gatt.discoverServices(); 275 } 276 } 277 278 @Override onServicesDiscovered(BluetoothGatt gatt, int status)279 public void onServicesDiscovered(BluetoothGatt gatt, int status) { 280 if (status != BluetoothGatt.GATT_SUCCESS) { 281 if (DEBUG) { 282 Log.e(TAG, "Service discovery failure on " + gatt); 283 } 284 return; 285 } 286 287 final BluetoothGattService battService = gatt.getService(GATT_BATTERY_SERVICE_UUID); 288 if (battService == null) { 289 if (DEBUG) { 290 Log.d(TAG, "No battery service"); 291 } 292 return; 293 } 294 295 final BluetoothGattCharacteristic battLevel = 296 battService.getCharacteristic(GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID); 297 if (battLevel == null) { 298 if (DEBUG) { 299 Log.d(TAG, "No battery level"); 300 } 301 return; 302 } 303 304 gatt.readCharacteristic(battLevel); 305 } 306 307 @Override onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)308 public void onCharacteristicRead(BluetoothGatt gatt, 309 BluetoothGattCharacteristic characteristic, int status) { 310 if (status != BluetoothGatt.GATT_SUCCESS) { 311 if (DEBUG) { 312 Log.e(TAG, "Read characteristic failure on " + gatt + " " + characteristic); 313 } 314 return; 315 } 316 317 if (GATT_BATTERY_LEVEL_CHARACTERISTIC_UUID.equals(characteristic.getUuid())) { 318 final int batteryLevel = 319 characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); 320 mMsgHandler.post(new Runnable() { 321 @Override 322 public void run() { 323 if (mBatteryPref != null && !mUnpairing) { 324 mBatteryPref.setTitle(getString(R.string.accessory_battery, 325 batteryLevel)); 326 mBatteryPref.setVisible(true); 327 } 328 } 329 }); 330 } 331 } 332 } 333 334 public static class UnpairConfirmFragment extends GuidedStepFragment { 335 prepareArgs(@onNull Bundle args, String deviceName, @DrawableRes int deviceImgId)336 public static void prepareArgs(@NonNull Bundle args, String deviceName, 337 @DrawableRes int deviceImgId) { 338 args.putString(ARG_ACCESSORY_NAME, deviceName); 339 args.putInt(ARG_ACCESSORY_ICON_ID, deviceImgId); 340 } 341 342 @NonNull 343 @Override onCreateGuidance(Bundle savedInstanceState)344 public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { 345 return new GuidanceStylist.Guidance( 346 getString(R.string.accessory_unpair), 347 null, 348 getArguments().getString(ARG_ACCESSORY_NAME), 349 getContext().getDrawable(getArguments().getInt(ARG_ACCESSORY_ICON_ID, 350 R.drawable.ic_qs_bluetooth_not_connected)) 351 ); 352 } 353 354 @Override onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)355 public void onCreateActions(@NonNull List<GuidedAction> actions, 356 Bundle savedInstanceState) { 357 final Context context = getContext(); 358 actions.add(new GuidedAction.Builder(context) 359 .clickAction(GuidedAction.ACTION_ID_OK).build()); 360 actions.add(new GuidedAction.Builder(context) 361 .clickAction(GuidedAction.ACTION_ID_CANCEL).build()); 362 } 363 364 @Override onGuidedActionClicked(GuidedAction action)365 public void onGuidedActionClicked(GuidedAction action) { 366 if (action.getId() == GuidedAction.ACTION_ID_OK) { 367 final BluetoothAccessoryFragment fragment = 368 (BluetoothAccessoryFragment) getTargetFragment(); 369 fragment.unpairDevice(); 370 } else if (action.getId() == GuidedAction.ACTION_ID_CANCEL) { 371 getFragmentManager().popBackStack(); 372 } else { 373 super.onGuidedActionClicked(action); 374 } 375 } 376 } 377 } 378