package no.nordicsemi.android.support.v18.scanner; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.le.BluetoothLeScanner; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Build; import java.util.ArrayList; import java.util.List; import androidx.annotation.RequiresApi; /** * This receiver, registered in AndroidManifest, will translate received * {@link android.bluetooth.le.ScanResult}s into compat {@link ScanResult}s and will send * a {@link PendingIntent} registered by the user with those converted data. It will also apply * any filters, perform batching or emulate callback types * {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} and * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} on devices that do not support it. */ public class PendingIntentReceiver extends BroadcastReceiver { /* package */ static final String ACTION = "no.nordicsemi.android.support.v18.ACTION_FOUND"; /* package */ static final String EXTRA_PENDING_INTENT = "no.nordicsemi.android.support.v18.EXTRA_PENDING_INTENT"; /* package */ static final String EXTRA_FILTERS = "no.nordicsemi.android.support.v18.EXTRA_FILTERS"; /* package */ static final String EXTRA_SETTINGS = "no.nordicsemi.android.support.v18.EXTRA_SETTINGS"; /* package */ static final String EXTRA_USE_HARDWARE_BATCHING = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_BATCHING"; /* package */ static final String EXTRA_USE_HARDWARE_FILTERING = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_FILTERING"; /* package */ static final String EXTRA_USE_HARDWARE_CALLBACK_TYPES = "no.nordicsemi.android.support.v18.EXTRA_USE_HARDWARE_CALLBACK_TYPES"; /* package */ static final String EXTRA_MATCH_LOST_TIMEOUT = "no.nordicsemi.android.support.v18.EXTRA_MATCH_LOST_TIMEOUT"; /* package */ static final String EXTRA_MATCH_LOST_INTERVAL = "no.nordicsemi.android.support.v18.EXTRA_MATCH_LOST_INTERVAL"; /* package */ static final String EXTRA_MATCH_MODE = "no.nordicsemi.android.support.v18.EXTRA_MATCH_MODE"; /* package */ static final String EXTRA_NUM_OF_MATCHES = "no.nordicsemi.android.support.v18.EXTRA_NUM_OF_MATCHES"; @RequiresApi(api = Build.VERSION_CODES.O) @Override public void onReceive(final Context context, final Intent intent) { // Ensure we are ok. if (context == null || intent == null) return; // Find the target pending intent. final PendingIntent callbackIntent = intent.getParcelableExtra(EXTRA_PENDING_INTENT); if (callbackIntent == null) return; // Filters and settings have been set as native objects, otherwise they could not be // serialized by the system scanner. final ArrayList<android.bluetooth.le.ScanFilter> nativeScanFilters = intent.getParcelableArrayListExtra(EXTRA_FILTERS); final android.bluetooth.le.ScanSettings nativeScanSettings = intent.getParcelableExtra(EXTRA_SETTINGS); if (nativeScanFilters == null || nativeScanSettings == null) return; // Some ScanSettings parameters are only on compat version and need to be sent separately. final boolean useHardwareBatchingIfSupported = intent.getBooleanExtra(EXTRA_USE_HARDWARE_BATCHING, true); final boolean useHardwareFilteringIfSupported = intent.getBooleanExtra(EXTRA_USE_HARDWARE_FILTERING, true); final boolean useHardwareCallbackTypesIfSupported = intent.getBooleanExtra(EXTRA_USE_HARDWARE_CALLBACK_TYPES, true); final long matchLostDeviceTimeout = intent.getLongExtra(EXTRA_MATCH_LOST_TIMEOUT, ScanSettings.MATCH_LOST_DEVICE_TIMEOUT_DEFAULT); final long matchLostTaskInterval = intent.getLongExtra(EXTRA_MATCH_LOST_INTERVAL, ScanSettings.MATCH_LOST_TASK_INTERVAL_DEFAULT); final int matchMode = intent.getIntExtra(EXTRA_MATCH_MODE, ScanSettings.MATCH_MODE_AGGRESSIVE); final int numOfMatches = intent.getIntExtra(EXTRA_NUM_OF_MATCHES, ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT); // Convert native objects to compat versions. final BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner(); final BluetoothLeScannerImplOreo scannerImpl = (BluetoothLeScannerImplOreo) scanner; final ArrayList<ScanFilter> filters = scannerImpl.fromNativeScanFilters(nativeScanFilters); final ScanSettings settings = scannerImpl.fromNativeScanSettings(nativeScanSettings, useHardwareBatchingIfSupported, useHardwareFilteringIfSupported, useHardwareCallbackTypesIfSupported, matchLostDeviceTimeout, matchLostTaskInterval, matchMode, numOfMatches); // Check device capabilities and create a wrapper that will send a PendingIntent. final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); final boolean offloadedBatchingSupported = adapter.isOffloadedScanBatchingSupported(); final boolean offloadedFilteringSupported = adapter.isOffloadedFilteringSupported(); // Obtain or create a PendingIntentExecutorWrapper. A static instance (obtained from a // static BluetoothLeScannerCompat singleton) is necessary as it allows to keeps // track of found devices and emulate batching and callback types if those are not // supported or a compat version was forced. BluetoothLeScannerImplOreo.PendingIntentExecutorWrapper wrapper; //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (scanner) { try { wrapper = scannerImpl.getWrapper(callbackIntent); } catch (final IllegalStateException e) { // Scanning has been stopped. return; } if (wrapper == null) { // Wrapper has not been created, or was created, but the app was then killed // and must be created again. Some information will be lost (batched devices). wrapper = new BluetoothLeScannerImplOreo.PendingIntentExecutorWrapper(offloadedBatchingSupported, offloadedFilteringSupported, filters, settings, callbackIntent); scannerImpl.addWrapper(callbackIntent, wrapper); } } // The context may change each time. Set the one time temporary context that will be used // to send PendingIntent. It will be released after the results were handled. wrapper.executor.setTemporaryContext(context); // Check what results were received and send them to PendingIntent. final List<android.bluetooth.le.ScanResult> nativeScanResults = intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT); if (nativeScanResults != null) { final ArrayList<ScanResult> results = scannerImpl.fromNativeScanResults(nativeScanResults); if (settings.getReportDelayMillis() > 0) { wrapper.handleScanResults(results); } else if (!results.isEmpty()) { final int callbackType = intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, ScanSettings.CALLBACK_TYPE_ALL_MATCHES); wrapper.handleScanResult(callbackType, results.get(0)); } } else { final int errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0); if (errorCode != 0) { wrapper.handleScanError(errorCode); } } // Release the temporary context reference, so that static executor does not hold a // reference to a context. wrapper.executor.setTemporaryContext(null); } }