• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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