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.FlaggedApi; 22 import android.annotation.IntDef; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.annotation.RequiresPermission; 26 import android.annotation.SuppressLint; 27 import android.annotation.SystemApi; 28 import android.annotation.SystemService; 29 import android.bluetooth.BluetoothManager; 30 import android.content.Context; 31 import android.location.LocationManager; 32 import android.nearby.aidl.IOffloadCallback; 33 import android.os.RemoteException; 34 import android.os.SystemProperties; 35 import android.provider.Settings; 36 import android.util.Log; 37 38 import com.android.internal.annotations.GuardedBy; 39 import com.android.internal.util.Preconditions; 40 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 import java.lang.ref.WeakReference; 44 import java.util.List; 45 import java.util.Objects; 46 import java.util.WeakHashMap; 47 import java.util.concurrent.Executor; 48 import java.util.function.Consumer; 49 50 /** 51 * This class provides a way to perform Nearby related operations such as scanning, broadcasting 52 * and connecting to nearby devices. 53 * 54 * <p> To get a {@link NearbyManager} instance, call the 55 * <code>Context.getSystemService(NearbyManager.class)</code>. 56 * 57 * @hide 58 */ 59 @SystemApi 60 @SystemService(Context.NEARBY_SERVICE) 61 public class NearbyManager { 62 63 /** 64 * Represents the scanning state. 65 * 66 * @hide 67 */ 68 @IntDef({ 69 ScanStatus.UNKNOWN, 70 ScanStatus.SUCCESS, 71 ScanStatus.ERROR, 72 }) 73 @Retention(RetentionPolicy.SOURCE) 74 public @interface ScanStatus { 75 // The undetermined status, some modules may be initializing. Retry is suggested. 76 int UNKNOWN = 0; 77 // The successful state. 78 int SUCCESS = 1; 79 // Failed state. 80 int ERROR = 2; 81 } 82 83 /** 84 * Return value of {@link #getPoweredOffFindingMode()} when this powered off finding is not 85 * supported the device. 86 */ 87 @FlaggedApi("com.android.nearby.flags.powered_off_finding") 88 public static final int POWERED_OFF_FINDING_MODE_UNSUPPORTED = 0; 89 90 /** 91 * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link 92 * #setPoweredOffFindingMode(int)} when powered off finding is supported but disabled. The 93 * device will not start to advertise when powered off. 94 */ 95 @FlaggedApi("com.android.nearby.flags.powered_off_finding") 96 public static final int POWERED_OFF_FINDING_MODE_DISABLED = 1; 97 98 /** 99 * Return value of {@link #getPoweredOffFindingMode()} and argument of {@link 100 * #setPoweredOffFindingMode(int)} when powered off finding is enabled. The device will start to 101 * advertise when powered off. 102 */ 103 @FlaggedApi("com.android.nearby.flags.powered_off_finding") 104 public static final int POWERED_OFF_FINDING_MODE_ENABLED = 2; 105 106 /** 107 * Powered off finding modes. 108 * 109 * @hide 110 */ 111 @IntDef( 112 prefix = {"POWERED_OFF_FINDING_MODE"}, 113 value = { 114 POWERED_OFF_FINDING_MODE_UNSUPPORTED, 115 POWERED_OFF_FINDING_MODE_DISABLED, 116 POWERED_OFF_FINDING_MODE_ENABLED, 117 }) 118 @Retention(RetentionPolicy.SOURCE) 119 public @interface PoweredOffFindingMode {} 120 121 private static final String TAG = "NearbyManager"; 122 123 private static final int POWERED_OFF_FINDING_EID_LENGTH = 20; 124 125 private static final String POWER_OFF_FINDING_SUPPORTED_PROPERTY = 126 "ro.bluetooth.finder.supported"; 127 128 /** 129 * TODO(b/286137024): Remove this when CTS R5 is rolled out. 130 * Whether allows Fast Pair to scan. 131 * 132 * (0 = disabled, 1 = enabled) 133 * 134 * @hide 135 */ 136 public static final String FAST_PAIR_SCAN_ENABLED = "fast_pair_scan_enabled"; 137 138 @GuardedBy("sScanListeners") 139 private static final WeakHashMap<ScanCallback, WeakReference<ScanListenerTransport>> 140 sScanListeners = new WeakHashMap<>(); 141 @GuardedBy("sBroadcastListeners") 142 private static final WeakHashMap<BroadcastCallback, WeakReference<BroadcastListenerTransport>> 143 sBroadcastListeners = new WeakHashMap<>(); 144 145 private final Context mContext; 146 private final INearbyManager mService; 147 148 /** 149 * Creates a new NearbyManager. 150 * 151 * @param service the service object 152 */ NearbyManager(@onNull Context context, @NonNull INearbyManager service)153 NearbyManager(@NonNull Context context, @NonNull INearbyManager service) { 154 Objects.requireNonNull(context); 155 Objects.requireNonNull(service); 156 mContext = context; 157 mService = service; 158 } 159 160 // This can be null when NearbyDeviceParcelable field not set for Presence device 161 // or the scan type is not recognized. 162 @Nullable toClientNearbyDevice( NearbyDeviceParcelable nearbyDeviceParcelable, @ScanRequest.ScanType int scanType)163 private static NearbyDevice toClientNearbyDevice( 164 NearbyDeviceParcelable nearbyDeviceParcelable, 165 @ScanRequest.ScanType int scanType) { 166 if (scanType == ScanRequest.SCAN_TYPE_FAST_PAIR) { 167 return new FastPairDevice.Builder() 168 .setName(nearbyDeviceParcelable.getName()) 169 .addMedium(nearbyDeviceParcelable.getMedium()) 170 .setRssi(nearbyDeviceParcelable.getRssi()) 171 .setTxPower(nearbyDeviceParcelable.getTxPower()) 172 .setModelId(nearbyDeviceParcelable.getFastPairModelId()) 173 .setBluetoothAddress(nearbyDeviceParcelable.getBluetoothAddress()) 174 .setData(nearbyDeviceParcelable.getData()).build(); 175 } 176 177 if (scanType == ScanRequest.SCAN_TYPE_NEARBY_PRESENCE) { 178 PresenceDevice presenceDevice = nearbyDeviceParcelable.getPresenceDevice(); 179 if (presenceDevice == null) { 180 Log.e(TAG, 181 "Cannot find any Presence device in discovered NearbyDeviceParcelable"); 182 } 183 return presenceDevice; 184 } 185 return null; 186 } 187 188 /** 189 * Start scan for nearby devices with given parameters. Devices matching {@link ScanRequest} 190 * will be delivered through the given callback. 191 * 192 * @param scanRequest various parameters clients send when requesting scanning 193 * @param executor executor where the listener method is called 194 * @param scanCallback the callback to notify clients when there is a scan result 195 * 196 * @return whether scanning was successfully started 197 */ 198 @RequiresPermission(allOf = {android.Manifest.permission.BLUETOOTH_SCAN, 199 android.Manifest.permission.BLUETOOTH_PRIVILEGED}) 200 @ScanStatus startScan(@onNull ScanRequest scanRequest, @CallbackExecutor @NonNull Executor executor, @NonNull ScanCallback scanCallback)201 public int startScan(@NonNull ScanRequest scanRequest, 202 @CallbackExecutor @NonNull Executor executor, 203 @NonNull ScanCallback scanCallback) { 204 Objects.requireNonNull(scanRequest, "scanRequest must not be null"); 205 Objects.requireNonNull(scanCallback, "scanCallback must not be null"); 206 Objects.requireNonNull(executor, "executor must not be null"); 207 208 try { 209 synchronized (sScanListeners) { 210 WeakReference<ScanListenerTransport> reference = sScanListeners.get(scanCallback); 211 ScanListenerTransport transport = reference != null ? reference.get() : null; 212 if (transport == null) { 213 transport = new ScanListenerTransport(scanRequest.getScanType(), scanCallback, 214 executor); 215 } else { 216 Preconditions.checkState(transport.isRegistered()); 217 transport.setExecutor(executor); 218 } 219 @ScanStatus int status = mService.registerScanListener(scanRequest, transport, 220 mContext.getPackageName(), mContext.getAttributionTag()); 221 if (status != ScanStatus.SUCCESS) { 222 return status; 223 } 224 sScanListeners.put(scanCallback, new WeakReference<>(transport)); 225 return ScanStatus.SUCCESS; 226 } 227 } catch (RemoteException e) { 228 throw e.rethrowFromSystemServer(); 229 } 230 } 231 232 /** 233 * Stops the nearby device scan for the specified callback. The given callback 234 * is guaranteed not to receive any invocations that happen after this method 235 * is invoked. 236 * 237 * Suppressed lint: Registration methods should have overload that accepts delivery Executor. 238 * Already have executor in startScan() method. 239 * 240 * @param scanCallback the callback that was used to start the scan 241 */ 242 @SuppressLint("ExecutorRegistration") 243 @RequiresPermission(allOf = {android.Manifest.permission.BLUETOOTH_SCAN, 244 android.Manifest.permission.BLUETOOTH_PRIVILEGED}) stopScan(@onNull ScanCallback scanCallback)245 public void stopScan(@NonNull ScanCallback scanCallback) { 246 Preconditions.checkArgument(scanCallback != null, 247 "invalid null scanCallback"); 248 try { 249 synchronized (sScanListeners) { 250 WeakReference<ScanListenerTransport> reference = sScanListeners.remove( 251 scanCallback); 252 ScanListenerTransport transport = reference != null ? reference.get() : null; 253 if (transport != null) { 254 transport.unregister(); 255 mService.unregisterScanListener(transport, mContext.getPackageName(), 256 mContext.getAttributionTag()); 257 } else { 258 Log.e(TAG, "Cannot stop scan with this callback " 259 + "because it is never registered."); 260 } 261 } 262 } catch (RemoteException e) { 263 throw e.rethrowFromSystemServer(); 264 } 265 } 266 267 /** 268 * Start broadcasting the request using nearby specification. 269 * 270 * @param broadcastRequest request for the nearby broadcast 271 * @param executor executor for running the callback 272 * @param callback callback for notifying the client 273 */ 274 @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADVERTISE, 275 android.Manifest.permission.BLUETOOTH_PRIVILEGED}) startBroadcast(@onNull BroadcastRequest broadcastRequest, @CallbackExecutor @NonNull Executor executor, @NonNull BroadcastCallback callback)276 public void startBroadcast(@NonNull BroadcastRequest broadcastRequest, 277 @CallbackExecutor @NonNull Executor executor, @NonNull BroadcastCallback callback) { 278 try { 279 synchronized (sBroadcastListeners) { 280 WeakReference<BroadcastListenerTransport> reference = sBroadcastListeners.get( 281 callback); 282 BroadcastListenerTransport transport = reference != null ? reference.get() : null; 283 if (transport == null) { 284 transport = new BroadcastListenerTransport(callback, executor); 285 } else { 286 Preconditions.checkState(transport.isRegistered()); 287 transport.setExecutor(executor); 288 } 289 mService.startBroadcast(new BroadcastRequestParcelable(broadcastRequest), transport, 290 mContext.getPackageName(), mContext.getAttributionTag()); 291 sBroadcastListeners.put(callback, new WeakReference<>(transport)); 292 } 293 } catch (RemoteException e) { 294 throw e.rethrowFromSystemServer(); 295 } 296 } 297 298 /** 299 * Stop the broadcast associated with the given callback. 300 * 301 * @param callback the callback that was used for starting the broadcast 302 */ 303 @SuppressLint("ExecutorRegistration") 304 @RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADVERTISE, 305 android.Manifest.permission.BLUETOOTH_PRIVILEGED}) stopBroadcast(@onNull BroadcastCallback callback)306 public void stopBroadcast(@NonNull BroadcastCallback callback) { 307 try { 308 synchronized (sBroadcastListeners) { 309 WeakReference<BroadcastListenerTransport> reference = sBroadcastListeners.remove( 310 callback); 311 BroadcastListenerTransport transport = reference != null ? reference.get() : null; 312 if (transport != null) { 313 transport.unregister(); 314 mService.stopBroadcast(transport, mContext.getPackageName(), 315 mContext.getAttributionTag()); 316 } else { 317 Log.e(TAG, "Cannot stop broadcast with this callback " 318 + "because it is never registered."); 319 } 320 } 321 } catch (RemoteException e) { 322 throw e.rethrowFromSystemServer(); 323 } 324 } 325 326 /** 327 * Query offload capability in a device. The query is asynchronous and result is called back 328 * in {@link Consumer}, which is set to true if offload is supported. 329 * 330 * @param executor the callback will take place on this {@link Executor} 331 * @param callback the callback invoked with {@link OffloadCapability} 332 */ queryOffloadCapability(@onNull @allbackExecutor Executor executor, @NonNull Consumer<OffloadCapability> callback)333 public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor, 334 @NonNull Consumer<OffloadCapability> callback) { 335 Objects.requireNonNull(executor); 336 Objects.requireNonNull(callback); 337 try { 338 mService.queryOffloadCapability(new OffloadTransport(executor, callback)); 339 } catch (RemoteException e) { 340 throw e.rethrowFromSystemServer(); 341 } 342 } 343 344 private static class OffloadTransport extends IOffloadCallback.Stub { 345 346 private final Executor mExecutor; 347 // Null when cancelled 348 volatile @Nullable Consumer<OffloadCapability> mConsumer; 349 OffloadTransport(Executor executor, Consumer<OffloadCapability> consumer)350 OffloadTransport(Executor executor, Consumer<OffloadCapability> consumer) { 351 Preconditions.checkArgument(executor != null, "illegal null executor"); 352 Preconditions.checkArgument(consumer != null, "illegal null consumer"); 353 mExecutor = executor; 354 mConsumer = consumer; 355 } 356 357 @Override onQueryComplete(OffloadCapability capability)358 public void onQueryComplete(OffloadCapability capability) { 359 mExecutor.execute(() -> { 360 if (mConsumer != null) { 361 mConsumer.accept(capability); 362 } 363 }); 364 } 365 } 366 367 private static class ScanListenerTransport extends IScanListener.Stub { 368 369 private @ScanRequest.ScanType int mScanType; 370 private volatile @Nullable ScanCallback mScanCallback; 371 private Executor mExecutor; 372 ScanListenerTransport(@canRequest.ScanType int scanType, ScanCallback scanCallback, @CallbackExecutor Executor executor)373 ScanListenerTransport(@ScanRequest.ScanType int scanType, ScanCallback scanCallback, 374 @CallbackExecutor Executor executor) { 375 Preconditions.checkArgument(scanCallback != null, 376 "invalid null callback"); 377 Preconditions.checkState(ScanRequest.isValidScanType(scanType), 378 "invalid scan type : " + scanType 379 + ", scan type must be one of ScanRequest#SCAN_TYPE_"); 380 mScanType = scanType; 381 mScanCallback = scanCallback; 382 mExecutor = executor; 383 } 384 setExecutor(Executor executor)385 void setExecutor(Executor executor) { 386 Preconditions.checkArgument( 387 executor != null, "invalid null executor"); 388 mExecutor = executor; 389 } 390 isRegistered()391 boolean isRegistered() { 392 return mScanCallback != null; 393 } 394 unregister()395 void unregister() { 396 mScanCallback = null; 397 } 398 399 @Override onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable)400 public void onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable) 401 throws RemoteException { 402 mExecutor.execute(() -> { 403 NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType); 404 if (mScanCallback != null && nearbyDevice != null) { 405 mScanCallback.onDiscovered(nearbyDevice); 406 } 407 }); 408 } 409 410 @Override onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable)411 public void onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable) 412 throws RemoteException { 413 mExecutor.execute(() -> { 414 NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType); 415 if (mScanCallback != null && nearbyDevice != null) { 416 mScanCallback.onUpdated( 417 toClientNearbyDevice(nearbyDeviceParcelable, mScanType)); 418 } 419 }); 420 } 421 422 @Override onLost(NearbyDeviceParcelable nearbyDeviceParcelable)423 public void onLost(NearbyDeviceParcelable nearbyDeviceParcelable) throws RemoteException { 424 mExecutor.execute(() -> { 425 NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType); 426 if (mScanCallback != null && nearbyDevice != null) { 427 mScanCallback.onLost( 428 toClientNearbyDevice(nearbyDeviceParcelable, mScanType)); 429 } 430 }); 431 } 432 433 @Override onError(int errorCode)434 public void onError(int errorCode) { 435 mExecutor.execute(() -> { 436 if (mScanCallback != null) { 437 mScanCallback.onError(errorCode); 438 } 439 }); 440 } 441 } 442 443 private static class BroadcastListenerTransport extends IBroadcastListener.Stub { 444 private volatile @Nullable BroadcastCallback mBroadcastCallback; 445 private Executor mExecutor; 446 BroadcastListenerTransport(BroadcastCallback broadcastCallback, @CallbackExecutor Executor executor)447 BroadcastListenerTransport(BroadcastCallback broadcastCallback, 448 @CallbackExecutor Executor executor) { 449 mBroadcastCallback = broadcastCallback; 450 mExecutor = executor; 451 } 452 setExecutor(Executor executor)453 void setExecutor(Executor executor) { 454 Preconditions.checkArgument( 455 executor != null, "invalid null executor"); 456 mExecutor = executor; 457 } 458 isRegistered()459 boolean isRegistered() { 460 return mBroadcastCallback != null; 461 } 462 unregister()463 void unregister() { 464 mBroadcastCallback = null; 465 } 466 467 @Override onStatusChanged(int status)468 public void onStatusChanged(int status) { 469 mExecutor.execute(() -> { 470 if (mBroadcastCallback != null) { 471 mBroadcastCallback.onStatusChanged(status); 472 } 473 }); 474 } 475 } 476 477 /** 478 * TODO(b/286137024): Remove this when CTS R5 is rolled out. 479 * Read from {@link Settings} whether Fast Pair scan is enabled. 480 * 481 * @param context the {@link Context} to query the setting 482 * @return whether the Fast Pair is enabled 483 * @hide 484 */ getFastPairScanEnabled(@onNull Context context)485 public static boolean getFastPairScanEnabled(@NonNull Context context) { 486 final int enabled = Settings.Secure.getInt( 487 context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, 0); 488 return enabled != 0; 489 } 490 491 /** 492 * TODO(b/286137024): Remove this when CTS R5 is rolled out. 493 * Write into {@link Settings} whether Fast Pair scan is enabled 494 * 495 * @param context the {@link Context} to set the setting 496 * @param enable whether the Fast Pair scan should be enabled 497 * @hide 498 */ 499 @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS) setFastPairScanEnabled(@onNull Context context, boolean enable)500 public static void setFastPairScanEnabled(@NonNull Context context, boolean enable) { 501 Settings.Secure.putInt( 502 context.getContentResolver(), FAST_PAIR_SCAN_ENABLED, enable ? 1 : 0); 503 Log.v(TAG, String.format( 504 "successfully %s Fast Pair scan", enable ? "enables" : "disables")); 505 } 506 507 /** 508 * Sets the precomputed EIDs for advertising when the phone is powered off. The Bluetooth 509 * controller will store these EIDs in its memory, and will start advertising them in Find My 510 * Device network EID frames when powered off, only if the powered off finding mode was 511 * previously enabled by calling {@link #setPoweredOffFindingMode(int)}. 512 * 513 * <p>The EIDs are cryptographic ephemeral identifiers that change periodically, based on the 514 * Android clock at the time of the shutdown. They are used as the public part of asymmetric key 515 * pairs. Members of the Find My Device network can use them to encrypt the location of where 516 * they sight the advertising device. Only someone in possession of the private key (the device 517 * owner or someone that the device owner shared the key with) can decrypt this encrypted 518 * location. 519 * 520 * <p>Android will typically call this method during the shutdown process. Even after the 521 * method was called, it is still possible to call {#link setPoweredOffFindingMode() to disable 522 * the advertisement, for example to temporarily disable it for a single shutdown. 523 * 524 * <p>If called more than once, the EIDs of the most recent call overrides the EIDs from any 525 * previous call. 526 * 527 * @throws IllegalArgumentException if the length of one of the EIDs is not 20 bytes 528 */ 529 @FlaggedApi("com.android.nearby.flags.powered_off_finding") 530 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) setPoweredOffFindingEphemeralIds(@onNull List<byte[]> eids)531 public void setPoweredOffFindingEphemeralIds(@NonNull List<byte[]> eids) { 532 Objects.requireNonNull(eids); 533 if (!isPoweredOffFindingSupported()) { 534 throw new UnsupportedOperationException( 535 "Powered off finding is not supported on this device"); 536 } 537 List<PoweredOffFindingEphemeralId> ephemeralIdList = eids.stream().map( 538 eid -> { 539 Preconditions.checkArgument(eid.length == POWERED_OFF_FINDING_EID_LENGTH); 540 PoweredOffFindingEphemeralId ephemeralId = new PoweredOffFindingEphemeralId(); 541 ephemeralId.bytes = eid; 542 return ephemeralId; 543 }).toList(); 544 try { 545 mService.setPoweredOffFindingEphemeralIds(ephemeralIdList); 546 } catch (RemoteException e) { 547 throw e.rethrowFromSystemServer(); 548 } 549 550 } 551 552 /** 553 * Turns the powered off finding on or off. Power off finding will operate only if this method 554 * was called at least once since boot, and the value of the argument {@code 555 * poweredOffFindinMode} was {@link #POWERED_OFF_FINDING_MODE_ENABLED} the last time the method 556 * was called. 557 * 558 * <p>When an Android device with the powered off finding feature is turned off (either as part 559 * of a normal shutdown or due to dead battery), its Bluetooth chip starts to advertise Find My 560 * Device network EID frames with the EID payload that were provided by the last call to {@link 561 * #setPoweredOffFindingEphemeralIds(List)}. These EIDs can be sighted by other Android devices 562 * in BLE range that are part of the Find My Device network. The Android sighters use the EID to 563 * encrypt the location of the Android device and upload it to the server, in a way that only 564 * the owner of the advertising device, or people that the owner shared their encryption key 565 * with, can decrypt the location. 566 * 567 * @param poweredOffFindingMode {@link #POWERED_OFF_FINDING_MODE_ENABLED} or {@link 568 * #POWERED_OFF_FINDING_MODE_DISABLED} 569 * 570 * @throws IllegalStateException if called with {@link #POWERED_OFF_FINDING_MODE_ENABLED} when 571 * Bluetooth or location services are disabled 572 */ 573 @FlaggedApi("com.android.nearby.flags.powered_off_finding") 574 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) setPoweredOffFindingMode(@oweredOffFindingMode int poweredOffFindingMode)575 public void setPoweredOffFindingMode(@PoweredOffFindingMode int poweredOffFindingMode) { 576 Preconditions.checkArgument( 577 poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED 578 || poweredOffFindingMode == POWERED_OFF_FINDING_MODE_DISABLED, 579 "invalid poweredOffFindingMode"); 580 if (!isPoweredOffFindingSupported()) { 581 throw new UnsupportedOperationException( 582 "Powered off finding is not supported on this device"); 583 } 584 if (poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED) { 585 Preconditions.checkState(areLocationAndBluetoothEnabled(), 586 "Location services and Bluetooth must be on"); 587 } 588 try { 589 mService.setPoweredOffModeEnabled( 590 poweredOffFindingMode == POWERED_OFF_FINDING_MODE_ENABLED); 591 } catch (RemoteException e) { 592 throw e.rethrowFromSystemServer(); 593 } 594 } 595 596 /** 597 * Returns the state of the powered off finding feature. 598 * 599 * <p>{@link #POWERED_OFF_FINDING_MODE_UNSUPPORTED} if the feature is not supported by the 600 * device, {@link #POWERED_OFF_FINDING_MODE_DISABLED} if this was the last value set by {@link 601 * #setPoweredOffFindingMode(int)} or if no value was set since boot, {@link 602 * #POWERED_OFF_FINDING_MODE_ENABLED} if this was the last value set by {@link 603 * #setPoweredOffFindingMode(int)} 604 */ 605 @FlaggedApi("com.android.nearby.flags.powered_off_finding") 606 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) getPoweredOffFindingMode()607 public @PoweredOffFindingMode int getPoweredOffFindingMode() { 608 if (!isPoweredOffFindingSupported()) { 609 return POWERED_OFF_FINDING_MODE_UNSUPPORTED; 610 } 611 try { 612 return mService.getPoweredOffModeEnabled() 613 ? POWERED_OFF_FINDING_MODE_ENABLED : POWERED_OFF_FINDING_MODE_DISABLED; 614 } catch (RemoteException e) { 615 throw e.rethrowFromSystemServer(); 616 } 617 } 618 isPoweredOffFindingSupported()619 private boolean isPoweredOffFindingSupported() { 620 return Boolean.parseBoolean(SystemProperties.get(POWER_OFF_FINDING_SUPPORTED_PROPERTY)); 621 } 622 areLocationAndBluetoothEnabled()623 private boolean areLocationAndBluetoothEnabled() { 624 return mContext.getSystemService(BluetoothManager.class).getAdapter().isEnabled() 625 && mContext.getSystemService(LocationManager.class).isLocationEnabled(); 626 } 627 } 628