DeviceDiscoveryService.java revision da0acdf938f1d6e7a978e143d5d80d8dd5af52ad
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 17package com.android.companiondevicemanager; 18 19import static android.companion.BluetoothDeviceFilterUtils.getDeviceDisplayName; 20import static android.companion.BluetoothLEDeviceFilter.nullsafe; 21 22import android.annotation.NonNull; 23import android.annotation.Nullable; 24import android.app.PendingIntent; 25import android.app.Service; 26import android.bluetooth.BluetoothAdapter; 27import android.bluetooth.BluetoothDevice; 28import android.bluetooth.BluetoothManager; 29import android.bluetooth.le.BluetoothLeScanner; 30import android.bluetooth.le.ScanCallback; 31import android.bluetooth.le.ScanFilter; 32import android.bluetooth.le.ScanResult; 33import android.bluetooth.le.ScanSettings; 34import android.companion.AssociationRequest; 35import android.companion.BluetoothLEDeviceFilter; 36import android.companion.ICompanionDeviceDiscoveryService; 37import android.companion.ICompanionDeviceDiscoveryServiceCallback; 38import android.companion.IFindDeviceCallback; 39import android.content.BroadcastReceiver; 40import android.content.Context; 41import android.content.Intent; 42import android.content.IntentFilter; 43import android.graphics.Color; 44import android.graphics.drawable.Drawable; 45import android.os.IBinder; 46import android.os.RemoteException; 47import android.util.Log; 48import android.view.View; 49import android.view.ViewGroup; 50import android.widget.ArrayAdapter; 51import android.widget.TextView; 52 53import java.util.ArrayList; 54import java.util.Collections; 55import java.util.List; 56 57public class DeviceDiscoveryService extends Service { 58 59 private static final boolean DEBUG = false; 60 private static final String LOG_TAG = "DeviceDiscoveryService"; 61 62 static DeviceDiscoveryService sInstance; 63 64 private BluetoothAdapter mBluetoothAdapter; 65 private BluetoothLEDeviceFilter mFilter; 66 private ScanFilter mScanFilter; 67 private ScanSettings mDefaultScanSettings = new ScanSettings.Builder().build(); 68 AssociationRequest<?> mRequest; 69 List<BluetoothDevice> mDevicesFound; 70 BluetoothDevice mSelectedDevice; 71 DevicesAdapter mDevicesAdapter; 72 IFindDeviceCallback mFindCallback; 73 ICompanionDeviceDiscoveryServiceCallback mServiceCallback; 74 String mCallingPackage; 75 76 private final ICompanionDeviceDiscoveryService mBinder = 77 new ICompanionDeviceDiscoveryService.Stub() { 78 @Override 79 public void startDiscovery(AssociationRequest request, 80 String callingPackage, 81 IFindDeviceCallback findCallback, 82 ICompanionDeviceDiscoveryServiceCallback serviceCallback) { 83 if (DEBUG) { 84 Log.i(LOG_TAG, 85 "startDiscovery() called with: filter = [" + request 86 + "], findCallback = [" + findCallback + "]" 87 + "], serviceCallback = [" + serviceCallback + "]"); 88 } 89 mFindCallback = findCallback; 90 mServiceCallback = serviceCallback; 91 mCallingPackage = callingPackage; 92 DeviceDiscoveryService.this.startDiscovery(request); 93 } 94 }; 95 96 private final ScanCallback mBLEScanCallback = new ScanCallback() { 97 @Override 98 public void onScanResult(int callbackType, ScanResult result) { 99 final BluetoothDevice device = result.getDevice(); 100 if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) { 101 onDeviceLost(device); 102 } else { 103 onDeviceFound(device); 104 } 105 } 106 }; 107 108 private BluetoothLeScanner mBLEScanner; 109 110 private BroadcastReceiver mBluetoothDeviceFoundBroadcastReceiver = new BroadcastReceiver() { 111 @Override 112 public void onReceive(Context context, Intent intent) { 113 final BluetoothDevice device = intent.getParcelableExtra( 114 BluetoothDevice.EXTRA_DEVICE); 115 if (!mFilter.matches(device)) return; // ignore device 116 117 if (intent.getAction().equals(BluetoothDevice.ACTION_FOUND)) { 118 onDeviceFound(device); 119 } else { 120 onDeviceLost(device); 121 } 122 } 123 }; 124 125 @Override 126 public IBinder onBind(Intent intent) { 127 if (DEBUG) Log.i(LOG_TAG, "onBind(" + intent + ")"); 128 return mBinder.asBinder(); 129 } 130 131 @Override 132 public void onCreate() { 133 super.onCreate(); 134 135 if (DEBUG) Log.i(LOG_TAG, "onCreate()"); 136 137 mBluetoothAdapter = getSystemService(BluetoothManager.class).getAdapter(); 138 mBLEScanner = mBluetoothAdapter.getBluetoothLeScanner(); 139 140 mDevicesFound = new ArrayList<>(); 141 mDevicesAdapter = new DevicesAdapter(); 142 143 sInstance = this; 144 } 145 146 private void startDiscovery(AssociationRequest<?> request) { 147 //TODO support other protocols as well 148 mRequest = request; 149 mFilter = nullsafe((BluetoothLEDeviceFilter) request.getDeviceFilter()); 150 mScanFilter = mFilter.getScanFilter(); 151 152 reset(); 153 154 final IntentFilter intentFilter = new IntentFilter(); 155 intentFilter.addAction(BluetoothDevice.ACTION_FOUND); 156 intentFilter.addAction(BluetoothDevice.ACTION_DISAPPEARED); 157 158 registerReceiver(mBluetoothDeviceFoundBroadcastReceiver, intentFilter); 159 mBluetoothAdapter.startDiscovery(); 160 161 mBLEScanner.startScan( 162 Collections.singletonList(mScanFilter), mDefaultScanSettings, mBLEScanCallback); 163 } 164 165 private void reset() { 166 mDevicesFound.clear(); 167 mSelectedDevice = null; 168 mDevicesAdapter.notifyDataSetChanged(); 169 } 170 171 @Override 172 public boolean onUnbind(Intent intent) { 173 stopScan(); 174 return super.onUnbind(intent); 175 } 176 177 public void onDeviceSelected() { 178 try { 179 mServiceCallback.onDeviceSelected(mCallingPackage, getUserId()); 180 } catch (RemoteException e) { 181 Log.e(LOG_TAG, "Error reporting selected device"); 182 } 183 } 184 185 private void stopScan() { 186 if (DEBUG) Log.i(LOG_TAG, "stopScan() called"); 187 mBluetoothAdapter.cancelDiscovery(); 188 mBLEScanner.stopScan(mBLEScanCallback); 189 unregisterReceiver(mBluetoothDeviceFoundBroadcastReceiver); 190 stopSelf(); 191 } 192 193 private void onDeviceFound(BluetoothDevice device) { 194 if (mDevicesFound.contains(device)) { 195 return; 196 } 197 198 if (DEBUG) { 199 Log.i(LOG_TAG, "Considering device " + getDeviceDisplayName(device)); 200 } 201 202 if (!mFilter.matches(device)) { 203 return; 204 } 205 206 if (DEBUG) { 207 Log.i(LOG_TAG, "Found device " + getDeviceDisplayName(device)); 208 } 209 if (mDevicesFound.isEmpty()) { 210 onReadyToShowUI(); 211 } 212 mDevicesFound.add(device); 213 mDevicesAdapter.notifyDataSetChanged(); 214 } 215 216 //TODO also, on timeout -> call onFailure 217 private void onReadyToShowUI() { 218 try { 219 mFindCallback.onSuccess(PendingIntent.getActivity( 220 this, 0, 221 new Intent(this, DeviceChooserActivity.class), 222 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_CANCEL_CURRENT 223 | PendingIntent.FLAG_IMMUTABLE)); 224 } catch (RemoteException e) { 225 throw new RuntimeException(e); 226 } 227 } 228 229 private void onDeviceLost(BluetoothDevice device) { 230 mDevicesFound.remove(device); 231 mDevicesAdapter.notifyDataSetChanged(); 232 if (DEBUG) { 233 Log.i(LOG_TAG, "Lost device " + getDeviceDisplayName(device)); 234 } 235 } 236 237 class DevicesAdapter extends ArrayAdapter<BluetoothDevice> { 238 private Drawable BLUETOOTH_ICON = icon(android.R.drawable.stat_sys_data_bluetooth); 239 240 private Drawable icon(int drawableRes) { 241 Drawable icon = getResources().getDrawable(drawableRes, null); 242 icon.setTint(Color.DKGRAY); 243 return icon; 244 } 245 246 public DevicesAdapter() { 247 super(DeviceDiscoveryService.this, 0, mDevicesFound); 248 } 249 250 @Override 251 public View getView( 252 int position, 253 @Nullable View convertView, 254 @NonNull ViewGroup parent) { 255 TextView view = convertView instanceof TextView 256 ? (TextView) convertView 257 : newView(); 258 bind(view, getItem(position)); 259 return view; 260 } 261 262 private void bind(TextView textView, BluetoothDevice device) { 263 textView.setText(getDeviceDisplayName(device)); 264 textView.setBackgroundColor( 265 device.equals(mSelectedDevice) 266 ? Color.GRAY 267 : Color.TRANSPARENT); 268 textView.setOnClickListener((view) -> { 269 mSelectedDevice = device; 270 notifyDataSetChanged(); 271 }); 272 } 273 274 //TODO move to a layout file 275 private TextView newView() { 276 final TextView textView = new TextView(DeviceDiscoveryService.this); 277 textView.setTextColor(Color.BLACK); 278 final int padding = DeviceChooserActivity.getPadding(getResources()); 279 textView.setPadding(padding, padding, padding, padding); 280 textView.setCompoundDrawablesWithIntrinsicBounds( 281 BLUETOOTH_ICON, null, null, null); 282 textView.setCompoundDrawablePadding(padding); 283 return textView; 284 } 285 } 286} 287