Home | History | Annotate | Download | only in accessories
      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 
    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 
    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
    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
    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
    170     public void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
    171         super.onSaveInstanceState(savedInstanceState);
    172         savedInstanceState.putBoolean(SAVE_STATE_UNPAIRING, mUnpairing);
    173     }
    174 
    175     @Override
    176     public void onStop() {
    177         super.onStop();
    178         if (mDeviceGatt != null) {
    179             mDeviceGatt.close();
    180         }
    181         getActivity().unregisterReceiver(mBroadcastReceiver);
    182     }
    183 
    184     @Override
    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 
    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 
    214     private void navigateBack() {
    215         if (!getFragmentManager().popBackStackImmediate() && getActivity() != null) {
    216             getActivity().onBackPressed();
    217         }
    218     }
    219 
    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
    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
    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
    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 
    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
    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
    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
    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