• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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