Search code examples
androidbluetooth-lowenergyandroid-backgroundandroid-13

After Android 13 update, BLE scan result is not found frequently


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


Solution

  • 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;