blob: 333ba5c0f4c1b1a790e685e05296684e28c15e30 [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.nearby;
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
import android.nearby.aidl.IOffloadCallback;
import android.os.RemoteException;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* This class provides a way to perform Nearby related operations such as scanning, broadcasting
* and connecting to nearby devices.
*
* <p> To get a {@link NearbyManager} instance, call the
* <code>Context.getSystemService(NearbyManager.class)</code>.
*
* @hide
*/
@SystemApi
@SystemService(Context.NEARBY_SERVICE)
public class NearbyManager {
/**
* Represents the scanning state.
*
* @hide
*/
@IntDef({
ScanStatus.UNKNOWN,
ScanStatus.SUCCESS,
ScanStatus.ERROR,
})
public @interface ScanStatus {
// The undetermined status, some modules may be initializing. Retry is suggested.
int UNKNOWN = 0;
// The successful state.
int SUCCESS = 1;
// Failed state.
int ERROR = 2;
}
private static final String TAG = "NearbyManager";
/**
* Whether allows Fast Pair to scan.
*
* (0 = disabled, 1 = enabled)
*
* @hide
*/
public static final String FAST_PAIR_SCAN_ENABLED = "fast_pair_scan_enabled";
@GuardedBy("sScanListeners")
private static final WeakHashMap<ScanCallback, WeakReference<ScanListenerTransport>>
sScanListeners = new WeakHashMap<>();
@GuardedBy("sBroadcastListeners")
private static final WeakHashMap<BroadcastCallback, WeakReference<BroadcastListenerTransport>>
sBroadcastListeners = new WeakHashMap<>();
private final Context mContext;
private final INearbyManager mService;
/**
* Creates a new NearbyManager.
*
* @param service the service object
*/
NearbyManager(@NonNull Context context, @NonNull INearbyManager service) {
Objects.requireNonNull(context);
Objects.requireNonNull(service);
mContext = context;
mService = service;
}
// This can be null when NearbyDeviceParcelable field not set for Presence device
// or the scan type is not recognized.
@Nullable
private static NearbyDevice toClientNearbyDevice(
NearbyDeviceParcelable nearbyDeviceParcelable,
@ScanRequest.ScanType int scanType) {
if (scanType == ScanRequest.SCAN_TYPE_FAST_PAIR) {
return new FastPairDevice.Builder()
.setName(nearbyDeviceParcelable.getName())
.addMedium(nearbyDeviceParcelable.getMedium())
.setRssi(nearbyDeviceParcelable.getRssi())
.setTxPower(nearbyDeviceParcelable.getTxPower())
.setModelId(nearbyDeviceParcelable.getFastPairModelId())
.setBluetoothAddress(nearbyDeviceParcelable.getBluetoothAddress())
.setData(nearbyDeviceParcelable.getData()).build();
}
if (scanType == ScanRequest.SCAN_TYPE_NEARBY_PRESENCE) {
PresenceDevice presenceDevice = nearbyDeviceParcelable.getPresenceDevice();
if (presenceDevice == null) {
Log.e(TAG,
"Cannot find any Presence device in discovered NearbyDeviceParcelable");
}
return presenceDevice;
}
return null;
}
/**
* Start scan for nearby devices with given parameters. Devices matching {@link ScanRequest}
* will be delivered through the given callback.
*
* @param scanRequest various parameters clients send when requesting scanning
* @param executor executor where the listener method is called
* @param scanCallback the callback to notify clients when there is a scan result
*
* @return whether scanning was successfully started
*/
@RequiresPermission(allOf = {android.Manifest.permission.BLUETOOTH_SCAN,
android.Manifest.permission.BLUETOOTH_PRIVILEGED})
@ScanStatus
public int startScan(@NonNull ScanRequest scanRequest,
@CallbackExecutor @NonNull Executor executor,
@NonNull ScanCallback scanCallback) {
Objects.requireNonNull(scanRequest, "scanRequest must not be null");
Objects.requireNonNull(scanCallback, "scanCallback must not be null");
Objects.requireNonNull(executor, "executor must not be null");
try {
synchronized (sScanListeners) {
WeakReference<ScanListenerTransport> reference = sScanListeners.get(scanCallback);
ScanListenerTransport transport = reference != null ? reference.get() : null;
if (transport == null) {
transport = new ScanListenerTransport(scanRequest.getScanType(), scanCallback,
executor);
} else {
Preconditions.checkState(transport.isRegistered());
transport.setExecutor(executor);
}
@ScanStatus int status = mService.registerScanListener(scanRequest, transport,
mContext.getPackageName(), mContext.getAttributionTag());
if (status != ScanStatus.SUCCESS) {
return status;
}
sScanListeners.put(scanCallback, new WeakReference<>(transport));
return ScanStatus.SUCCESS;
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Stops the nearby device scan for the specified callback. The given callback
* is guaranteed not to receive any invocations that happen after this method
* is invoked.
*
* Suppressed lint: Registration methods should have overload that accepts delivery Executor.
* Already have executor in startScan() method.
*
* @param scanCallback the callback that was used to start the scan
*/
@SuppressLint("ExecutorRegistration")
@RequiresPermission(allOf = {android.Manifest.permission.BLUETOOTH_SCAN,
android.Manifest.permission.BLUETOOTH_PRIVILEGED})
public void stopScan(@NonNull ScanCallback scanCallback) {
Preconditions.checkArgument(scanCallback != null,
"invalid null scanCallback");
try {
synchronized (sScanListeners) {
WeakReference<ScanListenerTransport> reference = sScanListeners.remove(
scanCallback);
ScanListenerTransport transport = reference != null ? reference.get() : null;
if (transport != null) {
transport.unregister();
mService.unregisterScanListener(transport, mContext.getPackageName(),
mContext.getAttributionTag());
} else {
Log.e(TAG, "Cannot stop scan with this callback "
+ "because it is never registered.");
}
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Start broadcasting the request using nearby specification.
*
* @param broadcastRequest request for the nearby broadcast
* @param executor executor for running the callback
* @param callback callback for notifying the client
*/
@RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADVERTISE,
android.Manifest.permission.BLUETOOTH_PRIVILEGED})
public void startBroadcast(@NonNull BroadcastRequest broadcastRequest,
@CallbackExecutor @NonNull Executor executor, @NonNull BroadcastCallback callback) {
try {
synchronized (sBroadcastListeners) {
WeakReference<BroadcastListenerTransport> reference = sBroadcastListeners.get(
callback);
BroadcastListenerTransport transport = reference != null ? reference.get() : null;
if (transport == null) {
transport = new BroadcastListenerTransport(callback, executor);
} else {
Preconditions.checkState(transport.isRegistered());
transport.setExecutor(executor);
}
mService.startBroadcast(new BroadcastRequestParcelable(broadcastRequest), transport,
mContext.getPackageName(), mContext.getAttributionTag());
sBroadcastListeners.put(callback, new WeakReference<>(transport));
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Stop the broadcast associated with the given callback.
*
* @param callback the callback that was used for starting the broadcast
*/
@SuppressLint("ExecutorRegistration")
@RequiresPermission(allOf = {Manifest.permission.BLUETOOTH_ADVERTISE,
android.Manifest.permission.BLUETOOTH_PRIVILEGED})
public void stopBroadcast(@NonNull BroadcastCallback callback) {
try {
synchronized (sBroadcastListeners) {
WeakReference<BroadcastListenerTransport> reference = sBroadcastListeners.remove(
callback);
BroadcastListenerTransport transport = reference != null ? reference.get() : null;
if (transport != null) {
transport.unregister();
mService.stopBroadcast(transport, mContext.getPackageName(),
mContext.getAttributionTag());
} else {
Log.e(TAG, "Cannot stop broadcast with this callback "
+ "because it is never registered.");
}
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Query offload capability in a device. The query is asynchronous and result is called back
* in {@link Consumer}, which is set to true if offload is supported.
*
* @param executor the callback will take place on this {@link Executor}
* @param callback the callback invoked with {@link OffloadCapability}
*/
public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<OffloadCapability> callback) {
try {
mService.queryOffloadCapability(new OffloadTransport(executor, callback));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
private static class OffloadTransport extends IOffloadCallback.Stub {
private final Executor mExecutor;
// Null when cancelled
volatile @Nullable Consumer<OffloadCapability> mConsumer;
OffloadTransport(Executor executor, Consumer<OffloadCapability> consumer) {
Preconditions.checkArgument(executor != null, "illegal null executor");
Preconditions.checkArgument(consumer != null, "illegal null consumer");
mExecutor = executor;
mConsumer = consumer;
}
@Override
public void onQueryComplete(OffloadCapability capability) {
mExecutor.execute(() -> {
if (mConsumer != null) {
mConsumer.accept(capability);
}
});
}
}
private static class ScanListenerTransport extends IScanListener.Stub {
private @ScanRequest.ScanType int mScanType;
private volatile @Nullable ScanCallback mScanCallback;
private Executor mExecutor;
ScanListenerTransport(@ScanRequest.ScanType int scanType, ScanCallback scanCallback,
@CallbackExecutor Executor executor) {
Preconditions.checkArgument(scanCallback != null,
"invalid null callback");
Preconditions.checkState(ScanRequest.isValidScanType(scanType),
"invalid scan type : " + scanType
+ ", scan type must be one of ScanRequest#SCAN_TYPE_");
mScanType = scanType;
mScanCallback = scanCallback;
mExecutor = executor;
}
void setExecutor(Executor executor) {
Preconditions.checkArgument(
executor != null, "invalid null executor");
mExecutor = executor;
}
boolean isRegistered() {
return mScanCallback != null;
}
void unregister() {
mScanCallback = null;
}
@Override
public void onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable)
throws RemoteException {
mExecutor.execute(() -> {
NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
if (mScanCallback != null && nearbyDevice != null) {
mScanCallback.onDiscovered(nearbyDevice);
}
});
}
@Override
public void onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable)
throws RemoteException {
mExecutor.execute(() -> {
NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
if (mScanCallback != null && nearbyDevice != null) {
mScanCallback.onUpdated(
toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
}
});
}
@Override
public void onLost(NearbyDeviceParcelable nearbyDeviceParcelable) throws RemoteException {
mExecutor.execute(() -> {
NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
if (mScanCallback != null && nearbyDevice != null) {
mScanCallback.onLost(
toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
}
});
}
@Override
public void onError(int errorCode) {
mExecutor.execute(() -> {
if (mScanCallback != null) {
mScanCallback.onError(errorCode);
}
});
}
}
private static class BroadcastListenerTransport extends IBroadcastListener.Stub {
private volatile @Nullable BroadcastCallback mBroadcastCallback;
private Executor mExecutor;
BroadcastListenerTransport(BroadcastCallback broadcastCallback,
@CallbackExecutor Executor executor) {
mBroadcastCallback = broadcastCallback;
mExecutor = executor;
}
void setExecutor(Executor executor) {
Preconditions.checkArgument(
executor != null, "invalid null executor");
mExecutor = executor;
}
boolean isRegistered() {
return mBroadcastCallback != null;
}
void unregister() {
mBroadcastCallback = null;
}
@Override
public void onStatusChanged(int status) {
mExecutor.execute(() -> {
if (mBroadcastCallback != null) {
mBroadcastCallback.onStatusChanged(status);
}
});
}
}
}