| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.car.settings.bluetooth; |
| |
| import android.annotation.NonNull; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothClass; |
| import android.bluetooth.BluetoothDevice; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.os.AsyncTask; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.support.v7.widget.RecyclerView; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.ViewGroup; |
| import android.widget.ImageButton; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import com.android.car.settings.R; |
| import com.android.car.settings.common.BaseFragment; |
| import com.android.car.view.PagedListView; |
| import com.android.settingslib.bluetooth.BluetoothCallback; |
| import com.android.settingslib.bluetooth.BluetoothDeviceFilter; |
| import com.android.settingslib.bluetooth.CachedBluetoothDevice; |
| import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; |
| import com.android.settingslib.bluetooth.LocalBluetoothAdapter; |
| import com.android.settingslib.bluetooth.LocalBluetoothManager; |
| import com.android.settingslib.bluetooth.LocalBluetoothProfile; |
| import com.android.settingslib.bluetooth.HidProfile; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Renders {@link android.bluetooth.BluetoothDevice} to a view to be displayed as a row in a list. |
| */ |
| public class BluetoothDeviceListAdapter |
| extends RecyclerView.Adapter<BluetoothDeviceListAdapter.ViewHolder> |
| implements PagedListView.ItemCap, BluetoothCallback { |
| private static final String TAG = "BluetoothDeviceListAdapter"; |
| private static final int DEVICE_ROW_TYPE = 1; |
| private static final int BONDED_DEVICE_HEADER_TYPE = 2; |
| private static final int AVAILABLE_DEVICE_HEADER_TYPE = 3; |
| private static final int NUM_OF_HEADERS = 2; |
| public static final int DELAY_MILLIS = 1000; |
| |
| private final Handler mHandler = new Handler(Looper.getMainLooper()); |
| private final HashSet<CachedBluetoothDevice> mBondedDevices = new HashSet<>(); |
| private final HashSet<CachedBluetoothDevice> mAvailableDevices = new HashSet<>(); |
| private final LocalBluetoothAdapter mLocalAdapter; |
| private final LocalBluetoothManager mLocalManager; |
| private final CachedBluetoothDeviceManager mDeviceManager; |
| private final Context mContext; |
| private final BaseFragment.FragmentController mFragmentController; |
| |
| /* Talk-back descriptions for various BT icons */ |
| public final String mComputerDescription; |
| public final String mInputPeripheralDescription; |
| public final String mHeadsetDescription; |
| public final String mPhoneDescription; |
| public final String mImagingDescription; |
| public final String mHeadphoneDescription; |
| public final String mBluetoothDescription; |
| |
| private SortTask mSortTask = new SortTask(); |
| |
| private ArrayList<CachedBluetoothDevice> mBondedDevicesSorted = new ArrayList<>(); |
| private ArrayList<CachedBluetoothDevice> mAvailableDevicesSorted = new ArrayList<>(); |
| |
| class ViewHolder extends RecyclerView.ViewHolder { |
| private final ImageView mIcon; |
| private final TextView mTitle; |
| private final TextView mDesc; |
| private final ImageButton mActionButton; |
| private final DeviceAttributeChangeCallback mCallback = |
| new DeviceAttributeChangeCallback(this); |
| |
| public ViewHolder(View view) { |
| super(view); |
| mTitle = (TextView) view.findViewById(R.id.title); |
| mDesc = (TextView) view.findViewById(R.id.desc); |
| mIcon = (ImageView) view.findViewById(R.id.icon); |
| mActionButton = (ImageButton) view.findViewById(R.id.action); |
| view.setOnClickListener(new BluetoothClickListener(this)); |
| } |
| } |
| |
| public BluetoothDeviceListAdapter( |
| Context context, |
| LocalBluetoothManager localBluetoothManager, |
| BaseFragment.FragmentController fragmentController) { |
| mContext = context; |
| mLocalManager = localBluetoothManager; |
| mFragmentController = fragmentController; |
| mLocalAdapter = mLocalManager.getBluetoothAdapter(); |
| mDeviceManager = mLocalManager.getCachedDeviceManager(); |
| |
| Resources r = context.getResources(); |
| mComputerDescription = r.getString(R.string.bluetooth_talkback_computer); |
| mInputPeripheralDescription = r.getString( |
| R.string.bluetooth_talkback_input_peripheral); |
| mHeadsetDescription = r.getString(R.string.bluetooth_talkback_headset); |
| mPhoneDescription = r.getString(R.string.bluetooth_talkback_phone); |
| mImagingDescription = r.getString(R.string.bluetooth_talkback_imaging); |
| mHeadphoneDescription = r.getString(R.string.bluetooth_talkback_headphone); |
| mBluetoothDescription = r.getString(R.string.bluetooth_talkback_bluetooth); |
| } |
| |
| public void start() { |
| mLocalManager.getEventManager().registerCallback(this); |
| if (mLocalAdapter.isEnabled()) { |
| mLocalAdapter.startScanning(true); |
| addBondDevices(); |
| addCachedDevices(); |
| } |
| mSortTask.execute(); |
| } |
| |
| public void stop() { |
| mLocalAdapter.stopScanning(); |
| mDeviceManager.clearNonBondedDevices(); |
| mLocalManager.getEventManager().unregisterCallback(this); |
| mBondedDevices.clear(); |
| mAvailableDevices.clear(); |
| mSortTask.cancel(true); |
| } |
| |
| @Override |
| public BluetoothDeviceListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, |
| int viewType) { |
| View v; |
| LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); |
| switch (viewType) { |
| case BONDED_DEVICE_HEADER_TYPE: |
| v = layoutInflater.inflate(R.layout.single_text_line_item, parent, false); |
| v.setEnabled(false); |
| ((TextView) v.findViewById(R.id.title)).setText( |
| R.string.bluetooth_preference_paired_devices); |
| break; |
| case AVAILABLE_DEVICE_HEADER_TYPE: |
| v = layoutInflater.inflate(R.layout.single_text_line_item, parent, false); |
| v.setEnabled(false); |
| ((TextView) v.findViewById(R.id.title)).setText( |
| R.string.bluetooth_preference_found_devices); |
| break; |
| default: |
| v = layoutInflater.inflate(R.layout.icon_widget_line_item, parent, false); |
| } |
| return new ViewHolder(v); |
| } |
| |
| @Override |
| public int getItemCount() { |
| return mAvailableDevicesSorted.size() + NUM_OF_HEADERS + mBondedDevicesSorted.size(); |
| } |
| |
| @Override |
| public void setMaxItems(int maxItems) { |
| // no limit in this list. |
| } |
| |
| @Override |
| public void onBindViewHolder(ViewHolder holder, int position) { |
| final CachedBluetoothDevice bluetoothDevice = getItem(position); |
| if (bluetoothDevice == null) { |
| // this row is for in-list headers |
| return; |
| } |
| if (holder.getOldPosition() != RecyclerView.NO_POSITION) { |
| getItem(holder.getOldPosition()).unregisterCallback(holder.mCallback); |
| } |
| bluetoothDevice.registerCallback(holder.mCallback); |
| holder.mTitle.setText(bluetoothDevice.getName()); |
| Pair<Integer, String> pair = getBtClassDrawableWithDescription(bluetoothDevice); |
| holder.mIcon.setImageResource(pair.first); |
| int summaryResourceId = bluetoothDevice.getConnectionSummary(); |
| if (summaryResourceId != 0) { |
| holder.mDesc.setText(summaryResourceId); |
| holder.mDesc.setVisibility(View.VISIBLE); |
| } else { |
| holder.mDesc.setVisibility(View.GONE); |
| } |
| if (BluetoothDeviceFilter.BONDED_DEVICE_FILTER.matches(bluetoothDevice.getDevice())) { |
| holder.mActionButton.setVisibility(View.VISIBLE); |
| holder.mActionButton.setOnClickListener(v -> { |
| mFragmentController.launchFragment( |
| BluetoothDetailFragment.getInstance(bluetoothDevice.getDevice())); |
| }); |
| } else { |
| holder.mActionButton.setVisibility(View.GONE); |
| } |
| } |
| |
| @Override |
| public int getItemViewType(int position) { |
| // the first row is the header for the bonded device list; |
| if (position == 0) { |
| return BONDED_DEVICE_HEADER_TYPE; |
| } |
| // after the end of the bonded device list is the header of the available device list. |
| if (position == mBondedDevicesSorted.size() + 1) { |
| return AVAILABLE_DEVICE_HEADER_TYPE; |
| } |
| return DEVICE_ROW_TYPE; |
| } |
| |
| private CachedBluetoothDevice getItem(int position) { |
| if (position > 0 && position <= mBondedDevicesSorted.size()) { |
| // off set the header row |
| return mBondedDevicesSorted.get(position - 1); |
| } |
| if (position > mBondedDevicesSorted.size() + 1 |
| && position <= mBondedDevicesSorted.size() + 1 + mAvailableDevicesSorted.size()) { |
| // off set two header row and the size of bonded device list. |
| return mAvailableDevicesSorted.get( |
| position - NUM_OF_HEADERS - mBondedDevicesSorted.size()); |
| } |
| // otherwise it's a in list header |
| return null; |
| } |
| |
| // callback functions |
| @Override |
| public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { |
| if (addDevice(cachedDevice)) { |
| ArrayList<CachedBluetoothDevice> devices = new ArrayList<>(mBondedDevices); |
| Collections.sort(devices); |
| mBondedDevicesSorted = devices; |
| notifyDataSetChanged(); |
| } |
| } |
| |
| @Override |
| public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { |
| onDeviceDeleted(cachedDevice, true /* refresh */); |
| } |
| |
| @Override |
| public void onBluetoothStateChanged(int bluetoothState) { |
| switch (bluetoothState) { |
| case BluetoothAdapter.STATE_OFF: |
| mBondedDevices.clear(); |
| mBondedDevicesSorted.clear(); |
| mAvailableDevices.clear(); |
| mAvailableDevicesSorted.clear(); |
| notifyDataSetChanged(); |
| break; |
| case BluetoothAdapter.STATE_ON: |
| mLocalAdapter.startScanning(true); |
| addBondDevices(); |
| addCachedDevices(); |
| break; |
| default: |
| } |
| } |
| |
| @Override |
| public void onScanningStateChanged(boolean started) { |
| // don't care |
| } |
| |
| @Override |
| public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { |
| onDeviceDeleted(cachedDevice, false /* refresh */); |
| onDeviceAdded(cachedDevice); |
| } |
| |
| /** |
| * Call back for the first connection or the last connection to ANY device/profile. Not |
| * suitable for monitor per device level connection. |
| */ |
| @Override |
| public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { |
| onDeviceDeleted(cachedDevice, false); |
| onDeviceAdded(cachedDevice); |
| } |
| |
| private void onDeviceDeleted(CachedBluetoothDevice cachedDevice, boolean refresh) { |
| // the device might changed bonding state, so need to remove from both sets. |
| if (mBondedDevices.remove(cachedDevice)) { |
| mBondedDevicesSorted.remove(cachedDevice); |
| } |
| mAvailableDevices.remove(cachedDevice); |
| if (refresh) { |
| notifyDataSetChanged(); |
| } |
| } |
| |
| private void addDevices(Collection<CachedBluetoothDevice> cachedDevices) { |
| boolean needSort = false; |
| for (CachedBluetoothDevice device : cachedDevices) { |
| if (addDevice(device)) { |
| needSort = true; |
| } |
| } |
| if (needSort) { |
| ArrayList<CachedBluetoothDevice> devices = |
| new ArrayList<CachedBluetoothDevice>(mBondedDevices); |
| Collections.sort(devices); |
| mBondedDevicesSorted = devices; |
| notifyDataSetChanged(); |
| } |
| } |
| |
| /** |
| * @return {@code true} if list changed and needed sort again. |
| */ |
| private boolean addDevice(CachedBluetoothDevice cachedDevice) { |
| boolean needSort = false; |
| if (BluetoothDeviceFilter.BONDED_DEVICE_FILTER.matches(cachedDevice.getDevice())) { |
| if (mBondedDevices.add(cachedDevice)) { |
| needSort = true; |
| } |
| } |
| if (BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER.matches(cachedDevice.getDevice())) { |
| // refresh is done at SortTask. |
| mAvailableDevices.add(cachedDevice); |
| } |
| return needSort; |
| } |
| |
| private void addBondDevices() { |
| Set<BluetoothDevice> bondedDevices = mLocalAdapter.getBondedDevices(); |
| if (bondedDevices == null) { |
| return; |
| } |
| ArrayList<CachedBluetoothDevice> cachedBluetoothDevices = new ArrayList<>(); |
| for (BluetoothDevice device : bondedDevices) { |
| CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device); |
| if (cachedDevice == null) { |
| cachedDevice = mDeviceManager.addDevice( |
| mLocalAdapter, mLocalManager.getProfileManager(), device); |
| } |
| cachedBluetoothDevices.add(cachedDevice); |
| } |
| addDevices(cachedBluetoothDevices); |
| } |
| |
| private void addCachedDevices() { |
| addDevices(mDeviceManager.getCachedDevicesCopy()); |
| } |
| |
| private Pair<Integer, String> getBtClassDrawableWithDescription( |
| CachedBluetoothDevice bluetoothDevice) { |
| BluetoothClass btClass = bluetoothDevice.getBtClass(); |
| if (btClass != null) { |
| switch (btClass.getMajorDeviceClass()) { |
| case BluetoothClass.Device.Major.COMPUTER: |
| return new Pair<>(R.drawable.ic_bt_laptop, mComputerDescription); |
| |
| case BluetoothClass.Device.Major.PHONE: |
| return new Pair<>(R.drawable.ic_bt_cellphone, mPhoneDescription); |
| |
| case BluetoothClass.Device.Major.PERIPHERAL: |
| return new Pair<>(HidProfile.getHidClassDrawable(btClass), |
| mInputPeripheralDescription); |
| |
| case BluetoothClass.Device.Major.IMAGING: |
| return new Pair<>(R.drawable.ic_bt_imaging, mImagingDescription); |
| |
| default: |
| // unrecognized device class; continue |
| } |
| } else { |
| Log.w(TAG, "btClass is null"); |
| } |
| |
| List<LocalBluetoothProfile> profiles = bluetoothDevice.getProfiles(); |
| for (LocalBluetoothProfile profile : profiles) { |
| int resId = profile.getDrawableResource(btClass); |
| if (resId != 0) { |
| return new Pair<Integer, String>(resId, null); |
| } |
| } |
| if (btClass != null) { |
| if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) { |
| return new Pair<Integer, String>(R.drawable.ic_bt_headset_hfp, mHeadsetDescription); |
| } |
| if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) { |
| return new Pair<Integer, String>(R.drawable.ic_bt_headphones_a2dp, |
| mHeadphoneDescription); |
| } |
| } |
| return new Pair<Integer, String>(R.drawable.ic_settings_bluetooth, mBluetoothDescription); |
| } |
| |
| /** |
| * Updates device render upon device attribute change. |
| */ |
| // TODO: This is a walk around for handling attribute callback. Since the callback doesn't |
| // contain the information about which device needs to be updated, we have to maintain a |
| // local reference to the device. Fix the code in CachedBluetoothDevice.Callback to return |
| // a reference of the device been updated. |
| private class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback { |
| |
| private final ViewHolder mViewHolder; |
| |
| DeviceAttributeChangeCallback(ViewHolder viewHolder) { |
| mViewHolder = viewHolder; |
| } |
| |
| @Override |
| public void onDeviceAttributesChanged() { |
| notifyItemChanged(mViewHolder.getAdapterPosition()); |
| } |
| } |
| |
| private class BluetoothClickListener implements OnClickListener { |
| private final ViewHolder mViewHolder; |
| |
| BluetoothClickListener(ViewHolder viewHolder) { |
| mViewHolder = viewHolder; |
| } |
| |
| @Override |
| public void onClick(View v) { |
| CachedBluetoothDevice device = getItem(mViewHolder.getAdapterPosition()); |
| int bondState = device.getBondState(); |
| |
| if (device.isConnected()) { |
| // TODO: ask user for confirmation |
| device.disconnect(); |
| } else if (bondState == BluetoothDevice.BOND_BONDED) { |
| device.connect(true); |
| } else if (bondState == BluetoothDevice.BOND_NONE) { |
| if (!device.startPairing()) { |
| showError(device.getName(), |
| R.string.bluetooth_pairing_error_message); |
| } |
| } |
| } |
| } |
| |
| private void showError(String name, int messageResId) { |
| String message = mContext.getString(messageResId, name); |
| Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); |
| } |
| |
| /** |
| * Provides an ordered bt device list periodically. |
| */ |
| // TODO: improve the way we sort BT devices. Ideally we should keep all devices in a TreeSet |
| // and as devices are added the correct order is maintained, that requires a consistent |
| // logic between equals and compareTo function, unfortunately it's not the case in |
| // CachedBluetoothDevice class. Fix that and improve the way we order devices. |
| private class SortTask extends AsyncTask<Void, Void, ArrayList<CachedBluetoothDevice>> { |
| |
| /** |
| * Returns {code null} if no changed are made. |
| */ |
| @Override |
| protected ArrayList<CachedBluetoothDevice> doInBackground(Void... v) { |
| if (mAvailableDevicesSorted != null |
| && mAvailableDevicesSorted.size() == mAvailableDevices.size()) { |
| return null; |
| } |
| ArrayList<CachedBluetoothDevice> devices = |
| new ArrayList<CachedBluetoothDevice>(mAvailableDevices); |
| Collections.sort(devices); |
| return devices; |
| } |
| |
| @Override |
| protected void onPostExecute(ArrayList<CachedBluetoothDevice> devices) { |
| // skip if no changes are made. |
| if (devices != null) { |
| mAvailableDevicesSorted = devices; |
| notifyDataSetChanged(); |
| } |
| mHandler.postDelayed(new Runnable() { |
| public void run() { |
| mSortTask = new SortTask(); |
| mSortTask.execute(); |
| } |
| }, DELAY_MILLIS); |
| } |
| } |
| } |