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