/* * Copyright (C) 2013 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.companiondevicemanager; import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal; import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress; import static com.android.internal.util.ArrayUtils.isEmpty; import static com.android.internal.util.CollectionUtils.emptyIfNull; import static com.android.internal.util.CollectionUtils.size; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothManager; import android.bluetooth.le.BluetoothLeScanner; import android.bluetooth.le.ScanCallback; import android.bluetooth.le.ScanFilter; import android.bluetooth.le.ScanResult; import android.bluetooth.le.ScanSettings; import android.companion.AssociationRequest; import android.companion.BluetoothDeviceFilter; import android.companion.BluetoothLeDeviceFilter; import android.companion.DeviceFilter; import android.companion.ICompanionDeviceDiscoveryService; import android.companion.ICompanionDeviceDiscoveryServiceCallback; import android.companion.IFindDeviceCallback; import android.companion.WifiDeviceFilter; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.wifi.WifiManager; import android.os.Handler; import android.os.IBinder; import android.os.Parcelable; import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; import com.android.internal.util.Preconditions; import java.util.ArrayList; import java.util.List; import java.util.Objects; public class DeviceDiscoveryService extends Service { private static final boolean DEBUG = false; private static final String LOG_TAG = "DeviceDiscoveryService"; private static final long SCAN_TIMEOUT = 20000; static DeviceDiscoveryService sInstance; private BluetoothAdapter mBluetoothAdapter; private WifiManager mWifiManager; @Nullable private BluetoothLeScanner mBLEScanner; private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().build(); private List> mFilters; private List mBLEFilters; private List mBluetoothFilters; private List mWifiFilters; private List mBLEScanFilters; AssociationRequest mRequest; List mDevicesFound; DeviceFilterPair mSelectedDevice; DevicesAdapter mDevicesAdapter; IFindDeviceCallback mFindCallback; ICompanionDeviceDiscoveryServiceCallback mServiceCallback; boolean mIsScanning = false; @Nullable DeviceChooserActivity mActivity = null; private final ICompanionDeviceDiscoveryService mBinder = new ICompanionDeviceDiscoveryService.Stub() { @Override public void startDiscovery(AssociationRequest request, String callingPackage, IFindDeviceCallback findCallback, ICompanionDeviceDiscoveryServiceCallback serviceCallback) { if (DEBUG) { Log.i(LOG_TAG, "startDiscovery() called with: filter = [" + request + "], findCallback = [" + findCallback + "]" + "], serviceCallback = [" + serviceCallback + "]"); } mFindCallback = findCallback; mServiceCallback = serviceCallback; DeviceDiscoveryService.this.startDiscovery(request); } }; private ScanCallback mBLEScanCallback; private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver; private WifiBroadcastReceiver mWifiBroadcastReceiver; @Override public IBinder onBind(Intent intent) { if (DEBUG) Log.i(LOG_TAG, "onBind(" + intent + ")"); return mBinder.asBinder(); } @Override public void onCreate() { super.onCreate(); if (DEBUG) Log.i(LOG_TAG, "onCreate()"); mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter(); mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner(); mWifiManager = getSystemService(WifiManager.class); mDevicesFound = new ArrayList<>(); mDevicesAdapter = new DevicesAdapter(); sInstance = this; } private void startDiscovery(AssociationRequest request) { if (!request.equals(mRequest)) { mRequest = request; mFilters = request.getDeviceFilters(); mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class); mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class); mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLeDeviceFilter.class); mBLEScanFilters = CollectionUtils.map(mBLEFilters, BluetoothLeDeviceFilter::getScanFilter); reset(); } else if (DEBUG) Log.i(LOG_TAG, "startDiscovery: duplicate request: " + request); if (!ArrayUtils.isEmpty(mDevicesFound)) { onReadyToShowUI(); } // If filtering to get single device by mac address, also search in the set of already // bonded devices to allow linking those directly String singleMacAddressFilter = null; if (mRequest.isSingleDevice()) { int numFilters = size(mBluetoothFilters); for (int i = 0; i < numFilters; i++) { BluetoothDeviceFilter filter = mBluetoothFilters.get(i); if (!TextUtils.isEmpty(filter.getAddress())) { singleMacAddressFilter = filter.getAddress(); break; } } } if (singleMacAddressFilter != null) { for (BluetoothDevice dev : emptyIfNull(mBluetoothAdapter.getBondedDevices())) { onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters)); } } if (shouldScan(mBluetoothFilters)) { final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(BluetoothDevice.ACTION_FOUND); mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver(); registerReceiver(mBluetoothBroadcastReceiver, intentFilter); mBluetoothAdapter.startDiscovery(); } if (shouldScan(mBLEFilters) && mBLEScanner != null) { mBLEScanCallback = new BLEScanCallback(); mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback); } if (shouldScan(mWifiFilters)) { mWifiBroadcastReceiver = new WifiBroadcastReceiver(); registerReceiver(mWifiBroadcastReceiver, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); mWifiManager.startScan(); } mIsScanning = true; Handler.getMain().sendMessageDelayed( obtainMessage(DeviceDiscoveryService::stopScan, this), SCAN_TIMEOUT); } private boolean shouldScan(List mediumSpecificFilters) { return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters); } private void reset() { if (DEBUG) Log.i(LOG_TAG, "reset()"); stopScan(); mDevicesFound.clear(); mSelectedDevice = null; notifyDataSetChanged(); } @Override public boolean onUnbind(Intent intent) { stopScan(); return super.onUnbind(intent); } private void stopScan() { if (DEBUG) Log.i(LOG_TAG, "stopScan()"); if (!mIsScanning) return; mIsScanning = false; DeviceChooserActivity activity = mActivity; if (activity != null) { if (activity.mDeviceListView != null) { activity.mDeviceListView.removeFooterView(activity.mLoadingIndicator); } mActivity = null; } mBluetoothAdapter.cancelDiscovery(); if (mBluetoothBroadcastReceiver != null) { unregisterReceiver(mBluetoothBroadcastReceiver); mBluetoothBroadcastReceiver = null; } if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback); if (mWifiBroadcastReceiver != null) { unregisterReceiver(mWifiBroadcastReceiver); mWifiBroadcastReceiver = null; } } private void onDeviceFound(@Nullable DeviceFilterPair device) { if (device == null) return; if (mDevicesFound.contains(device)) { return; } if (DEBUG) Log.i(LOG_TAG, "Found device " + device); if (mDevicesFound.isEmpty()) { onReadyToShowUI(); } mDevicesFound.add(device); notifyDataSetChanged(); } private void notifyDataSetChanged() { Handler.getMain().sendMessage(obtainMessage( DevicesAdapter::notifyDataSetChanged, mDevicesAdapter)); } //TODO also, on timeout -> call onFailure private void onReadyToShowUI() { try { mFindCallback.onSuccess(PendingIntent.getActivity( this, 0, new Intent(this, DeviceChooserActivity.class), PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE)); } catch (RemoteException e) { throw new RuntimeException(e); } } private void onDeviceLost(@Nullable DeviceFilterPair device) { mDevicesFound.remove(device); notifyDataSetChanged(); if (DEBUG) Log.i(LOG_TAG, "Lost device " + device.getDisplayName()); } void onDeviceSelected(String callingPackage, String deviceAddress) { try { mServiceCallback.onDeviceSelected( //TODO is this the right userId? callingPackage, getUserId(), deviceAddress); } catch (RemoteException e) { Log.e(LOG_TAG, "Failed to record association: " + callingPackage + " <-> " + deviceAddress); } } void onCancel() { if (DEBUG) Log.i(LOG_TAG, "onCancel()"); try { mServiceCallback.onDeviceSelectionCancel(); } catch (RemoteException e) { throw new RuntimeException(e); } } class DevicesAdapter extends ArrayAdapter { private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth); private Drawable WIFI_ICON = icon(com.android.internal.R.drawable.ic_wifi_signal_3); private Drawable icon(int drawableRes) { Drawable icon = getResources().getDrawable(drawableRes, null); icon.setTint(Color.DKGRAY); return icon; } public DevicesAdapter() { super(DeviceDiscoveryService.this, 0, mDevicesFound); } @Override public View getView( int position, @Nullable View convertView, @NonNull ViewGroup parent) { TextView view = convertView instanceof TextView ? (TextView) convertView : newView(); bind(view, getItem(position)); return view; } private void bind(TextView textView, DeviceFilterPair device) { textView.setText(device.getDisplayName()); textView.setBackgroundColor( device.equals(mSelectedDevice) ? Color.GRAY : Color.TRANSPARENT); textView.setCompoundDrawablesWithIntrinsicBounds( device.device instanceof android.net.wifi.ScanResult ? WIFI_ICON : BLUETOOTH_ICON, null, null, null); textView.setOnClickListener((view) -> { mSelectedDevice = device; notifyDataSetChanged(); }); } //TODO move to a layout file private TextView newView() { final TextView textView = new TextView(DeviceDiscoveryService.this); textView.setTextColor(Color.BLACK); final int padding = DeviceChooserActivity.getPadding(getResources()); textView.setPadding(padding, padding, padding, padding); textView.setCompoundDrawablePadding(padding); return textView; } } /** * A pair of device and a filter that matched this device if any. * * @param device type */ static class DeviceFilterPair { public final T device; @Nullable public final DeviceFilter filter; private DeviceFilterPair(T device, @Nullable DeviceFilter filter) { this.device = device; this.filter = filter; } /** * {@code (device, null)} if the filters list is empty or null * {@code null} if none of the provided filters match the device * {@code (device, filter)} where filter is among the list of filters and matches the device */ @Nullable public static DeviceFilterPair findMatch( T dev, @Nullable List> filters) { if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null); final DeviceFilter matchingFilter = CollectionUtils.find(filters, f -> f.matches(dev)); DeviceFilterPair result = matchingFilter != null ? new DeviceFilterPair<>(dev, matchingFilter) : null; if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters + ") -> " + result); return result; } public String getDisplayName() { if (filter == null) { Preconditions.checkNotNull(device); if (device instanceof BluetoothDevice) { return getDeviceDisplayNameInternal((BluetoothDevice) device); } else if (device instanceof android.net.wifi.ScanResult) { return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device); } else if (device instanceof ScanResult) { return getDeviceDisplayNameInternal(((ScanResult) device).getDevice()); } else { throw new IllegalArgumentException("Unknown device type: " + device.getClass()); } } return filter.getDeviceDisplayName(device); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; DeviceFilterPair that = (DeviceFilterPair) o; return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device)); } @Override public int hashCode() { return Objects.hash(getDeviceMacAddress(device)); } @Override public String toString() { return "DeviceFilterPair{" + "device=" + device + ", filter=" + filter + '}'; } } private class BLEScanCallback extends ScanCallback { public BLEScanCallback() { if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this); } @Override public void onScanResult(int callbackType, ScanResult result) { if (DEBUG) { Log.i(LOG_TAG, "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result + ")"); } final DeviceFilterPair deviceFilterPair = DeviceFilterPair.findMatch(result, mBLEFilters); if (deviceFilterPair == null) return; if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { onDeviceLost(deviceFilterPair); } else { onDeviceFound(deviceFilterPair); } } } private class BluetoothBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (DEBUG) { Log.i(LOG_TAG, "BL.onReceive(context = " + context + ", intent = " + intent + ")"); } final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); final DeviceFilterPair deviceFilterPair = DeviceFilterPair.findMatch(device, mBluetoothFilters); if (deviceFilterPair == null) return; if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) { onDeviceFound(deviceFilterPair); } else { onDeviceLost(deviceFilterPair); } } } private class WifiBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { List scanResults = mWifiManager.getScanResults(); if (DEBUG) { Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults)); } for (int i = 0; i < scanResults.size(); i++) { onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters)); } } } } }