Firstly I tried a background BLE scan. (It is periodically.)
The app uses a lot of background, but my client wants it like that.
The my implementation was done in the following ways:
I wake up the app every hour with my Alarm Manager.
If received a call back from the app's Broadcast Receiver, Schedule the next alarm and the WorkManager perform with expedited option.
Start BLE Scan 40 seconds -> send BLE Device Data to Server.
Manifest.xml
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<uses-feature android:name="android.hardware.location.gps" android:required="true" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
scanCallbackImpl.java
public class ScanCallbackImpl extends ScanCallback {
private final PublishSubject<ScanResult> onScanResult$;
private final Context context;
private static final String SCAN_TEST = "SCAN_TEST";
public ScanCallbackImpl(Context context) {
super();
this.context = context;
onScanResult$ = PublishSubject.create();
}
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
Log.d(SCAN_TEST, "onScanResult");
onScanResult$.onNext(result);
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
Log.d(SCAN_TEST, "onBatchScanResult");
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
Log.d(SCAN_TEST, "onScanFailed");
switch (errorCode) {
case ScanCallback.SCAN_FAILED_ALREADY_STARTED:
onScanResult$.onError(new AlreadyStartedException(context));
break;
case ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
onScanResult$.onError(new ApplicationRegistrationFailedException(context));
break;
case ScanCallback.SCAN_FAILED_INTERNAL_ERROR:
onScanResult$.onError(new InternalErrorException(context));
break;
case ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED:
onScanResult$.onError(new FeatureUnsupportedException(context));
break;
}
}
public PublishSubject<ScanResult> getOnScanResult$() {
return onScanResult$;
}
}
ScanService.java
private void scanClose() {
if(onScanResult != null && !onScanResult.isDisposed()){
onScanResult.dispose();
}
if(startScan != null && !startScan.isDisposed()){
startScan.dispose();
}
}
public Observable<ScanDeviceVo> startScan(String deviceName, String macAddress, long timeoutSec, ScanMode scanMode) {
scanClose();
Observable<ScanResult> observable = Observable.create(emitter -> {
if(!checkBleScanSupport()){
emitter.onError(new BleScanUnsupportedException(context));
return;
}
if(!checkEnable(emitter)){
return;
}
if(!isAlwaysLocation()){
emitter.onError(new LocationPermissionException(context));
return;
}
BluetoothLeScanner bleScanner = bluetoothAdapter.getBluetoothLeScanner();
scanCallback = new ScanCallbackImpl(context);
scanCallback.getOnScanResult$()
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.doOnSubscribe(disposable -> onScanResult = disposable)
.subscribe(emitter::onNext, emitter::onError);
isScanning = true;
bleScanner.startScan(setScanFilterList(deviceName, macAddress), setScanSetting(scanMode), scanCallback);
});
return observable
.observeOn(Schedulers.io())
.take(timeoutSec, TimeUnit.SECONDS)
.timeout(timeoutSec, TimeUnit.SECONDS)
.onErrorResumeNext(throwable -> throwable instanceof TimeoutException ? Observable.error(new NotFoundException(context)) : Observable.error(throwable))
.distinct(scanResult -> scanResult.getScanRecord().getBytes())
.map(scanResult -> ScanDeviceVo.builder()
.name(scanResult.getDevice().getName())
.macAddr(scanResult.getDevice().getAddress())
.packet(scanResult.getScanRecord().getBytes())
.rssi(scanResult.getRssi()).build())
.doOnSubscribe(disposable -> startScan = disposable)
.doOnTerminate(this::stopScan);
}
private ScanSettings setScanSetting(ScanMode scanMode){
ScanSettings.Builder builder = new ScanSettings.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
builder = builder.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE);
}
return builder.setScanMode(scanMode.rawValue())
.setReportDelay(0)
.build();
}
private List<ScanFilter> setScanFilterList(String deviceName, String macAddress) {
List<ScanFilter> scanFilterList = new Vector<>();
ScanFilter.Builder builder = new ScanFilter.Builder();
if(!deviceName.equals("")){
builder = builder.setDeviceName(deviceName);
}
if(!macAddress.equals("")){
builder = builder.setDeviceAddress(macAddress);
}
scanFilterList.add(builder.build());
return scanFilterList;
}
ScanWorker.java
....
....
ScanService scanService = new ScanService(context);
scanService.startScan(deviceName, syncedMac, 40, ScanMode.BALANCED)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.firstOrError()
.onErrorResumeNext(throwable -> {
scanService.stopScan();
return throwable instanceof NoSuchElementException ? Single.error(new NotFoundException(context)) : Single.error(throwable);
})
.map(scanDeviceVo -> new AdvData(scanDeviceVo.getPacket()))
.subscribe(advData -> { ..send with retrofit.. });
....
....
It works well on Android 12 and earlier. (Even in Deep Doze Mode.) but after the update to Android 13, BLE Scan Result is Not found frequently...
(Alarm works, workManager with expedited option works, scanCallbak in onScanResult not called often)
I discover an Android Developer website. (https://developer.android.com/about/versions/13/changes/battery)
What changed with Android 13 is that "If you receive too many broadcast receivers, they are added to the Restricted app stand by bucket."
I entered the following command at the terminal.
adb shell am get-standby-bucket PACKAGE_NAME
//20
Is the standby bucket value 20 related to frequent missing BLE scan results?
I've been troubled for days...