blob: 1e262314284dc30bd4f9732711fb1e69d0d3c672 [file] [log] [blame]
/*
* 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 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.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";
static DeviceDiscoveryService sInstance;
private BluetoothAdapter mBluetoothAdapter;
private WifiManager mWifiManager;
@Nullable private BluetoothLeScanner mBLEScanner;
private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().build();
private List<DeviceFilter<?>> mFilters;
private List<BluetoothLeDeviceFilter> mBLEFilters;
private List<BluetoothDeviceFilter> mBluetoothFilters;
private List<WifiDeviceFilter> mWifiFilters;
private List<ScanFilter> mBLEScanFilters;
AssociationRequest mRequest;
List<DeviceFilterPair> mDevicesFound;
DeviceFilterPair mSelectedDevice;
DevicesAdapter mDevicesAdapter;
IFindDeviceCallback mFindCallback;
ICompanionDeviceDiscoveryServiceCallback mServiceCallback;
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);
intentFilter.addAction(BluetoothDevice.ACTION_DISAPPEARED);
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();
}
}
private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) {
return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters);
}
private void reset() {
if (DEBUG) Log.i(LOG_TAG, "reset()");
stopScan();
mDevicesFound.clear();
mSelectedDevice = null;
mDevicesAdapter.notifyDataSetChanged();
}
@Override
public boolean onUnbind(Intent intent) {
stopScan();
return super.onUnbind(intent);
}
private void stopScan() {
if (DEBUG) Log.i(LOG_TAG, "stopScan()");
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);
mDevicesAdapter.notifyDataSetChanged();
}
//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);
mDevicesAdapter.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<DeviceFilterPair> {
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 <T> device type
*/
static class DeviceFilterPair<T extends Parcelable> {
public final T device;
@Nullable
public final DeviceFilter<T> filter;
private DeviceFilterPair(T device, @Nullable DeviceFilter<T> 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 <T extends Parcelable> DeviceFilterPair<T> findMatch(
T dev, @Nullable List<? extends DeviceFilter<T>> filters) {
if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null);
final DeviceFilter<T> matchingFilter
= CollectionUtils.find(filters, f -> f.matches(dev));
DeviceFilterPair<T> 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<ScanResult> 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<BluetoothDevice> 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<android.net.wifi.ScanResult> 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));
}
}
}
}
}