Home | History | Annotate | Download | only in bluetooth
      1 /*
      2  * Copyright (C) 2017 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 package com.android.car.settings.bluetooth;
     17 
     18 import android.bluetooth.BluetoothAdapter;
     19 import android.bluetooth.BluetoothClass;
     20 import android.bluetooth.BluetoothDevice;
     21 import android.content.Context;
     22 import android.content.res.Resources;
     23 import android.os.AsyncTask;
     24 import android.os.Handler;
     25 import android.os.Looper;
     26 import android.os.SystemProperties;
     27 import android.support.v7.widget.RecyclerView;
     28 import android.util.Pair;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.view.View.OnClickListener;
     32 import android.view.ViewGroup;
     33 import android.widget.ImageButton;
     34 import android.widget.ImageView;
     35 import android.widget.TextView;
     36 import android.widget.Toast;
     37 
     38 import androidx.car.widget.PagedListView;
     39 
     40 import com.android.car.settings.R;
     41 import com.android.car.settings.common.BaseFragment;
     42 import com.android.car.settings.common.Logger;
     43 import com.android.settingslib.bluetooth.BluetoothCallback;
     44 import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
     45 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
     46 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
     47 import com.android.settingslib.bluetooth.HidProfile;
     48 import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
     49 import com.android.settingslib.bluetooth.LocalBluetoothManager;
     50 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
     51 
     52 import java.util.ArrayList;
     53 import java.util.Collection;
     54 import java.util.Collections;
     55 import java.util.HashSet;
     56 import java.util.List;
     57 import java.util.Set;
     58 
     59 /**
     60  * Renders {@link android.bluetooth.BluetoothDevice} to a view to be displayed as a row in a list.
     61  */
     62 public class BluetoothDeviceListAdapter
     63         extends RecyclerView.Adapter<BluetoothDeviceListAdapter.ViewHolder>
     64         implements PagedListView.ItemCap, BluetoothCallback {
     65     private static final Logger LOG = new Logger(BluetoothDeviceListAdapter.class);
     66     // Copied from BluetoothDeviceNoNamePreferenceController.java
     67     private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
     68             "persist.bluetooth.showdeviceswithoutnames";
     69     private static final int DEVICE_ROW_TYPE = 1;
     70     private static final int BONDED_DEVICE_HEADER_TYPE = 2;
     71     private static final int AVAILABLE_DEVICE_HEADER_TYPE = 3;
     72     private static final int NUM_OF_HEADERS = 2;
     73     public static final int DELAY_MILLIS = 1000;
     74 
     75     private final Handler mHandler = new Handler(Looper.getMainLooper());
     76     private final HashSet<CachedBluetoothDevice> mBondedDevices = new HashSet<>();
     77     private final HashSet<CachedBluetoothDevice> mAvailableDevices = new HashSet<>();
     78     private final LocalBluetoothAdapter mLocalAdapter;
     79     private final LocalBluetoothManager mLocalManager;
     80     private final CachedBluetoothDeviceManager mDeviceManager;
     81     private final Context mContext;
     82     private final BaseFragment.FragmentController mFragmentController;
     83     private final boolean mShowDevicesWithoutNames;
     84 
     85     /* Talk-back descriptions for various BT icons */
     86     public final String mComputerDescription;
     87     public final String mInputPeripheralDescription;
     88     public final String mHeadsetDescription;
     89     public final String mPhoneDescription;
     90     public final String mImagingDescription;
     91     public final String mHeadphoneDescription;
     92     public final String mBluetoothDescription;
     93 
     94     private SortTask mSortTask;
     95 
     96     private ArrayList<CachedBluetoothDevice> mBondedDevicesSorted = new ArrayList<>();
     97     private ArrayList<CachedBluetoothDevice> mAvailableDevicesSorted = new ArrayList<>();
     98 
     99     class ViewHolder extends RecyclerView.ViewHolder {
    100         private final ImageView mIcon;
    101         private final TextView mTitle;
    102         private final TextView mDesc;
    103         private final ImageButton mActionButton;
    104         private final DeviceAttributeChangeCallback mCallback =
    105                 new DeviceAttributeChangeCallback(this);
    106 
    107         public ViewHolder(View view) {
    108             super(view);
    109             mTitle = (TextView) view.findViewById(R.id.title);
    110             mDesc = (TextView) view.findViewById(R.id.desc);
    111             mIcon = (ImageView) view.findViewById(R.id.icon);
    112             mActionButton = (ImageButton) view.findViewById(R.id.action);
    113             view.setOnClickListener(new BluetoothClickListener(this));
    114         }
    115     }
    116 
    117     public BluetoothDeviceListAdapter(
    118             Context context,
    119             LocalBluetoothManager localBluetoothManager,
    120             BaseFragment.FragmentController fragmentController) {
    121         mContext = context;
    122         mLocalManager = localBluetoothManager;
    123         mFragmentController = fragmentController;
    124         mLocalAdapter = mLocalManager.getBluetoothAdapter();
    125         mDeviceManager = mLocalManager.getCachedDeviceManager();
    126 
    127         Resources r = context.getResources();
    128         mComputerDescription = r.getString(R.string.bluetooth_talkback_computer);
    129         mInputPeripheralDescription = r.getString(
    130                 R.string.bluetooth_talkback_input_peripheral);
    131         mHeadsetDescription = r.getString(R.string.bluetooth_talkback_headset);
    132         mPhoneDescription = r.getString(R.string.bluetooth_talkback_phone);
    133         mImagingDescription = r.getString(R.string.bluetooth_talkback_imaging);
    134         mHeadphoneDescription = r.getString(R.string.bluetooth_talkback_headphone);
    135         mBluetoothDescription = r.getString(R.string.bluetooth_talkback_bluetooth);
    136         mShowDevicesWithoutNames =
    137                 SystemProperties.getBoolean(BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false);
    138     }
    139 
    140     public void start() {
    141         mLocalManager.getEventManager().registerCallback(this);
    142         if (mLocalAdapter.isEnabled()) {
    143             mLocalAdapter.startScanning(true);
    144             addBondDevices();
    145             addCachedDevices();
    146         }
    147         // create task here to avoid re-executing existing tasks.
    148         mSortTask = new SortTask();
    149         mSortTask.execute();
    150     }
    151 
    152     public void stop() {
    153         mLocalAdapter.stopScanning();
    154         mDeviceManager.clearNonBondedDevices();
    155         mLocalManager.getEventManager().unregisterCallback(this);
    156         mBondedDevices.clear();
    157         mBondedDevicesSorted.clear();
    158         mAvailableDevices.clear();
    159         mAvailableDevicesSorted.clear();
    160         mSortTask.cancel(true);
    161     }
    162 
    163     @Override
    164     public BluetoothDeviceListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent,
    165             int viewType) {
    166         View v;
    167         LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
    168         switch (viewType) {
    169             case BONDED_DEVICE_HEADER_TYPE:
    170                 v = layoutInflater.inflate(R.layout.single_text_line_item, parent, false);
    171                 v.setEnabled(false);
    172                 ((TextView) v.findViewById(R.id.title)).setText(
    173                         R.string.bluetooth_preference_paired_devices);
    174                 break;
    175             case AVAILABLE_DEVICE_HEADER_TYPE:
    176                 v = layoutInflater.inflate(R.layout.single_text_line_item, parent, false);
    177                 v.setEnabled(false);
    178                 ((TextView) v.findViewById(R.id.title)).setText(
    179                         R.string.bluetooth_preference_found_devices);
    180                 break;
    181             default:
    182                 v = layoutInflater.inflate(R.layout.icon_widget_line_item, parent, false);
    183         }
    184         return new ViewHolder(v);
    185     }
    186 
    187     @Override
    188     public int getItemCount() {
    189         return mAvailableDevicesSorted.size() + NUM_OF_HEADERS + mBondedDevicesSorted.size();
    190     }
    191 
    192     @Override
    193     public void setMaxItems(int maxItems) {
    194         // no limit in this list.
    195     }
    196 
    197     @Override
    198     public void onBindViewHolder(ViewHolder holder, int position) {
    199         final CachedBluetoothDevice bluetoothDevice = getItem(position);
    200         if (bluetoothDevice == null) {
    201             // this row is for in-list headers
    202             return;
    203         }
    204         if (holder.getOldPosition() != RecyclerView.NO_POSITION) {
    205             getItem(holder.getOldPosition()).unregisterCallback(holder.mCallback);
    206         }
    207         bluetoothDevice.registerCallback(holder.mCallback);
    208         holder.mTitle.setText(bluetoothDevice.getName());
    209         Pair<Integer, String> pair = getBtClassDrawableWithDescription(bluetoothDevice);
    210         holder.mIcon.setImageResource(pair.first);
    211         String summaryText = bluetoothDevice.getCarConnectionSummary();
    212         if (summaryText != null) {
    213             holder.mDesc.setText(summaryText);
    214             holder.mDesc.setVisibility(View.VISIBLE);
    215         } else {
    216             holder.mDesc.setVisibility(View.GONE);
    217         }
    218         if (BluetoothDeviceFilter.BONDED_DEVICE_FILTER.matches(bluetoothDevice.getDevice())) {
    219             holder.mActionButton.setVisibility(View.VISIBLE);
    220             holder.mActionButton.setOnClickListener(v -> {
    221                 mFragmentController.launchFragment(
    222                         BluetoothDetailFragment.getInstance(bluetoothDevice.getDevice()));
    223                 });
    224         } else {
    225             holder.mActionButton.setVisibility(View.GONE);
    226         }
    227     }
    228 
    229     @Override
    230     public int getItemViewType(int position) {
    231         // the first row is the header for the bonded device list;
    232         if (position == 0) {
    233             return BONDED_DEVICE_HEADER_TYPE;
    234         }
    235         // after the end of the bonded device list is the header of the available device list.
    236         if (position == mBondedDevicesSorted.size() + 1) {
    237             return AVAILABLE_DEVICE_HEADER_TYPE;
    238         }
    239         return DEVICE_ROW_TYPE;
    240     }
    241 
    242     private CachedBluetoothDevice getItem(int position) {
    243         if (position > 0 && position <= mBondedDevicesSorted.size()) {
    244             // off set the header row
    245             return mBondedDevicesSorted.get(position - 1);
    246         }
    247         if (position > mBondedDevicesSorted.size() + 1
    248                 && position <= mBondedDevicesSorted.size() + 1 + mAvailableDevicesSorted.size()) {
    249             // off set two header row and the size of bonded device list.
    250             return mAvailableDevicesSorted.get(
    251                     position - NUM_OF_HEADERS - mBondedDevicesSorted.size());
    252         }
    253         // otherwise it's a in list header
    254         return null;
    255     }
    256 
    257     // callback functions
    258     @Override
    259     public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
    260         if (addDevice(cachedDevice)) {
    261             ArrayList<CachedBluetoothDevice> devices = new ArrayList<>(mBondedDevices);
    262             Collections.sort(devices);
    263             mBondedDevicesSorted = devices;
    264             notifyDataSetChanged();
    265         }
    266     }
    267 
    268     @Override
    269     public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
    270         // the device might changed bonding state, so need to remove from both sets.
    271         if (mBondedDevices.remove(cachedDevice)) {
    272             mBondedDevicesSorted.remove(cachedDevice);
    273         }
    274         mAvailableDevices.remove(cachedDevice);
    275         notifyDataSetChanged();
    276     }
    277 
    278     @Override
    279     public void onBluetoothStateChanged(int bluetoothState) {
    280         switch (bluetoothState) {
    281             case BluetoothAdapter.STATE_OFF:
    282                 mBondedDevices.clear();
    283                 mBondedDevicesSorted.clear();
    284                 mAvailableDevices.clear();
    285                 mAvailableDevicesSorted.clear();
    286                 notifyDataSetChanged();
    287                 break;
    288             case BluetoothAdapter.STATE_ON:
    289                 mLocalAdapter.startScanning(true);
    290                 addBondDevices();
    291                 addCachedDevices();
    292                 break;
    293             default:
    294         }
    295     }
    296 
    297     public void reset() {
    298         mBondedDevices.clear();
    299         mBondedDevicesSorted.clear();
    300         mAvailableDevices.clear();
    301         mAvailableDevicesSorted.clear();
    302         mLocalAdapter.startScanning(true);
    303         addBondDevices();
    304         addCachedDevices();
    305         notifyDataSetChanged();
    306     }
    307 
    308     @Override
    309     public void onScanningStateChanged(boolean started) {
    310         // don't care
    311     }
    312 
    313     @Override
    314     public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
    315         onDeviceDeleted(cachedDevice);
    316         onDeviceAdded(cachedDevice);
    317     }
    318 
    319     /**
    320      * Call back for the first connection or the last connection to ANY device/profile. Not
    321      * suitable for monitor per device level connection.
    322      */
    323     @Override
    324     public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {
    325         onDeviceDeleted(cachedDevice);
    326         onDeviceAdded(cachedDevice);
    327     }
    328 
    329     @Override
    330     public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {
    331         // Not used (for now)
    332     }
    333 
    334     @Override
    335     public void onAudioModeChanged() {
    336         // Not used (for now)
    337     }
    338 
    339     private void addDevices(Collection<CachedBluetoothDevice> cachedDevices) {
    340         boolean needSort = false;
    341         for (CachedBluetoothDevice device : cachedDevices) {
    342             if (addDevice(device)) {
    343                 needSort = true;
    344             }
    345         }
    346         if (needSort) {
    347             ArrayList<CachedBluetoothDevice> devices =
    348                     new ArrayList<CachedBluetoothDevice>(mBondedDevices);
    349             Collections.sort(devices);
    350             mBondedDevicesSorted = devices;
    351             notifyDataSetChanged();
    352         }
    353     }
    354 
    355     /**
    356      * @return {@code true} if list changed and needed sort again.
    357      */
    358     private boolean addDevice(CachedBluetoothDevice cachedDevice) {
    359         boolean needSort = false;
    360         if (BluetoothDeviceFilter.BONDED_DEVICE_FILTER.matches(cachedDevice.getDevice())) {
    361             if (mBondedDevices.add(cachedDevice)) {
    362                 needSort = true;
    363             }
    364         }
    365         if (BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER.matches(cachedDevice.getDevice())
    366                 && (mShowDevicesWithoutNames || cachedDevice.hasHumanReadableName())) {
    367             // reset is done at SortTask.
    368             mAvailableDevices.add(cachedDevice);
    369         }
    370         return needSort;
    371     }
    372 
    373     private void addBondDevices() {
    374         Set<BluetoothDevice> bondedDevices = mLocalAdapter.getBondedDevices();
    375         if (bondedDevices == null) {
    376             return;
    377         }
    378         ArrayList<CachedBluetoothDevice> cachedBluetoothDevices = new ArrayList<>();
    379         for (BluetoothDevice device : bondedDevices) {
    380             CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
    381             if (cachedDevice == null) {
    382                 cachedDevice = mDeviceManager.addDevice(
    383                         mLocalAdapter, mLocalManager.getProfileManager(), device);
    384             }
    385             cachedBluetoothDevices.add(cachedDevice);
    386         }
    387         addDevices(cachedBluetoothDevices);
    388     }
    389 
    390     private void addCachedDevices() {
    391         addDevices(mDeviceManager.getCachedDevicesCopy());
    392     }
    393 
    394     private Pair<Integer, String> getBtClassDrawableWithDescription(
    395             CachedBluetoothDevice bluetoothDevice) {
    396         BluetoothClass btClass = bluetoothDevice.getBtClass();
    397         if (btClass != null) {
    398             switch (btClass.getMajorDeviceClass()) {
    399                 case BluetoothClass.Device.Major.COMPUTER:
    400                     return new Pair<>(R.drawable.ic_bt_laptop, mComputerDescription);
    401 
    402                 case BluetoothClass.Device.Major.PHONE:
    403                     return new Pair<>(R.drawable.ic_bt_cellphone, mPhoneDescription);
    404 
    405                 case BluetoothClass.Device.Major.PERIPHERAL:
    406                     return new Pair<>(HidProfile.getHidClassDrawable(btClass),
    407                             mInputPeripheralDescription);
    408 
    409                 case BluetoothClass.Device.Major.IMAGING:
    410                     return new Pair<>(R.drawable.ic_bt_imaging, mImagingDescription);
    411 
    412                 default:
    413                     // unrecognized device class; continue
    414             }
    415         } else {
    416             LOG.w("btClass is null");
    417         }
    418 
    419         List<LocalBluetoothProfile> profiles = bluetoothDevice.getProfiles();
    420         for (LocalBluetoothProfile profile : profiles) {
    421             int resId = profile.getDrawableResource(btClass);
    422             if (resId != 0) {
    423                 return new Pair<Integer, String>(resId, null);
    424             }
    425         }
    426         if (btClass != null) {
    427             if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
    428                 return new Pair<Integer, String>(R.drawable.ic_bt_headset_hfp, mHeadsetDescription);
    429             }
    430             if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
    431                 return new Pair<Integer, String>(R.drawable.ic_bt_headphones_a2dp,
    432                         mHeadphoneDescription);
    433             }
    434         }
    435         return new Pair<Integer, String>(R.drawable.ic_settings_bluetooth, mBluetoothDescription);
    436     }
    437 
    438     /**
    439      * Updates device render upon device attribute change.
    440      */
    441     // TODO: This is a walk around for handling attribute callback. Since the callback doesn't
    442     // contain the information about which device needs to be updated, we have to maintain a
    443     // local reference to the device. Fix the code in CachedBluetoothDevice.Callback to return
    444     // a reference of the device been updated.
    445     private class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback {
    446 
    447         private final ViewHolder mViewHolder;
    448 
    449         DeviceAttributeChangeCallback(ViewHolder viewHolder) {
    450             mViewHolder = viewHolder;
    451         }
    452 
    453         @Override
    454         public void onDeviceAttributesChanged() {
    455             notifyItemChanged(mViewHolder.getAdapterPosition());
    456         }
    457     }
    458 
    459     private class BluetoothClickListener implements OnClickListener {
    460         private final ViewHolder mViewHolder;
    461 
    462         BluetoothClickListener(ViewHolder viewHolder) {
    463             mViewHolder = viewHolder;
    464         }
    465 
    466         @Override
    467         public void onClick(View v) {
    468             CachedBluetoothDevice device = getItem(mViewHolder.getAdapterPosition());
    469             int bondState = device.getBondState();
    470 
    471             if (device.isConnected()) {
    472                 // TODO: ask user for confirmation
    473                 device.disconnect();
    474             } else if (bondState == BluetoothDevice.BOND_BONDED) {
    475                 device.connect(true);
    476             } else if (bondState == BluetoothDevice.BOND_NONE) {
    477                 if (!device.startPairing()) {
    478                     showError(device.getName(),
    479                             R.string.bluetooth_pairing_error_message);
    480                     return;
    481                 }
    482                 // allow MAP and PBAP since this is client side, permission should be handled on
    483                 // server side. i.e. the phone side.
    484                 device.setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
    485                 device.setMessagePermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
    486             }
    487         }
    488     }
    489 
    490     private void showError(String name, int messageResId) {
    491         String message = mContext.getString(messageResId, name);
    492         Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
    493     }
    494 
    495     /**
    496      * Provides an ordered bt device list periodically.
    497      */
    498     // TODO: improve the way we sort BT devices. Ideally we should keep all devices in a TreeSet
    499     // and as devices are added the correct order is maintained, that requires a consistent
    500     // logic between equals and compareTo function, unfortunately it's not the case in
    501     // CachedBluetoothDevice class. Fix that and improve the way we order devices.
    502     private class SortTask extends AsyncTask<Void, Void, ArrayList<CachedBluetoothDevice>> {
    503 
    504         /**
    505          * Returns {code null} if no changed are made.
    506          */
    507         @Override
    508         protected ArrayList<CachedBluetoothDevice> doInBackground(Void... v) {
    509             if (mAvailableDevicesSorted != null
    510                     && mAvailableDevicesSorted.size() == mAvailableDevices.size()) {
    511                 return null;
    512             }
    513             ArrayList<CachedBluetoothDevice> devices =
    514                     new ArrayList<CachedBluetoothDevice>(mAvailableDevices);
    515             Collections.sort(devices);
    516             return devices;
    517         }
    518 
    519         @Override
    520         protected void onPostExecute(ArrayList<CachedBluetoothDevice> devices) {
    521             // skip if no changes are made.
    522             if (devices != null) {
    523                 mAvailableDevicesSorted = devices;
    524                 notifyDataSetChanged();
    525             }
    526             mHandler.postDelayed(new Runnable() {
    527                 public void run() {
    528                     mSortTask = new SortTask();
    529                     mSortTask.execute();
    530                 }
    531             }, DELAY_MILLIS);
    532         }
    533     }
    534 }
    535