/*
 * Copyright 2019 Fitbit, Inc. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

package com.fitbit.bluetooth.fbgatt;

import com.fitbit.bluetooth.fbgatt.exception.BitGattStartException;
import com.fitbit.bluetooth.fbgatt.tx.GattClientDiscoverServicesTransaction;
import com.fitbit.bluetooth.fbgatt.tx.GattConnectTransaction;
import com.fitbit.bluetooth.fbgatt.tx.GattDisconnectTransaction;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import timber.log.Timber;

/**
 * This scanner is designed to remain connected to one or more bluetooth peripherals through
 * bluetooth toggles, etc ...  Changing the filters will internally stop and start the scanner.
 * <p>
 * This is a common use-case for wearable peripherals, and for power reasons it is preferred to
 * remain connected rather than to continuously scan and connect.
 * <p>
 * Please see the README.md for more detailed instructions
 * <p>
 * Created by iowens on 6/10/19.
 */
@SuppressWarnings("unused") // API Method
public class AlwaysConnectedScanner implements FitbitGatt.FitbitGattCallback {
    /**
     * Time between high priority scans
     */
    private static final long MIN_TIME_BETWEEN_LOW_LATENCY_SCANS = TimeUnit.MINUTES.toMillis(5);
    /**
     * The maximum amount of time allowed for a high-priority scan, there is no reason that any peripheral
     * should require more than ten seconds of high priority scanning
     */
    private static final long MAX_TIME_FOR_HIGH_PRIORITY_SCAN = TimeUnit.SECONDS.toMillis(10);
    /**
     * Whether this is test mode or not
     */
    private boolean testMode;
    /**
     * Whether we should keep looking even after a device connects ( we will know by ACL )
     */
    private boolean shouldKeepLooking;
    /**
     * How many devices should we be looking for, if less than 1 then this feature is disabled
     * disabled and the system will keep scanning regardless of shouldKeepLooking
     */
    private int numberOfExpectedDevices;
    /**
     * The number of devices that we have actually matched and connected
     */
    private AtomicInteger numberOfMatchingConnectedDevices = new AtomicInteger(0);
    /**
     * The pointer to the peripheral scanner
     */
    private @Nullable
    PeripheralScanner scanner;
    /**
     * The scan filters to find the devices we are looking for
     */
    private CopyOnWriteArrayList<ScanFilter> scanFilters = new CopyOnWriteArrayList<>();
    /**
     * The connected scanner listeners
     */
    private CopyOnWriteArrayList<AlwaysConnectedScannerListener> listeners = new CopyOnWriteArrayList<>();
    /**
     * Whether the scanner is enabled or not
     */
    private AtomicBoolean isScannerEnabled = new AtomicBoolean(false);
    /**
     * Will indicate whether a high priority scan can be performed
     */
    private AtomicBoolean canStartHighPriorityScan = new AtomicBoolean(true);
    /**
     * Member for a main handler for scheduling
     */
    private Handler mainHandlerForScheduling;
    /**
     * Test mode high priority scan time, shouldn't be used for production purposes
     */
    @VisibleForTesting
    static final long MAX_TIME_FOR_TEST_MODE_HIGH_PRIORITY_SCAN = TimeUnit.SECONDS.toMillis(2);

    /**
     * Only here for mocking purposes
     */

    @VisibleForTesting
    AlwaysConnectedScanner(){

    }

    /**
     * Will set the initial expectation for how this scanner should operate when devices are found
     *
     * @param numberOfExpectedDevices The number of expected devices
     * @param shouldKeepLooking       Whether to keep looking after one of the devices is found
     * @param looper                  The looper for scheduling events
     */
    @SuppressWarnings("unused")
    // API Method
    AlwaysConnectedScanner(int numberOfExpectedDevices, boolean shouldKeepLooking, Looper looper) {
        this(numberOfExpectedDevices, shouldKeepLooking, false, looper);
    }

    private AlwaysConnectedScanner(int numberOfExpectedDevices, boolean shouldKeepLooking, boolean testMode, Looper looper) {
        this.numberOfExpectedDevices = numberOfExpectedDevices;
        this.shouldKeepLooking = shouldKeepLooking;
        this.testMode = testMode;
        mainHandlerForScheduling = new Handler(looper);
    }

    @VisibleForTesting
    void setHandler(Handler hand) {
        mainHandlerForScheduling = hand;
    }

    /**
     * We need to be able to do this for testing
     */
    @VisibleForTesting
    void restartCanStart() {
        canStartHighPriorityScan.set(true);
    }

    /**
     * Will register a listener for always connected events if not already registered, if the instance
     * is already registered, then it will be ignored
     *
     * @param listener The always connected scanner listener for callbacks
     */
    @SuppressWarnings("unused") // API Method
    public void registerAlwaysConnectedScannerListener(AlwaysConnectedScannerListener listener) {
        if (!listeners.contains(listener)) {
            listeners.add(listener);
        }
    }

    /**
     * Will unregister an always connected listener
     *
     * @param listener The always connected scanner listener for callbacks
     */
    @SuppressWarnings("unused") // API Method
    public void unregisterAlwaysConnectedScannerListener(AlwaysConnectedScannerListener listener) {
        listeners.remove(listener);
    }

    /**
     * Will fetch the number of expected devices
     *
     * @return the number of expected devices
     */
    @SuppressWarnings("unused") // API Method
    public int getNumberOfExpectedDevices() {
        return numberOfExpectedDevices;
    }

    /**
     * Will establish whether this scanner should keep looking for matches
     *
     * @return whether to keep looking once a single matching device is connected
     */
    @SuppressWarnings("unused") // API Method
    public boolean shouldKeepLooking() {
        return shouldKeepLooking;
    }

    /**
     * Will set whether the scanner should keep looking when device is connected
     *
     * @param shouldKeepLooking true if the scanner should
     */
    @SuppressWarnings("unused") // API Method
    public void setShouldKeepLooking(boolean shouldKeepLooking) {
        this.shouldKeepLooking = shouldKeepLooking;
    }

    /**
     * Will return the state of this scanner, as to whether it is in operation or not
     *
     * @return true if this scanner is enabled, false if it is not
     */
    @SuppressWarnings("unused") // API Method
    public boolean isAlwaysConnectedScannerEnabled() {
        return isScannerEnabled.get();
    }

    /**
     * Will set the number of expected devices, will be picked up once the next set of scans begin
     *
     * @param numberOfExpectedDevices The number of expected devices
     */
    @SuppressWarnings("unused") // API Method
    public void setNumberOfExpectedDevices(int numberOfExpectedDevices) {
        this.numberOfExpectedDevices = numberOfExpectedDevices;
    }

    /**
     * Will indicate whether the always connected scanner is in the test mode or not
     *
     * @return true if in test mode, false if not
     */
    @SuppressWarnings("unused")
    // API Method
    boolean isInTestMode() {
        return testMode;
    }

    /**
     * Will add all provided scan filters, intended to only be used internally
     *
     * @param filters The list of filters to be added
     */
    void setScanFilters(List<ScanFilter> filters) {
        this.scanFilters.addAll(filters);
    }

    /**
     * Will set the system up in test mode
     *
     * @param testMode true for test mode, false if not
     */
    void setTestMode(boolean testMode) {
        this.testMode = testMode;
    }

    /**
     * Start the scanner with filters
     *
     * @param context The android context
     * @param filters The scan filters to use
     * @return True if started successfully
     */
    @SuppressWarnings("unused") // API Method
    public boolean startWithFilters(@NonNull Context context, @NonNull List<ScanFilter> filters) {
        scanFilters.addAll(filters);
        return start(context);
    }

    /**
     * Will add a list of filters to the existing set of filters, will not check for duplicates
     * and will not clear the existing filter set.  This method will only change the current set of
     * filters once every 30s so if you call this method multiple times, the changes will be spread
     * over 30s x n calls.
     *
     * @param context The android context
     * @param filters The list of filters
     */
    public synchronized void addScanFilters(@NonNull Context context, @NonNull List<ScanFilter> filters) {
        scanFilters.addAll(filters);
        if (FitbitGatt.getInstance().getPeripheralScanner() != null) {
            FitbitGatt.getInstance().getPeripheralScanner().setScanFilters(scanFilters);
        }
        // let's only change this once per scan too much warn interval
        mainHandlerForScheduling.postDelayed(() -> {
            FitbitGatt.getInstance().getPeripheralScanner().cancelPendingIntentBasedBackgroundScan();
            FitbitGatt.getInstance().getPeripheralScanner().startPendingIntentBasedBackgroundScan(scanFilters, context);
        }, PeripheralScanner.SCAN_TOO_MUCH_WARN_INTERVAL);
    }

    /**
     * Will append a new scan filter to the set of scan filters, will restart the internal
     * background scanner to ensure that the new filter is picked up.  Since we want to always
     * avoid the scan-too-much no-op for our scanner, we will only change this once per 30s.
     * <p>
     * This method will only change the current set of
     * filters once every 30s so if you call this method multiple times, the changes will be spread
     * over 30s x n calls.
     *
     * @param context    The android context
     * @param scanFilter The new scan filter to add
     */

    public synchronized void addScanFilter(@NonNull Context context, @NonNull ScanFilter scanFilter) {
        if (!scanFilters.contains(scanFilter)) {
            scanFilters.add(scanFilter);
        }
        if (FitbitGatt.getInstance().getPeripheralScanner() != null) {
            FitbitGatt.getInstance().getPeripheralScanner().setScanFilters(scanFilters);
        }
        // let's only change this once per scan too much warn interval
        mainHandlerForScheduling.postDelayed(() -> {
            FitbitGatt.getInstance().getPeripheralScanner().cancelPendingIntentBasedBackgroundScan();
            FitbitGatt.getInstance().getPeripheralScanner().startPendingIntentBasedBackgroundScan(scanFilters, context);
        }, PeripheralScanner.SCAN_TOO_MUCH_WARN_INTERVAL);
    }

    /**
     * Will remove a scan filter from the set of scan filters.
     * <p>
     * This method will only change the current set of
     * filters once every 30s so if you call this method multiple times, the changes will be spread
     * over 30s x n calls.
     *
     * @param context    The android context
     * @param scanFilter The scan filter to remove
     */

    public synchronized void removeScanFilter(@NonNull Context context, @NonNull ScanFilter scanFilter) {
        if (!scanFilters.contains(scanFilter)) {
            scanFilters.remove(scanFilter);
        }
        if (FitbitGatt.getInstance().getPeripheralScanner() != null) {
            FitbitGatt.getInstance().getPeripheralScanner().setScanFilters(scanFilters);
        }
        // let's only change this once per scan too much warn interval
        mainHandlerForScheduling.postDelayed(() -> {
            FitbitGatt.getInstance().getPeripheralScanner().cancelPendingIntentBasedBackgroundScan();
            FitbitGatt.getInstance().getPeripheralScanner().startPendingIntentBasedBackgroundScan(scanFilters, context);
        }, PeripheralScanner.SCAN_TOO_MUCH_WARN_INTERVAL);
    }

    /**
     * Will start finding and connecting to devices that match the filters.  Will not deal with matching
     * presently connected devices at the time of the start call.
     *
     * @param context The android application context
     * @return true if the scanner can start searching, false if the raw scanner is in use or the filters are insufficient
     */

    public boolean start(@NonNull Context context) {
        scanner = FitbitGatt.getInstance().getPeripheralScanner();
        if (scanner == null) {
            Timber.w("The scanner isn't set up yet, did you call FitbitGatt#start(...)?");
            return false;
        }
        if (scanner.isScanning() || scanner.isPeriodicalScanEnabled() || scanner.isPendingIntentScanning()) {
            Timber.w("You can not start an always connected scanner while using the regular scanner");
            return false;
        }
        if (scanFilters.isEmpty()) {
            Timber.w("You can not start a scanner with no filters");
            return false;
        }
        if (isScannerEnabled.get()) {
            Timber.w("The scanner was already enabled, no need to call this again");
            return false;
        }
        FitbitGatt.getInstance().registerGattEventListener(this);
        return startBackgroundScanning(context);
    }

    private boolean startBackgroundScanning(@NonNull Context context) {
        // we only want to start a pending intent scan if this is > Oreo MR1
        // otherwise we'll use the pending intent scan
        PeripheralScanner peripheralScanner = FitbitGatt.getInstance().getPeripheralScanner();
        if (peripheralScanner == null) {
            Timber.w("The scanner isn't set up yet, did you call FitbitGatt#start(...)?");
            return false;
        }
        if (FitbitGatt.atLeastSDK(Build.VERSION_CODES.O_MR1)) {
            peripheralScanner.startPendingIntentBasedBackgroundScan(scanFilters, context);
        } else {
            // this will start a low intensity scan immediately
            boolean didStart = peripheralScanner.startPeriodicScan(context);
            if (!didStart) {
                isScannerEnabled.set(false);
                return false;
            }
        }
        isScannerEnabled.set(true);
        return true;
    }

    private void stopScanningUntilDisconnectionEvent(Context context) {
        if (isAlwaysConnectedScannerEnabled()) {
            PeripheralScanner peripheralScanner = FitbitGatt.getInstance().getPeripheralScanner();
            if (peripheralScanner != null) {
                if (FitbitGatt.atLeastSDK(Build.VERSION_CODES.O_MR1)) {
                    peripheralScanner.cancelPendingIntentBasedBackgroundScan();
                } else {
                    peripheralScanner.cancelPeriodicalScan(context);
                }
                peripheralScanner.cancelHighPriorityScan(context);
                peripheralScanner.cancelScan(context);
                Timber.v("Stopped all of the scanning");
            } else {
                Timber.w("The scanner isn't set up yet, did you call FitbitGatt#start(...)?");
            }
        } else {
            Timber.w("The scanner was disabled so we will not continue");
        }
    }

    private boolean continueBackgroundScanning(@NonNull Context context) {
        if (isAlwaysConnectedScannerEnabled()) {
            PeripheralScanner peripheralScanner = FitbitGatt.getInstance().getPeripheralScanner();
            if (peripheralScanner != null) {
                if (FitbitGatt.atLeastSDK(Build.VERSION_CODES.O_MR1)) {
                    return peripheralScanner.startPendingIntentBasedBackgroundScan(scanFilters, context);
                } else {
                    return peripheralScanner.startPeriodicScan(context);
                }
            } else {
                Timber.w("The scanner isn't set up yet, did you call FitbitGatt#start(...)?");
                return false;
            }
        } else {
            Timber.w("The scanner was disabled so we will not continue");
            return false;
        }
    }

    /**
     * This should be performed due to some user interaction that requires as near a real-time
     * interaction with the peripheral as possible, can only be performed when the screen is on
     * and only once every 5 minutes.
     *
     * @return false if high priority scan was not started
     */
    @SuppressWarnings({"unused", "WeakerAccess"}) // API Method
    public boolean startHighPriorityScan(@NonNull Context context) {
        if (!canStartHighPriorityScan.get()) {
            Timber.w("You can't start a high priority scan right now");
            return false;
        }
        if (testMode) {
            return startScanIfPossible(context);
        } else {
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            if (pm == null) {
                Timber.i("The power manager was null");
                return false;
            }
            if (pm.isInteractive()) {
                return startScanIfPossible(context);
            } else {
                Timber.d("The screen isn't on, can't perform a high priority scan");
                return false;
            }
        }
    }

    private boolean startScanIfPossible(@NonNull Context context) {
        PeripheralScanner peripheralScanner = FitbitGatt.getInstance().getPeripheralScanner();
        if (peripheralScanner == null) {
            Timber.w("The scanner isn't set up yet, did you call FitbitGatt#start(...)?");
            return false;
        }
        boolean didStart = peripheralScanner.startHighPriorityScan(context);
        if (!didStart) {
            Timber.w("Couldn't start a high priority scan");
            return false;
        }
        if (testMode) {
            mainHandlerForScheduling.postDelayed(() -> peripheralScanner.cancelHighPriorityScan(context), MAX_TIME_FOR_TEST_MODE_HIGH_PRIORITY_SCAN);
        } else {
            mainHandlerForScheduling.postDelayed(() -> peripheralScanner.cancelHighPriorityScan(context), MAX_TIME_FOR_HIGH_PRIORITY_SCAN);
        }
        Timber.v("Will start high priority scanning, but will cancel in %dms", MAX_TIME_FOR_HIGH_PRIORITY_SCAN);
        mainHandlerForScheduling.postDelayed(() -> canStartHighPriorityScan.set(true), MIN_TIME_BETWEEN_LOW_LATENCY_SCANS);
        canStartHighPriorityScan.set(false);
        return true;
    }

    /**
     * Will stop the operation of this scanner
     */

    public void stop(@NonNull Context context) {
        // will stop the always connected scanner
        FitbitGatt.getInstance().unregisterGattEventListener(this);
        PeripheralScanner peripheralScanner = FitbitGatt.getInstance().getPeripheralScanner();
        if (peripheralScanner != null) {
            peripheralScanner.cancelScan(context);
            peripheralScanner.cancelPendingIntentBasedBackgroundScan();
            peripheralScanner.cancelPeriodicalScan(context);
        } else {
            Timber.w("The scanner isn't set up yet, did you call FitbitGatt#start(...)?");
            return;
        }
        isScannerEnabled.set(false);
    }

    /* ---------------------------------- General GATT callbacks ------------------------------ */

    @SuppressLint("VisibleForTests")
    @Override
    public void onBluetoothPeripheralDiscovered(GattConnection connection) {
        // we've discovered a device that is matching, let's try to connect
        Context appContext = FitbitGatt.getInstance().getAppContext();
        if (appContext != null) {
            // well, we will try to connect and evaluate the device against our filters.  If it
            // matches, then we will leave it connected, otherwise we will release the client_if
            // if we are in mock mode, we should finish the connection mock
            if (testMode) {
                connection.setMockMode(true);
                connection.setState(GattState.CONNECTED);
                int matchedDevices = numberOfMatchingConnectedDevices.incrementAndGet();
                // if the number of expected matched devices has been hit and continue scanning is
                // not enabled, then the scans will be cancelled until the connected devices
                // drops below the expected number
                if (numberOfExpectedDevices > 0 && !shouldKeepLooking && matchedDevices >= numberOfExpectedDevices) {
                    stopScanningUntilDisconnectionEvent(appContext);
                }
                Timber.v("Connection %s matches, calling listeners", connection);
                for (AlwaysConnectedScannerListener listener : listeners) {
                    listener.onPeripheralConnected(connection);
                }
                Timber.v("Matched devices connected: %d", matchedDevices);
            } else {
                GattConnectTransaction tx = new GattConnectTransaction(connection, GattState.CONNECTED);
                connection.runTx(tx, result -> {
                    if (result.getResultStatus().equals(TransactionResult.TransactionResultStatus.SUCCESS)) {
                        Timber.v("Connected, now discovering");
                        // we will go ahead and discover, since we are going through the hassle of connecting
                        // it doesn't make sense to hand the caller a connection that isn't immediately usable
                        // shouldn't be a big deal if already discovered, will return from cache
                        GattClientDiscoverServicesTransaction discover = new GattClientDiscoverServicesTransaction(connection, GattState.DISCOVERY_SUCCESS);
                        connection.runTx(discover, result1 -> {
                            if (result1.getResultStatus().equals(TransactionResult.TransactionResultStatus.SUCCESS)) {
                                // let's match it against filters, this could be expensive, so let's do it on the
                                // connection thread
                                connection.getClientTransactionQueueController().queueTransaction(() -> {
                                    for (ScanFilter filter : scanFilters) {
                                        if (doesConnectionMatchFilter(connection, filter)) {
                                            int matchedDevices = numberOfMatchingConnectedDevices.incrementAndGet();
                                            // if the number of expected matched devices has been hit and continue scanning is
                                            // not enabled, then the scans will be cancelled until the connected devices
                                            // drops below the expected number
                                            if (numberOfExpectedDevices > 0 && !shouldKeepLooking && matchedDevices >= numberOfExpectedDevices) {
                                                stopScanningUntilDisconnectionEvent(appContext);
                                            }
                                            Timber.v("Connection %s matches, calling listeners", connection);
                                            for (AlwaysConnectedScannerListener listener : listeners) {
                                                listener.onPeripheralConnected(connection);
                                            }
                                            Timber.v("Matched devices connected: %d", matchedDevices);
                                            return;
                                        }
                                    }
                                    // if there is no match, we can remain silent about it
                                    GattDisconnectTransaction disconnect = new GattDisconnectTransaction(connection, GattState.DISCONNECTED);
                                    connection.runTx(disconnect, result11 -> Timber.v("No match, tried to disconnect with result : %s", result11));
                                });
                            }
                        });
                    } else {
                        for (AlwaysConnectedScannerListener listener : listeners) {
                            listener.onPeripheralConnectionError(result);
                        }
                    }
                });
            }
        }
    }

    @Override
    public void onBluetoothPeripheralDisconnected(GattConnection connection) {
        // realistically we don't need to do anything here except decrement matching devices
        connection.getClientTransactionQueueController().queueTransaction(() -> {
            for (ScanFilter filter : scanFilters) {
                if (doesConnectionMatchFilter(connection, filter)) {
                    int matchedDevices = numberOfMatchingConnectedDevices.get();
                    if (matchedDevices > 0) {
                        matchedDevices = numberOfMatchingConnectedDevices.decrementAndGet();
                        Timber.v("Matched devices decremented to: %d", matchedDevices);
                    }
                }
            }
            // if something disconnected and we are configured to continue to match, we will
            // continue scanning, if we are already scanning this will do nothing.
            int matchingDevices = numberOfMatchingConnectedDevices.get();
            if (numberOfExpectedDevices > 1 && matchingDevices < numberOfExpectedDevices) {
                Timber.v("Dropped below the number of expected devices, so we should start scanning again");
                if (FitbitGatt.getInstance().getAppContext() != null) {
                    boolean didStart = continueBackgroundScanning(FitbitGatt.getInstance().getAppContext());
                    if (!didStart) {
                        Timber.w("Always connected scanner tried to start scanning but failed");
                    }
                }
            }
        });
    }

    @Override
    public void onScanStarted() {
        // that's cool
        //no-op
    }

    @Override
    public void onScanStopped() {
        Timber.i("Always connected scanner scan stopped");
    }

    @Override
    public void onScannerInitError(BitGattStartException error) {
        //no-op
    }

    @Override
    public void onPendingIntentScanStopped() {
        // bluetooth on means that we should restart our pending intent scan if it is not already
        // in operation
        if (FitbitGatt.getInstance().getAppContext() != null) {
            startBackgroundScanning(FitbitGatt.getInstance().getAppContext());
        } else {
            Timber.w("Couldn't resume scans because context is null");
        }
    }

    @Override
    public void onPendingIntentScanStarted() {
        // that's cool too
    }

    @Override
    public void onBluetoothOff() {
        // bluetooth off will stop all scans, but will not reset the 30s scan count
        numberOfMatchingConnectedDevices.set(0);
        isScannerEnabled.set(false);
        canStartHighPriorityScan.set(true);
    }

    @Override
    public void onBluetoothOn() {
        // bluetooth on means that we should restart our pending intent scan if it is not already
        // in operation
        Timber.i("If BT is coming on you can assume that all of your scan state has been stopped");
    }

    @Override
    public void onBluetoothTurningOn() {

    }

    @Override
    public void onBluetoothTurningOff() {

    }

    @Override
    public void onGattServerStarted(GattServerConnection serverConnection) {
        //no-op
    }

    @Override
    public void onGattServerStartError(BitGattStartException error) {
        //no-op
    }

    @Override
    public void onGattClientStarted() {
        //no-op
    }

    @Override
    public void onGattClientStartError(BitGattStartException error) {
        //no-op
    }

    /**
     * Will determine if the cached scan result matches the provided scan filter
     *
     * @param connection The gatt connection
     * @param filter     The scan filter
     * @return true if it's a match, false if not
     */
    @TargetApi(26)
    private boolean doesConnectionMatchFilter(@NonNull GattConnection connection, ScanFilter filter) {
        ScanRecord record = connection.getDevice().getScanRecord();
        if (record == null) {
            return false;
        }
        ScanResult result;
        if (FitbitGatt.atLeastSDK(Build.VERSION_CODES.O)) {
            result = new ScanResult(connection.getDevice().device,
                0x00, 1, 0x00, 0xFF, 127,
                connection.getDevice().getRssi(), record.getTxPowerLevel(), record, 0);
        } else {
            result = new ScanResult(connection.getDevice().device, record, connection.getDevice().getRssi(),
                0);
        }
        return filter.matches(result);
    }
}