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