Home | History | Annotate | Download | only in companiondevicemanager
      1 /*
      2  * Copyright (C) 2013 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.companiondevicemanager;
     18 
     19 import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayNameInternal;
     20 import static android.companion.BluetoothDeviceFilterUtils.getDeviceMacAddress;
     21 
     22 import static com.android.internal.util.ArrayUtils.isEmpty;
     23 import static com.android.internal.util.CollectionUtils.emptyIfNull;
     24 import static com.android.internal.util.CollectionUtils.size;
     25 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
     26 
     27 import android.annotation.NonNull;
     28 import android.annotation.Nullable;
     29 import android.app.PendingIntent;
     30 import android.app.Service;
     31 import android.bluetooth.BluetoothAdapter;
     32 import android.bluetooth.BluetoothDevice;
     33 import android.bluetooth.BluetoothManager;
     34 import android.bluetooth.le.BluetoothLeScanner;
     35 import android.bluetooth.le.ScanCallback;
     36 import android.bluetooth.le.ScanFilter;
     37 import android.bluetooth.le.ScanResult;
     38 import android.bluetooth.le.ScanSettings;
     39 import android.companion.AssociationRequest;
     40 import android.companion.BluetoothDeviceFilter;
     41 import android.companion.BluetoothLeDeviceFilter;
     42 import android.companion.DeviceFilter;
     43 import android.companion.ICompanionDeviceDiscoveryService;
     44 import android.companion.ICompanionDeviceDiscoveryServiceCallback;
     45 import android.companion.IFindDeviceCallback;
     46 import android.companion.WifiDeviceFilter;
     47 import android.content.BroadcastReceiver;
     48 import android.content.Context;
     49 import android.content.Intent;
     50 import android.content.IntentFilter;
     51 import android.graphics.Color;
     52 import android.graphics.drawable.Drawable;
     53 import android.net.wifi.WifiManager;
     54 import android.os.Handler;
     55 import android.os.IBinder;
     56 import android.os.Parcelable;
     57 import android.os.RemoteException;
     58 import android.text.TextUtils;
     59 import android.util.Log;
     60 import android.view.View;
     61 import android.view.ViewGroup;
     62 import android.widget.ArrayAdapter;
     63 import android.widget.TextView;
     64 
     65 import com.android.internal.util.ArrayUtils;
     66 import com.android.internal.util.CollectionUtils;
     67 import com.android.internal.util.Preconditions;
     68 
     69 import java.util.ArrayList;
     70 import java.util.List;
     71 import java.util.Objects;
     72 
     73 public class DeviceDiscoveryService extends Service {
     74 
     75     private static final boolean DEBUG = false;
     76     private static final String LOG_TAG = "DeviceDiscoveryService";
     77 
     78     private static final long SCAN_TIMEOUT = 20000;
     79 
     80     static DeviceDiscoveryService sInstance;
     81 
     82     private BluetoothAdapter mBluetoothAdapter;
     83     private WifiManager mWifiManager;
     84     @Nullable private BluetoothLeScanner mBLEScanner;
     85     private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().build();
     86 
     87     private List<DeviceFilter<?>> mFilters;
     88     private List<BluetoothLeDeviceFilter> mBLEFilters;
     89     private List<BluetoothDeviceFilter> mBluetoothFilters;
     90     private List<WifiDeviceFilter> mWifiFilters;
     91     private List<ScanFilter> mBLEScanFilters;
     92 
     93     AssociationRequest mRequest;
     94     List<DeviceFilterPair> mDevicesFound;
     95     DeviceFilterPair mSelectedDevice;
     96     DevicesAdapter mDevicesAdapter;
     97     IFindDeviceCallback mFindCallback;
     98 
     99     ICompanionDeviceDiscoveryServiceCallback mServiceCallback;
    100     boolean mIsScanning = false;
    101     @Nullable DeviceChooserActivity mActivity = null;
    102 
    103     private final ICompanionDeviceDiscoveryService mBinder =
    104             new ICompanionDeviceDiscoveryService.Stub() {
    105         @Override
    106         public void startDiscovery(AssociationRequest request,
    107                 String callingPackage,
    108                 IFindDeviceCallback findCallback,
    109                 ICompanionDeviceDiscoveryServiceCallback serviceCallback) {
    110             if (DEBUG) {
    111                 Log.i(LOG_TAG,
    112                         "startDiscovery() called with: filter = [" + request
    113                                 + "], findCallback = [" + findCallback + "]"
    114                                 + "], serviceCallback = [" + serviceCallback + "]");
    115             }
    116             mFindCallback = findCallback;
    117             mServiceCallback = serviceCallback;
    118             DeviceDiscoveryService.this.startDiscovery(request);
    119         }
    120     };
    121 
    122     private ScanCallback mBLEScanCallback;
    123     private BluetoothBroadcastReceiver mBluetoothBroadcastReceiver;
    124     private WifiBroadcastReceiver mWifiBroadcastReceiver;
    125 
    126     @Override
    127     public IBinder onBind(Intent intent) {
    128         if (DEBUG) Log.i(LOG_TAG, "onBind(" + intent + ")");
    129         return mBinder.asBinder();
    130     }
    131 
    132     @Override
    133     public void onCreate() {
    134         super.onCreate();
    135 
    136         if (DEBUG) Log.i(LOG_TAG, "onCreate()");
    137 
    138         mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter();
    139         mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
    140         mWifiManager = getSystemService(WifiManager.class);
    141 
    142         mDevicesFound = new ArrayList<>();
    143         mDevicesAdapter = new DevicesAdapter();
    144 
    145         sInstance = this;
    146     }
    147 
    148     private void startDiscovery(AssociationRequest request) {
    149         if (!request.equals(mRequest)) {
    150             mRequest = request;
    151 
    152             mFilters = request.getDeviceFilters();
    153             mWifiFilters = CollectionUtils.filter(mFilters, WifiDeviceFilter.class);
    154             mBluetoothFilters = CollectionUtils.filter(mFilters, BluetoothDeviceFilter.class);
    155             mBLEFilters = CollectionUtils.filter(mFilters, BluetoothLeDeviceFilter.class);
    156             mBLEScanFilters = CollectionUtils.map(mBLEFilters, BluetoothLeDeviceFilter::getScanFilter);
    157 
    158             reset();
    159         } else if (DEBUG) Log.i(LOG_TAG, "startDiscovery: duplicate request: " + request);
    160 
    161         if (!ArrayUtils.isEmpty(mDevicesFound)) {
    162             onReadyToShowUI();
    163         }
    164 
    165         // If filtering to get single device by mac address, also search in the set of already
    166         // bonded devices to allow linking those directly
    167         String singleMacAddressFilter = null;
    168         if (mRequest.isSingleDevice()) {
    169             int numFilters = size(mBluetoothFilters);
    170             for (int i = 0; i < numFilters; i++) {
    171                 BluetoothDeviceFilter filter = mBluetoothFilters.get(i);
    172                 if (!TextUtils.isEmpty(filter.getAddress())) {
    173                     singleMacAddressFilter = filter.getAddress();
    174                     break;
    175                 }
    176             }
    177         }
    178         if (singleMacAddressFilter != null) {
    179             for (BluetoothDevice dev : emptyIfNull(mBluetoothAdapter.getBondedDevices())) {
    180                 onDeviceFound(DeviceFilterPair.findMatch(dev, mBluetoothFilters));
    181             }
    182         }
    183 
    184         if (shouldScan(mBluetoothFilters)) {
    185             final IntentFilter intentFilter = new IntentFilter();
    186             intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
    187             intentFilter.addAction(BluetoothDevice.ACTION_DISAPPEARED);
    188 
    189             mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
    190             registerReceiver(mBluetoothBroadcastReceiver, intentFilter);
    191             mBluetoothAdapter.startDiscovery();
    192         }
    193 
    194         if (shouldScan(mBLEFilters) && mBLEScanner != null) {
    195             mBLEScanCallback = new BLEScanCallback();
    196             mBLEScanner.startScan(mBLEScanFilters, mDefaultScanSettings, mBLEScanCallback);
    197         }
    198 
    199         if (shouldScan(mWifiFilters)) {
    200             mWifiBroadcastReceiver = new WifiBroadcastReceiver();
    201             registerReceiver(mWifiBroadcastReceiver,
    202                     new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION));
    203             mWifiManager.startScan();
    204         }
    205         mIsScanning = true;
    206         Handler.getMain().sendMessageDelayed(
    207                 obtainMessage(DeviceDiscoveryService::stopScan, this),
    208                 SCAN_TIMEOUT);
    209     }
    210 
    211     private boolean shouldScan(List<? extends DeviceFilter> mediumSpecificFilters) {
    212         return !isEmpty(mediumSpecificFilters) || isEmpty(mFilters);
    213     }
    214 
    215     private void reset() {
    216         if (DEBUG) Log.i(LOG_TAG, "reset()");
    217         stopScan();
    218         mDevicesFound.clear();
    219         mSelectedDevice = null;
    220         notifyDataSetChanged();
    221     }
    222 
    223     @Override
    224     public boolean onUnbind(Intent intent) {
    225         stopScan();
    226         return super.onUnbind(intent);
    227     }
    228 
    229     private void stopScan() {
    230         if (DEBUG) Log.i(LOG_TAG, "stopScan()");
    231 
    232         if (!mIsScanning) return;
    233         mIsScanning = false;
    234 
    235         DeviceChooserActivity activity = mActivity;
    236         if (activity != null) {
    237             if (activity.mDeviceListView != null) {
    238                 activity.mDeviceListView.removeFooterView(activity.mLoadingIndicator);
    239             }
    240             mActivity = null;
    241         }
    242 
    243         mBluetoothAdapter.cancelDiscovery();
    244         if (mBluetoothBroadcastReceiver != null) {
    245             unregisterReceiver(mBluetoothBroadcastReceiver);
    246             mBluetoothBroadcastReceiver = null;
    247         }
    248         if (mBLEScanner != null) mBLEScanner.stopScan(mBLEScanCallback);
    249         if (mWifiBroadcastReceiver != null) {
    250             unregisterReceiver(mWifiBroadcastReceiver);
    251             mWifiBroadcastReceiver = null;
    252         }
    253     }
    254 
    255     private void onDeviceFound(@Nullable DeviceFilterPair device) {
    256         if (device == null) return;
    257 
    258         if (mDevicesFound.contains(device)) {
    259             return;
    260         }
    261 
    262         if (DEBUG) Log.i(LOG_TAG, "Found device " + device);
    263 
    264         if (mDevicesFound.isEmpty()) {
    265             onReadyToShowUI();
    266         }
    267         mDevicesFound.add(device);
    268         notifyDataSetChanged();
    269     }
    270 
    271     private void notifyDataSetChanged() {
    272         Handler.getMain().sendMessage(obtainMessage(
    273                 DevicesAdapter::notifyDataSetChanged, mDevicesAdapter));
    274     }
    275 
    276     //TODO also, on timeout -> call onFailure
    277     private void onReadyToShowUI() {
    278         try {
    279             mFindCallback.onSuccess(PendingIntent.getActivity(
    280                     this, 0,
    281                     new Intent(this, DeviceChooserActivity.class),
    282                     PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT
    283                             | PendingIntent.FLAG_IMMUTABLE));
    284         } catch (RemoteException e) {
    285             throw new RuntimeException(e);
    286         }
    287     }
    288 
    289     private void onDeviceLost(@Nullable DeviceFilterPair device) {
    290         mDevicesFound.remove(device);
    291         notifyDataSetChanged();
    292         if (DEBUG) Log.i(LOG_TAG, "Lost device " + device.getDisplayName());
    293     }
    294 
    295     void onDeviceSelected(String callingPackage, String deviceAddress) {
    296         try {
    297             mServiceCallback.onDeviceSelected(
    298                     //TODO is this the right userId?
    299                     callingPackage, getUserId(), deviceAddress);
    300         } catch (RemoteException e) {
    301             Log.e(LOG_TAG, "Failed to record association: "
    302                     + callingPackage + " <-> " + deviceAddress);
    303         }
    304     }
    305 
    306     void onCancel() {
    307         if (DEBUG) Log.i(LOG_TAG, "onCancel()");
    308         try {
    309             mServiceCallback.onDeviceSelectionCancel();
    310         } catch (RemoteException e) {
    311             throw new RuntimeException(e);
    312         }
    313     }
    314 
    315     class DevicesAdapter extends ArrayAdapter<DeviceFilterPair> {
    316         private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth);
    317         private Drawable WIFI_ICON = icon(com.android.internal.R.drawable.ic_wifi_signal_3);
    318 
    319         private Drawable icon(int drawableRes) {
    320             Drawable icon = getResources().getDrawable(drawableRes, null);
    321             icon.setTint(Color.DKGRAY);
    322             return icon;
    323         }
    324 
    325         public DevicesAdapter() {
    326             super(DeviceDiscoveryService.this, 0, mDevicesFound);
    327         }
    328 
    329         @Override
    330         public View getView(
    331                 int position,
    332                 @Nullable View convertView,
    333                 @NonNull ViewGroup parent) {
    334             TextView view = convertView instanceof TextView
    335                     ? (TextView) convertView
    336                     : newView();
    337             bind(view, getItem(position));
    338             return view;
    339         }
    340 
    341         private void bind(TextView textView, DeviceFilterPair device) {
    342             textView.setText(device.getDisplayName());
    343             textView.setBackgroundColor(
    344                     device.equals(mSelectedDevice)
    345                             ? Color.GRAY
    346                             : Color.TRANSPARENT);
    347             textView.setCompoundDrawablesWithIntrinsicBounds(
    348                     device.device instanceof android.net.wifi.ScanResult
    349                         ? WIFI_ICON
    350                         : BLUETOOTH_ICON,
    351                     null, null, null);
    352             textView.setOnClickListener((view) -> {
    353                 mSelectedDevice = device;
    354                 notifyDataSetChanged();
    355             });
    356         }
    357 
    358         //TODO move to a layout file
    359         private TextView newView() {
    360             final TextView textView = new TextView(DeviceDiscoveryService.this);
    361             textView.setTextColor(Color.BLACK);
    362             final int padding = DeviceChooserActivity.getPadding(getResources());
    363             textView.setPadding(padding, padding, padding, padding);
    364             textView.setCompoundDrawablePadding(padding);
    365             return textView;
    366         }
    367     }
    368 
    369     /**
    370      * A pair of device and a filter that matched this device if any.
    371      *
    372      * @param <T> device type
    373      */
    374     static class DeviceFilterPair<T extends Parcelable> {
    375         public final T device;
    376         @Nullable
    377         public final DeviceFilter<T> filter;
    378 
    379         private DeviceFilterPair(T device, @Nullable DeviceFilter<T> filter) {
    380             this.device = device;
    381             this.filter = filter;
    382         }
    383 
    384         /**
    385          * {@code (device, null)} if the filters list is empty or null
    386          * {@code null} if none of the provided filters match the device
    387          * {@code (device, filter)} where filter is among the list of filters and matches the device
    388          */
    389         @Nullable
    390         public static <T extends Parcelable> DeviceFilterPair<T> findMatch(
    391                 T dev, @Nullable List<? extends DeviceFilter<T>> filters) {
    392             if (isEmpty(filters)) return new DeviceFilterPair<>(dev, null);
    393             final DeviceFilter<T> matchingFilter
    394                     = CollectionUtils.find(filters, f -> f.matches(dev));
    395 
    396             DeviceFilterPair<T> result = matchingFilter != null
    397                     ? new DeviceFilterPair<>(dev, matchingFilter)
    398                     : null;
    399             if (DEBUG) Log.i(LOG_TAG, "findMatch(dev = " + dev + ", filters = " + filters +
    400                     ") -> " + result);
    401             return result;
    402         }
    403 
    404         public String getDisplayName() {
    405             if (filter == null) {
    406                 Preconditions.checkNotNull(device);
    407                 if (device instanceof BluetoothDevice) {
    408                     return getDeviceDisplayNameInternal((BluetoothDevice) device);
    409                 } else if (device instanceof android.net.wifi.ScanResult) {
    410                     return getDeviceDisplayNameInternal((android.net.wifi.ScanResult) device);
    411                 } else if (device instanceof ScanResult) {
    412                     return getDeviceDisplayNameInternal(((ScanResult) device).getDevice());
    413                 } else {
    414                     throw new IllegalArgumentException("Unknown device type: " + device.getClass());
    415                 }
    416             }
    417             return filter.getDeviceDisplayName(device);
    418         }
    419 
    420         @Override
    421         public boolean equals(Object o) {
    422             if (this == o) return true;
    423             if (o == null || getClass() != o.getClass()) return false;
    424             DeviceFilterPair<?> that = (DeviceFilterPair<?>) o;
    425             return Objects.equals(getDeviceMacAddress(device), getDeviceMacAddress(that.device));
    426         }
    427 
    428         @Override
    429         public int hashCode() {
    430             return Objects.hash(getDeviceMacAddress(device));
    431         }
    432 
    433         @Override
    434         public String toString() {
    435             return "DeviceFilterPair{" +
    436                     "device=" + device +
    437                     ", filter=" + filter +
    438                     '}';
    439         }
    440     }
    441 
    442     private class BLEScanCallback extends ScanCallback {
    443 
    444         public BLEScanCallback() {
    445             if (DEBUG) Log.i(LOG_TAG, "new BLEScanCallback() -> " + this);
    446         }
    447 
    448         @Override
    449         public void onScanResult(int callbackType, ScanResult result) {
    450             if (DEBUG) {
    451                 Log.i(LOG_TAG,
    452                         "BLE.onScanResult(callbackType = " + callbackType + ", result = " + result
    453                                 + ")");
    454             }
    455             final DeviceFilterPair<ScanResult> deviceFilterPair
    456                     = DeviceFilterPair.findMatch(result, mBLEFilters);
    457             if (deviceFilterPair == null) return;
    458             if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
    459                 onDeviceLost(deviceFilterPair);
    460             } else {
    461                 onDeviceFound(deviceFilterPair);
    462             }
    463         }
    464     }
    465 
    466     private class BluetoothBroadcastReceiver extends BroadcastReceiver {
    467         @Override
    468         public void onReceive(Context context, Intent intent) {
    469             if (DEBUG) {
    470                 Log.i(LOG_TAG,
    471                         "BL.onReceive(context = " + context + ", intent = " + intent + ")");
    472             }
    473             final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
    474             final DeviceFilterPair<BluetoothDevice> deviceFilterPair
    475                     = DeviceFilterPair.findMatch(device, mBluetoothFilters);
    476             if (deviceFilterPair == null) return;
    477             if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) {
    478                 onDeviceFound(deviceFilterPair);
    479             } else {
    480                 onDeviceLost(deviceFilterPair);
    481             }
    482         }
    483     }
    484 
    485     private class WifiBroadcastReceiver extends BroadcastReceiver {
    486         @Override
    487         public void onReceive(Context context, Intent intent) {
    488             if (intent.getAction().equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) {
    489                 List<android.net.wifi.ScanResult> scanResults = mWifiManager.getScanResults();
    490 
    491                 if (DEBUG) {
    492                     Log.i(LOG_TAG, "Wifi scan results: " + TextUtils.join("\n", scanResults));
    493                 }
    494 
    495                 for (int i = 0; i < scanResults.size(); i++) {
    496                     onDeviceFound(DeviceFilterPair.findMatch(scanResults.get(i), mWifiFilters));
    497                 }
    498             }
    499         }
    500     }
    501 }
    502