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