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