1 /* 2 * Copyright 2021 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 android.nearby; 18 19 import android.Manifest; 20 import android.annotation.CallbackExecutor; 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.RequiresPermission; 25 import android.annotation.SuppressLint; 26 import android.annotation.SystemApi; 27 import android.annotation.SystemService; 28 import android.content.Context; 29 import android.os.RemoteException; 30 import android.provider.Settings; 31 import android.util.Log; 32 33 import com.android.internal.annotations.GuardedBy; 34 import com.android.internal.util.Preconditions; 35 36 import java.lang.ref.WeakReference; 37 import java.util.Objects; 38 import java.util.WeakHashMap; 39 import java.util.concurrent.Executor; 40 41 /** 42 * This class provides a way to perform Nearby related operations such as scanning, broadcasting 43 * and connecting to nearby devices. 44 * 45 * <p> To get a {@link NearbyManager} instance, call the 46 * <code>Context.getSystemService(NearbyManager.class)</code>. 47 * 48 * @hide 49 */ 50 @SystemApi 51 @SystemService(Context.NEARBY_SERVICE) 52 public class NearbyManager { 53 54 /** 55 * Represents the scanning state. 56 * 57 * @hide 58 */ 59 @IntDef({ 60 ScanStatus.UNKNOWN, 61 ScanStatus.SUCCESS, 62 ScanStatus.ERROR, 63 }) 64 public @interface ScanStatus { 65 // Default, invalid state. 66 int UNKNOWN = 0; 67 // The successful state. 68 int SUCCESS = 1; 69 // Failed state. 70 int ERROR = 2; 71 } 72 73 private static final String TAG = "NearbyManager"; 74 75 /** 76 * Whether allows Fast Pair to scan. 77 * 78 * (0 = disabled, 1 = enabled) 79 * 80 * @hide 81 */ 82 public static final String FAST_PAIR_SCAN_ENABLED = "fast_pair_scan_enabled"; 83 84 @GuardedBy("sScanListeners") 85 private static final WeakHashMap<ScanCallback, WeakReference<ScanListenerTransport>> 86 sScanListeners = new WeakHashMap<>(); 87 @GuardedBy("sBroadcastListeners") 88 private static final WeakHashMap<BroadcastCallback, WeakReference<BroadcastListenerTransport>> 89 sBroadcastListeners = new WeakHashMap<>(); 90 91 private final Context mContext; 92 private final INearbyManager mService; 93 94 /** 95 * Creates a new NearbyManager. 96 * 97 * @param service the service object 98 */ NearbyManager(@onNull Context context, @NonNull INearbyManager service)99 NearbyManager(@NonNull Context context, @NonNull INearbyManager service) { 100 Objects.requireNonNull(context); 101 Objects.requireNonNull(service); 102 mContext = context; 103 mService = service; 104 } 105 toClientNearbyDevice( NearbyDeviceParcelable nearbyDeviceParcelable, @ScanRequest.ScanType int scanType)106 private static NearbyDevice toClientNearbyDevice( 107 NearbyDeviceParcelable nearbyDeviceParcelable, 108 @ScanRequest.ScanType int scanType) { 109 if (scanType == ScanRequest.SCAN_TYPE_FAST_PAIR) { 110 return new FastPairDevice.Builder() 111 .setName(nearbyDeviceParcelable.getName()) 112 .addMedium(nearbyDeviceParcelable.getMedium()) 113 .setRssi(nearbyDeviceParcelable.getRssi()) 114 .setTxPower(nearbyDeviceParcelable.getTxPower()) 115 .setModelId(nearbyDeviceParcelable.getFastPairModelId()) 116 .setBluetoothAddress(nearbyDeviceParcelable.getBluetoothAddress()) 117 .setData(nearbyDeviceParcelable.getData()).build(); 118 } 119 120 if (scanType == ScanRequest.SCAN_TYPE_NEARBY_PRESENCE) { 121 PublicCredential publicCredential = nearbyDeviceParcelable.getPublicCredential(); 122 if (publicCredential == null) { 123 return null; 124 } 125 byte[] salt = nearbyDeviceParcelable.getSalt(); 126 if (salt == null) { 127 salt = new byte[0]; 128 } 129 return new PresenceDevice.Builder( 130 // Use the public credential hash as the device Id. 131 String.valueOf(publicCredential.hashCode()), 132 salt, 133 publicCredential.getSecretId(), 134 publicCredential.getEncryptedMetadata()) 135 .setRssi(nearbyDeviceParcelable.getRssi()) 136 .addMedium(nearbyDeviceParcelable.getMedium()) 137 .build(); 138 } 139 return null; 140 } 141 142 /** 143 * Start scan for nearby devices with given parameters. Devices matching {@link ScanRequest} 144 * will be delivered through the given callback. 145 * 146 * @param scanRequest various parameters clients send when requesting scanning 147 * @param executor executor where the listener method is called 148 * @param scanCallback the callback to notify clients when there is a scan result 149 * 150 * @return whether scanning was successfully started 151 */ 152 @RequiresPermission(allOf = {android.Manifest.permission.BLUETOOTH_SCAN, 153 android.Manifest.permission.BLUETOOTH_PRIVILEGED}) 154 @ScanStatus startScan(@onNull ScanRequest scanRequest, @CallbackExecutor @NonNull Executor executor, @NonNull ScanCallback scanCallback)155 public int startScan(@NonNull ScanRequest scanRequest, 156 @CallbackExecutor @NonNull Executor executor, 157 @NonNull ScanCallback scanCallback) { 158 Objects.requireNonNull(scanRequest, "scanRequest must not be null"); 159 Objects.requireNonNull(scanCallback, "scanCallback must not be null"); 160 Objects.requireNonNull(executor, "executor must not be null"); 161 162 try { 163 synchronized (sScanListeners) { 164 WeakReference<ScanListenerTransport> reference = sScanListeners.get(scanCallback); 165 ScanListenerTransport transport = reference != null ? reference.get() : null; 166 if (transport == null) { 167 transport = new ScanListenerTransport(scanRequest.getScanType(), scanCallback, 168 executor); 169 } else { 170 Preconditions.checkState(transport.isRegistered()); 171 transport.setExecutor(executor); 172 } 173 @ScanStatus int status = mService.registerScanListener(scanRequest, transport, 174 mContext.getPackageName(), mContext.getAttributionTag()); 175 if (status != ScanStatus.SUCCESS) { 176 return status; 177 } 178 sScanListeners.put(scanCallback, new WeakReference<>(transport)); 179 return ScanStatus.SUCCESS; 180 } 181 } catch (RemoteException e) { 182 throw e.rethrowFromSystemServer(); 183 } 184 } 185 186 /** 187 * Stops the nearby device scan for the specified callback. The given callback 188 * is guaranteed not to receive any invocations that happen after this method 189 * is invoked. 190 * 191 * Suppressed lint: Registration methods should have overload that accepts delivery Executor. 192 * Already have executor in startScan() method. 193 * 194 * @param scanCallback the callback that was used to start the scan 195 */ 196 @SuppressLint("ExecutorRegistration") 197 @RequiresPermission(allOf = {android.Manifest.permission.BLUETOOTH_SCAN, 198 android.Manifest.permission.BLUETOOTH_PRIVILEGED}) stopScan(@onNull ScanCallback scanCallback)199 public void stopScan(@NonNull ScanCallback scanCallback) { 200 Preconditions.checkArgument(scanCallback != null, 201 "invalid null scanCallback"); 202 try { 203 synchronized (sScanListeners) { 204 WeakReference<ScanListenerTransport> reference = sScanListeners.remove( 205 scanCallback); 206 ScanListenerTransport transport = reference != null ? reference.get() : null; 207 if (transport != null) { 208 transport.unregister(); 209 mService.unregisterScanListener(transport, mContext.getPackageName(), 210 mContext.getAttributionTag()); 211 } else { 212 Log.e(TAG, "Cannot stop scan with this callback " 213 + "because it is never registered."); 214 } 215 } 216 } catch (RemoteException e) { 217 throw e.rethrowFromSystemServer(); 218 } 219 } 220 221 /** 222 * Start broadcasting the request using nearby specification. 223 * 224 * @param broadcastRequest request for the nearby broadcast 225 * @param executor executor for running the callback 226 * @param callback callback for notifying the client 227 */ 228 @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADVERTISE, 229 android.Manifest.permission.BLUETOOTH_PRIVILEGED}) startBroadcast(@onNull BroadcastRequest broadcastRequest, @CallbackExecutor @NonNull Executor executor, @NonNull BroadcastCallback callback)230 public void startBroadcast(@NonNull BroadcastRequest broadcastRequest, 231 @CallbackExecutor @NonNull Executor executor, @NonNull BroadcastCallback callback) { 232 try { 233 synchronized (sBroadcastListeners) { 234 WeakReference<BroadcastListenerTransport> reference = sBroadcastListeners.get( 235 callback); 236 BroadcastListenerTransport transport = reference != null ? reference.get() : null; 237 if (transport == null) { 238 transport = new BroadcastListenerTransport(callback, executor); 239 } else { 240 Preconditions.checkState(transport.isRegistered()); 241 transport.setExecutor(executor); 242 } 243 mService.startBroadcast(new BroadcastRequestParcelable(broadcastRequest), transport, 244 mContext.getPackageName(), mContext.getAttributionTag()); 245 sBroadcastListeners.put(callback, new WeakReference<>(transport)); 246 } 247 } catch (RemoteException e) { 248 throw e.rethrowFromSystemServer(); 249 } 250 } 251 252 /** 253 * Stop the broadcast associated with the given callback. 254 * 255 * @param callback the callback that was used for starting the broadcast 256 */ 257 @SuppressLint("ExecutorRegistration") 258 @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADVERTISE, 259 android.Manifest.permission.BLUETOOTH_PRIVILEGED}) stopBroadcast(@onNull BroadcastCallback callback)260 public void stopBroadcast(@NonNull BroadcastCallback callback) { 261 try { 262 synchronized (sBroadcastListeners) { 263 WeakReference<BroadcastListenerTransport> reference = sBroadcastListeners.remove( 264 callback); 265 BroadcastListenerTransport transport = reference != null ? reference.get() : null; 266 if (transport != null) { 267 transport.unregister(); 268 mService.stopBroadcast(transport, mContext.getPackageName(), 269 mContext.getAttributionTag()); 270 } else { 271 Log.e(TAG, "Cannot stop broadcast with this callback " 272 + "because it is never registered."); 273 } 274 } 275 } catch (RemoteException e) { 276 throw e.rethrowFromSystemServer(); 277 } 278 } 279 280 /** 281 * Read from {@link Settings} whether Fast Pair scan is enabled. 282 * 283 * @param context the {@link Context} to query the setting 284 * @return whether the Fast Pair is enabled 285 * @hide 286 */ getFastPairScanEnabled(@onNull Context context)287 public static boolean getFastPairScanEnabled(@NonNull Context context) { 288 final int enabled = Settings.Secure.getInt( 289 context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, 0); 290 return enabled != 0; 291 } 292 293 /** 294 * Write into {@link Settings} whether Fast Pair scan is enabled 295 * 296 * @param context the {@link Context} to set the setting 297 * @param enable whether the Fast Pair scan should be enabled 298 * @hide 299 */ 300 @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) setFastPairScanEnabled(@onNull Context context, boolean enable)301 public static void setFastPairScanEnabled(@NonNull Context context, boolean enable) { 302 Settings.Secure.putInt( 303 context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, enable ? 1 : 0); 304 } 305 306 private static class ScanListenerTransport extends IScanListener.Stub { 307 308 private @ScanRequest.ScanType int mScanType; 309 private volatile @Nullable ScanCallback mScanCallback; 310 private Executor mExecutor; 311 ScanListenerTransport(@canRequest.ScanType int scanType, ScanCallback scanCallback, @CallbackExecutor Executor executor)312 ScanListenerTransport(@ScanRequest.ScanType int scanType, ScanCallback scanCallback, 313 @CallbackExecutor Executor executor) { 314 Preconditions.checkArgument(scanCallback != null, 315 "invalid null callback"); 316 Preconditions.checkState(ScanRequest.isValidScanType(scanType), 317 "invalid scan type : " + scanType 318 + ", scan type must be one of ScanRequest#SCAN_TYPE_"); 319 mScanType = scanType; 320 mScanCallback = scanCallback; 321 mExecutor = executor; 322 } 323 setExecutor(Executor executor)324 void setExecutor(Executor executor) { 325 Preconditions.checkArgument( 326 executor != null, "invalid null executor"); 327 mExecutor = executor; 328 } 329 isRegistered()330 boolean isRegistered() { 331 return mScanCallback != null; 332 } 333 unregister()334 void unregister() { 335 mScanCallback = null; 336 } 337 338 @Override onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable)339 public void onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable) 340 throws RemoteException { 341 mExecutor.execute(() -> { 342 if (mScanCallback != null) { 343 mScanCallback.onDiscovered( 344 toClientNearbyDevice(nearbyDeviceParcelable, mScanType)); 345 } 346 }); 347 } 348 349 @Override onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable)350 public void onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable) 351 throws RemoteException { 352 mExecutor.execute(() -> { 353 if (mScanCallback != null) { 354 mScanCallback.onUpdated( 355 toClientNearbyDevice(nearbyDeviceParcelable, mScanType)); 356 } 357 }); 358 } 359 360 @Override onLost(NearbyDeviceParcelable nearbyDeviceParcelable)361 public void onLost(NearbyDeviceParcelable nearbyDeviceParcelable) throws RemoteException { 362 mExecutor.execute(() -> { 363 if (mScanCallback != null) { 364 mScanCallback.onLost( 365 toClientNearbyDevice(nearbyDeviceParcelable, mScanType)); 366 } 367 }); 368 } 369 370 @Override onError()371 public void onError() { 372 mExecutor.execute(() -> { 373 if (mScanCallback != null) { 374 Log.e("NearbyManager", "onError: There is an error in scan."); 375 } 376 }); 377 } 378 } 379 380 private static class BroadcastListenerTransport extends IBroadcastListener.Stub { 381 private volatile @Nullable BroadcastCallback mBroadcastCallback; 382 private Executor mExecutor; 383 BroadcastListenerTransport(BroadcastCallback broadcastCallback, @CallbackExecutor Executor executor)384 BroadcastListenerTransport(BroadcastCallback broadcastCallback, 385 @CallbackExecutor Executor executor) { 386 mBroadcastCallback = broadcastCallback; 387 mExecutor = executor; 388 } 389 setExecutor(Executor executor)390 void setExecutor(Executor executor) { 391 Preconditions.checkArgument( 392 executor != null, "invalid null executor"); 393 mExecutor = executor; 394 } 395 isRegistered()396 boolean isRegistered() { 397 return mBroadcastCallback != null; 398 } 399 unregister()400 void unregister() { 401 mBroadcastCallback = null; 402 } 403 404 @Override onStatusChanged(int status)405 public void onStatusChanged(int status) { 406 mExecutor.execute(() -> { 407 if (mBroadcastCallback != null) { 408 mBroadcastCallback.onStatusChanged(status); 409 } 410 }); 411 } 412 } 413 } 414