2

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:

  1. I wake up the app every hour with my Alarm Manager.

  2. If received a call back from the app's Broadcast Receiver, Schedule the next alarm and the WorkManager perform with expedited option.

  3. 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...

daemon
  • 45
  • 1
  • 9

3 Answers3

2

Google has clamped down on apps that are running services in the background. The code: 20 means that your app is put into the restricted bucket. Everything greater then 10 is restricted.

You can find more information in the documentation. You also need to take account that a phone behaves differently depending on power modes. You'll need to test that as well.

Lastly if the customer wants this behavior I think they should consider getting a custom Android device with their own ROM. Using your own ASOP gives you the option to do whatever and grant your apps to do whatever. Another option is to use this on ROOTED devices.

From the source code regarding buckets:

    STANDBY_BUCKET_EXEMPTED = 5;

    /**
     * The app was used very recently, currently in use or likely to be used very soon. Standby
     * bucket values that are &le; {@link #STANDBY_BUCKET_ACTIVE} will not be throttled by the
     * system while they are in this bucket. Buckets &gt; {@link #STANDBY_BUCKET_ACTIVE} will most
     * likely be restricted in some way. For instance, jobs and alarms may be deferred.
     * @see #getAppStandbyBucket()
     */
    STANDBY_BUCKET_ACTIVE = 10;

    /**
     * The app was used recently and/or likely to be used in the next few hours. Restrictions will
     * apply to these apps, such as deferral of jobs and alarms.
     * @see #getAppStandbyBucket()
     */
    STANDBY_BUCKET_WORKING_SET = 20;

    /**
     * The app was used in the last few days and/or likely to be used in the next few days.
     * Restrictions will apply to these apps, such as deferral of jobs and alarms. The delays may be
     * greater than for apps in higher buckets (lower bucket value). Bucket values &gt;
     * {@link #STANDBY_BUCKET_FREQUENT} may additionally have network access limited.
     * @see #getAppStandbyBucket()
     */
    STANDBY_BUCKET_FREQUENT = 30;

    /**
     * The app has not be used for several days and/or is unlikely to be used for several days.
     * Apps in this bucket will have more restrictions, including network restrictions, except
     * during certain short periods (at a minimum, once a day) when they are allowed to execute
     * jobs, access the network, etc.
     * @see #getAppStandbyBucket()
     */
    STANDBY_BUCKET_RARE = 40;

    /**
     * The app has not be used for several days, is unlikely to be used for several days, and has
     * been misbehaving in some manner.
     * Apps in this bucket will have the most restrictions, including network restrictions and
     * additional restrictions on jobs.
     * <p> Note: this bucket is not enabled in {@link Build.VERSION_CODES#R}.
     * @see #getAppStandbyBucket()
     */
    STANDBY_BUCKET_RESTRICTED = 45;

    /**
     * The app has never been used.
     */
    STANDBY_BUCKET_NEVER = 50;
Haroun Hajem
  • 5,223
  • 3
  • 26
  • 39
  • 1
    I solved a problem recently. The cause of the problem was related to the battery optimization settings. When I set it to 'unrestricted' in app settings -> battery, the BLE scan worked very well even in the background. Thanks! – daemon Mar 07 '23 at 02:53
  • @daemon Great! Stay updated on how Google want's to deal with background operations on Android. The problem has been that many apps are running background operations whom drain batteries. This is a cat and mouse game. Google changes the bar with almost every other update of the OS. It sucks since many apps want to do real work in the background to offer the user something valuable. – Haroun Hajem Mar 07 '23 at 09:12
0

Instead of triggering from the alarm manager, use companion device api's (If I'm not wrong available from android 12)

And also consider to change the current scan api to startScan(List filters, ScanSettings settings, PendingIntent callbackIntent)

Muhamed Riyas M
  • 5,055
  • 3
  • 30
  • 31
  • I solved a problem recently. The cause of the problem was related to the battery optimization settings. When I set it to 'unrestricted' in app settings -> battery, the BLE scan worked very well even in the background. Thanks! – daemon Mar 07 '23 at 02:52
  • Thats great, still I would recommend my solution to optimise the battery usage of app. Cheers! – Muhamed Riyas M Mar 07 '23 at 14:20
-1

Make sure your device's GPS location is turned on, like this in the settings.

Haroun Hajem
  • 5,223
  • 3
  • 26
  • 39
  • I solved a problem recently. The cause of the problem was related to the battery optimization settings. When I set it to 'unrestricted' in app settings -> battery, the BLE scan worked very well even in the background. Thanks! – daemon Mar 07 '23 at 02:53