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