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