package org.altbeacon.beacon.service.scanner;

import android.Manifest;
import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemClock;
import androidx.annotation.AnyThread;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;

import org.altbeacon.beacon.BeaconManager;
import org.altbeacon.beacon.logging.LogManager;
import org.altbeacon.beacon.startup.StartupBroadcastReceiver;
import org.altbeacon.bluetooth.BluetoothCrashResolver;

import java.util.Date;

@TargetApi(18)
public abstract class CycledLeScanner {
    public static final long ANDROID_N_MAX_SCAN_DURATION_MILLIS = 30 * 60 * 1000l; // 30 minutes
    private static final String TAG = "CycledLeScanner";
    private BluetoothAdapter mBluetoothAdapter;

    private long mLastScanCycleStartTime = 0l;
    private long mLastScanCycleEndTime = 0l;
    protected long mNextScanCycleStartTime = 0l;
    private long mScanCycleStopTime = 0l;
    // This is the last time this class actually commanded the OS
    // to start scanning.
    private long mCurrentScanStartTime = 0l;
    // True if the app has explicitly requested long running scans that
    // may go beyond what is normally allowed on Android N.
    private boolean mLongScanForcingEnabled = false;
    private boolean mScanning;
    protected boolean mScanningPaused;
    private boolean mScanCyclerStarted = false;
    private boolean mScanningEnabled = false;
    protected final Context mContext;
    private long mScanPeriod;
    // indicates that we decided not to turn scanning off at the end of a scan cycle (e.g. to
    // avoid doing too many scans in a limited time on Android 7.0 or because we are capable of
    // multiple detections.  If true, it indicates scanning needs to be stopped when we finish.
    private boolean mScanningLeftOn = false;
    private BroadcastReceiver mCancelAlarmOnUserSwitchBroadcastReceiver = null;

    protected long mBetweenScanPeriod;

    /**
     * Main thread handle for scheduling scan cycle tasks.
     * <p>
     * Use this to schedule deferred tasks such as the following:
     * <ul>
     *     <li>{@link #scheduleScanCycleStop()}</li>
     *     <li>{@link #scanLeDevice(Boolean) scanLeDevice(true)} from {@link #deferScanIfNeeded()}</li>
     * </ul>
     */
    @NonNull
    protected final Handler mHandler = new Handler(Looper.getMainLooper());

    /**
     * Handler to background thread for interacting with the low-level Android BLE scanner.
     * <p>
     * Use this to queue any potentially long running BLE scanner actions such as starts and stops.
     */
    @NonNull
    protected final Handler mScanHandler;

    /**
     * Worker thread hosting the internal scanner message queue.
     */
    @NonNull
    private final HandlerThread mScanThread;

    protected final BluetoothCrashResolver mBluetoothCrashResolver;
    protected final CycledLeScanCallback mCycledLeScanCallback;

    protected boolean mBackgroundFlag = false;
    protected boolean mRestartNeeded = false;

    /**
     * Flag indicating device hardware supports detecting multiple identical packets per scan.
     * <p>
     * Restarting scanning (stopping and immediately restarting) is necessary on many older Android
     * devices like the Nexus 4 and Moto G because once they detect a distinct BLE packet in a scan,
     * subsequent detections do not get a scan callback. Stopping scanning and restarting clears
     * this out, allowing subsequent detection of identical advertisements. On most newer device,
     * this is not necessary, and multiple callbacks are given for identical packets detected in
     * a single scan.
     * <p>
     * This is declared {@code volatile} because it may be set by a background scan thread while
     * we are in a method on the main thread which will end up checking it. Using this modifier
     * ensures that when we read the flag we'll always see the most recently written value. This is
     * also true for background scan threads which may be running concurrently.
     */
    private volatile boolean mDistinctPacketsDetectedPerScan = false;
    private static final long ANDROID_N_MIN_SCAN_CYCLE_MILLIS = 6000l;

    protected CycledLeScanner(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) {
        mScanPeriod = scanPeriod;
        mBetweenScanPeriod = betweenScanPeriod;
        mContext = context;
        mCycledLeScanCallback = cycledLeScanCallback;
        mBluetoothCrashResolver = crashResolver;
        mBackgroundFlag = backgroundFlag;

        mScanThread = new HandlerThread("CycledLeScannerThread");
        mScanThread.start();
        mScanHandler = new Handler(mScanThread.getLooper());
    }

    public static CycledLeScanner createScanner(Context context, long scanPeriod, long betweenScanPeriod, boolean backgroundFlag, CycledLeScanCallback cycledLeScanCallback, BluetoothCrashResolver crashResolver) {
        boolean useAndroidLScanner = false;
        boolean useAndroidOScanner = false;
        if (android.os.Build.VERSION.SDK_INT < 18) {
            LogManager.w(TAG, "Not supported prior to API 18.");
            return null;
        }

        if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            LogManager.i(TAG, "This is pre Android 5.0.  We are using old scanning APIs");
            useAndroidLScanner = false;

        }
        else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            if (BeaconManager.isAndroidLScanningDisabled()) {
                LogManager.i(TAG, "This is Android 5.0, but L scanning is disabled. We are using old scanning APIs");
                useAndroidLScanner = false;
            } else {
                LogManager.i(TAG, "This is Android 5.0.  We are using new scanning APIs");
                useAndroidLScanner = true;
            }
        }
        else {
            LogManager.i(TAG, "Using Android O scanner");
            useAndroidOScanner = true;
        }

        if (useAndroidOScanner) {
            return new CycledLeScannerForAndroidO(context, scanPeriod, betweenScanPeriod, backgroundFlag, cycledLeScanCallback, crashResolver);
        }
        else if (useAndroidLScanner) {
            return new CycledLeScannerForLollipop(context, scanPeriod, betweenScanPeriod, backgroundFlag, cycledLeScanCallback, crashResolver);
        } else {
            return new CycledLeScannerForJellyBeanMr2(context, scanPeriod, betweenScanPeriod, backgroundFlag, cycledLeScanCallback, crashResolver);
        }
    }

    /**
     * Enables the scanner to go to extra lengths to keep scans going for longer than would
     * otherwise be allowed.  Useful only for Android N and higher.
     * @param enabled
     */
    public void setLongScanForcingEnabled(boolean enabled) {
        mLongScanForcingEnabled = enabled;
    }

    /**
     * Tells the cycler the scan rate and whether it is in operating in background mode.
     * Background mode flag  is used only with the Android 5.0 scanning implementations to switch
     * between LOW_POWER_MODE vs. LOW_LATENCY_MODE
     * @param backgroundFlag
     */
    @MainThread
    public void setScanPeriods(long scanPeriod, long betweenScanPeriod, boolean backgroundFlag) {
        LogManager.d(TAG, "Set scan periods called with %s, %s Background mode must have changed.",
                scanPeriod, betweenScanPeriod);
        if (mBackgroundFlag != backgroundFlag) {
            mRestartNeeded = true;
        }
        mBackgroundFlag = backgroundFlag;
        mScanPeriod = scanPeriod;
        mBetweenScanPeriod = betweenScanPeriod;
        if (mBackgroundFlag) {
            LogManager.d(TAG, "We are in the background.  Setting wakeup alarm");
            setWakeUpAlarm();
        } else {
            LogManager.d(TAG, "We are not in the background.  Cancelling wakeup alarm");
            cancelWakeUpAlarm();
        }
        long now = SystemClock.elapsedRealtime();
        if (mNextScanCycleStartTime > now) {
            // We are waiting to start scanning.  We may need to adjust the next start time
            // only do an adjustment if we need to make it happen sooner.  Otherwise, it will
            // take effect on the next cycle.
            long proposedNextScanStartTime = (mLastScanCycleEndTime + betweenScanPeriod);
            if (proposedNextScanStartTime < mNextScanCycleStartTime) {
                mNextScanCycleStartTime = proposedNextScanStartTime;
                LogManager.i(TAG, "Adjusted nextScanStartTime to be %s",
                        new Date(mNextScanCycleStartTime - SystemClock.elapsedRealtime() + System.currentTimeMillis()));
            }
        }
        if (mScanCycleStopTime > now) {
            // we are waiting to stop scanning.  We may need to adjust the stop time
            // only do an adjustment if we need to make it happen sooner.  Otherwise, it will
            // take effect on the next cycle.
            long proposedScanStopTime = (mLastScanCycleStartTime + scanPeriod);
            if (proposedScanStopTime < mScanCycleStopTime) {
                mScanCycleStopTime = proposedScanStopTime;
                LogManager.i(TAG, "Adjusted scanStopTime to be %s", mScanCycleStopTime);
            }
        }
    }

    @MainThread
    public void start() {
        LogManager.d(TAG, "start called");
        mScanningEnabled = true;
        if (!mScanCyclerStarted) {
            scanLeDevice(true);
        } else {
            LogManager.d(TAG, "scanning already started");
        }
    }

    @MainThread
    public void stop() {
        LogManager.d(TAG, "stop called");
        mScanningEnabled = false;
        if (mScanCyclerStarted) {
            scanLeDevice(false);
            // If we have left scanning on between scan periods, now is the time to shut it off.
            if (mScanningLeftOn) {
                LogManager.d(TAG, "Stopping scanning previously left on.");
                mScanningLeftOn = false;
                try {
                    LogManager.d(TAG, "stopping bluetooth le scan");
                    finishScan();
                } catch (Exception e) {
                    LogManager.w(e, TAG, "Internal Android exception scanning for beacons");
                }
            }
        } else {
            LogManager.d(TAG, "scanning already stopped");
        }
    }

    @AnyThread
    public boolean getDistinctPacketsDetectedPerScan() {
        return mDistinctPacketsDetectedPerScan;
    }

    @AnyThread
    public void setDistinctPacketsDetectedPerScan(boolean detected) {
        mDistinctPacketsDetectedPerScan = detected;
    }

    @MainThread
    public void destroy() {
        LogManager.d(TAG, "Destroying");

        // Remove any postDelayed Runnables queued for the next scan cycle
        mHandler.removeCallbacksAndMessages(null);
        // We cannot quit the thread used by the handler until queued Runnables have been processed,
        // because the handler is what stops scanning, and we do not want scanning left on.
        // So we stop the thread using the handler, so we make sure it happens after all other
        // waiting Runnables are finished.
        mScanHandler.post(new Runnable() {
            @WorkerThread
            @Override
            public void run() {
                LogManager.d(TAG, "Quitting scan thread");
                mScanThread.quit();
            }
        });
        cleanupCancelAlarmOnUserSwitch();
    }

    protected abstract void stopScan();

    protected abstract boolean deferScanIfNeeded();

    protected abstract void startScan();

    @MainThread
    protected void scanLeDevice(final Boolean enable) {
        try {
            mScanCyclerStarted = true;
            if (getBluetoothAdapter() == null) {
                LogManager.e(TAG, "No Bluetooth adapter.  beaconService cannot scan.");
            }
            if (mScanningEnabled && enable) {
                if (deferScanIfNeeded()) {
                    return;
                }
                LogManager.d(TAG, "starting a new scan cycle");
                if (!mScanning || mScanningPaused || mRestartNeeded) {
                    mScanning = true;
                    mScanningPaused = false;
                    try {
                        if (getBluetoothAdapter() != null) {
                            if (getBluetoothAdapter().isEnabled()) {
                                if (mBluetoothCrashResolver != null && mBluetoothCrashResolver.isRecoveryInProgress()) {
                                    LogManager.w(TAG, "Skipping scan because crash recovery is in progress.");
                                } else {
                                    if (mScanningEnabled) {
                                        if (mRestartNeeded) {
                                            mRestartNeeded = false;
                                            LogManager.d(TAG, "restarting a bluetooth le scan");
                                        } else {
                                            LogManager.d(TAG, "starting a new bluetooth le scan");
                                        }
                                        try {
                                            if (android.os.Build.VERSION.SDK_INT < 23 || checkLocationPermission()) {
                                                mCurrentScanStartTime = SystemClock.elapsedRealtime();
                                                startScan();
                                            }
                                        } catch (Exception e) {
                                            LogManager.e(e, TAG, "Internal Android exception scanning for beacons");
                                        }
                                    } else {
                                        LogManager.d(TAG, "Scanning unnecessary - no monitoring or ranging active.");
                                    }
                                }
                                mLastScanCycleStartTime = SystemClock.elapsedRealtime();
                            } else {
                                LogManager.d(TAG, "Bluetooth is disabled.  Cannot scan for beacons.");
                            }
                        }
                    } catch (Exception e) {
                        LogManager.e(e, TAG, "Exception starting Bluetooth scan.  Perhaps Bluetooth is disabled or unavailable?");
                    }
                } else {
                    LogManager.d(TAG, "We are already scanning and have been for "+(
                            SystemClock.elapsedRealtime() - mCurrentScanStartTime
                            )+" millis");
                }
                mScanCycleStopTime = (SystemClock.elapsedRealtime() + mScanPeriod);
                scheduleScanCycleStop();

                LogManager.d(TAG, "Scan started");
            } else {
                LogManager.d(TAG, "disabling scan");
                mScanning = false;
                mScanCyclerStarted = false;
                stopScan();
                mCurrentScanStartTime = 0l;
                mLastScanCycleEndTime = SystemClock.elapsedRealtime();
                // Clear any queued schedule tasks as we're done scanning
                // This must be mHandler not mScanHandler.  mHandler is what does the scanning work.
                // If this is set to mScanHandler, then this can prevent a scan stop.
                mHandler.removeCallbacksAndMessages(null);
                finishScanCycle();
            }
        }
        catch (SecurityException e) {
            LogManager.w(TAG, "SecurityException working accessing bluetooth.");
        }
    }

    @MainThread
    protected void scheduleScanCycleStop() {
        // Stops scanning after a pre-defined scan period.
        long millisecondsUntilStop = mScanCycleStopTime - SystemClock.elapsedRealtime();
        if (mScanningEnabled && millisecondsUntilStop > 0) {
            LogManager.d(TAG, "Waiting to stop scan cycle for another %s milliseconds",
                    millisecondsUntilStop);
            if (mBackgroundFlag) {
                setWakeUpAlarm();
            }
            mHandler.postDelayed(new Runnable() {
                @MainThread
                @Override
                public void run() {
                    scheduleScanCycleStop();
                }
            }, millisecondsUntilStop > 1000 ? 1000 : millisecondsUntilStop);
        } else {
            finishScanCycle();
        }
    }

    protected abstract void finishScan();

    @MainThread
    private void finishScanCycle() {
        LogManager.d(TAG, "Done with scan cycle");
        try {
            mCycledLeScanCallback.onCycleEnd();
            if (mScanning) {
                if (getBluetoothAdapter() != null) {
                    if (getBluetoothAdapter().isEnabled()) {
                        // Determine if we need to restart scanning.  Restarting scanning is only
                        // needed on devices incapable of detecting multiple distinct BLE advertising
                        // packets in a single cycle, typically older Android devices (e.g. Nexus 4)
                        // On such devices, it is necessary to stop scanning and restart to detect
                        // multiple beacon packets in the same scan, allowing collection of multiple
                        // rssi measurements.  Restarting however, causes brief detection dropouts
                        // so it is best avoided.  If we know the device has detected to distinct
                        // packets in the same cycle, we will not restart scanning and just keep it
                        // going.
                        if (!mDistinctPacketsDetectedPerScan ||
                                mBetweenScanPeriod != 0 ||
                                mustStopScanToPreventAndroidNScanTimeout()) {
                            long now = SystemClock.elapsedRealtime();
                            if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
                                    mBetweenScanPeriod+mScanPeriod < ANDROID_N_MIN_SCAN_CYCLE_MILLIS &&
                                    now-mLastScanCycleStartTime < ANDROID_N_MIN_SCAN_CYCLE_MILLIS) {
                                // As of Android N, only 5 scans may be started in a 30 second period (6
                                // seconds per cycle)  otherwise they are blocked.  So we check here to see
                                // if the scan period is 6 seconds or less, and if we last stopped scanning
                                // fewer than 6 seconds ag and if so, we simply do not stop scanning
                                LogManager.d(TAG, "Not stopping scan because this is Android N and we" +
                                        " keep scanning for a minimum of 6 seconds at a time. "+
                                        "We will stop in "+(ANDROID_N_MIN_SCAN_CYCLE_MILLIS-(now-mLastScanCycleStartTime))+" millisconds.");
                                mScanningLeftOn = true;
                            }
                            else {
                                try {
                                    LogManager.d(TAG, "stopping bluetooth le scan");
                                    finishScan();
                                    mScanningLeftOn = false;
                                } catch (Exception e) {
                                    LogManager.w(e, TAG, "Internal Android exception scanning for beacons");
                                }
                            }
                        }
                        else {
                            LogManager.d(TAG, "Not stopping scanning.  Device capable of multiple indistinct detections per scan.");
                            mScanningLeftOn = true;
                        }

                        mLastScanCycleEndTime = SystemClock.elapsedRealtime();
                    } else {
                        LogManager.d(TAG, "Bluetooth is disabled.  Cannot scan for beacons.");
                        mRestartNeeded = true;
                    }
                }
                mNextScanCycleStartTime = getNextScanStartTime();
                if (mScanningEnabled) {
                    scanLeDevice(true);
                }
            }
            if (!mScanningEnabled) {
                LogManager.d(TAG, "Scanning disabled. ");
                mScanCyclerStarted = false;
                cancelWakeUpAlarm();
            }
        }
        catch (SecurityException e) {
            LogManager.w(TAG, "SecurityException working accessing bluetooth.");
        }
    }

    protected BluetoothAdapter getBluetoothAdapter() {
        try {
            if (mBluetoothAdapter == null) {
                // Initializes Bluetooth adapter.
                final BluetoothManager bluetoothManager =
                        (BluetoothManager) mContext.getApplicationContext().getSystemService(Context.BLUETOOTH_SERVICE);
                mBluetoothAdapter = bluetoothManager.getAdapter();
                if (mBluetoothAdapter == null) {
                    LogManager.w(TAG, "Failed to construct a BluetoothAdapter");
                }
            }
        }
        catch (SecurityException e) {
            // Thrown by Samsung Knox devices if bluetooth access denied for an app
            LogManager.e(TAG, "Cannot consruct bluetooth adapter.  Security Exception");
        }
        return mBluetoothAdapter;
    }


    private PendingIntent mWakeUpOperation = null;

    // In case we go into deep sleep, we will set up a wakeup alarm when in the background to kickoff
    // off the scan cycle again
    protected void setWakeUpAlarm() {
        // wake up time will be the maximum of 5 minutes, the scan period, the between scan period
        long milliseconds = 1000l * 60 * 5; /* five minutes */
        if (milliseconds < mBetweenScanPeriod) {
            milliseconds = mBetweenScanPeriod;
        }
        if (milliseconds < mScanPeriod) {
            milliseconds = mScanPeriod;
        }
        AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
        alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + milliseconds, getWakeUpOperation());
        LogManager.d(TAG, "Set a wakeup alarm to go off in %s ms: %s", milliseconds, getWakeUpOperation());
        cancelAlarmOnUserSwitch();
    }

    // Added to prevent crash on switching users.  See #876
    protected void cancelAlarmOnUserSwitch() {
        if (mCancelAlarmOnUserSwitchBroadcastReceiver == null) {
            IntentFilter filter = new IntentFilter();
            filter.addAction( Intent.ACTION_USER_BACKGROUND );
            filter.addAction( Intent.ACTION_USER_FOREGROUND );

            mCancelAlarmOnUserSwitchBroadcastReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    LogManager.w(TAG, "User switch detected.  Cancelling alarm to prevent potential crash.");
                    cancelWakeUpAlarm();
                }
            };
            mContext.registerReceiver(mCancelAlarmOnUserSwitchBroadcastReceiver, filter);
        }
    }
    protected void cleanupCancelAlarmOnUserSwitch() {
        if (mCancelAlarmOnUserSwitchBroadcastReceiver != null) {
            try {
                mContext.unregisterReceiver(mCancelAlarmOnUserSwitchBroadcastReceiver);
            }
            catch (IllegalArgumentException e) {} // thrown if OS does not think it was registered
            mCancelAlarmOnUserSwitchBroadcastReceiver = null;
        }
    }


    protected PendingIntent getWakeUpOperation() {
        if (mWakeUpOperation == null) {
            Intent wakeupIntent = new Intent(mContext, StartupBroadcastReceiver.class);
            wakeupIntent.putExtra("wakeup", true);
            mWakeUpOperation = PendingIntent.getBroadcast(mContext, 0, wakeupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        }
        return mWakeUpOperation;
    }

    protected void cancelWakeUpAlarm() {
        LogManager.d(TAG, "cancel wakeup alarm: %s", mWakeUpOperation);
        // We actually don't cancel the wakup alarm... we just reschedule for a long time in the
        // future.  This is to get around a limit on 500 alarms you can start per app on Samsung
        // devices.
        long milliseconds = Long.MAX_VALUE; // 2.9 million years from now
        AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
        alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, milliseconds, getWakeUpOperation());
        LogManager.d(TAG, "Set a wakeup alarm to go off in %s ms: %s", milliseconds - SystemClock.elapsedRealtime(), getWakeUpOperation());

    }

    private long getNextScanStartTime() {
        // Because many apps may use this library on the same device, we want to try to synchronize
        // scanning as much as possible in order to save battery.  Therefore, we will set the scan
        // intervals to be on a predictable interval using a modulus of the system time.  This may
        // cause scans to start a little earlier than otherwise, but it should be acceptable.
        // This way, if multiple apps on the device are using the default scan periods, then they
        // will all be doing scans at the same time, thereby saving battery when none are scanning.
        // This, of course, won't help at all if people set custom scan periods.  But since most
        // people accept the defaults, this will likely have a positive effect.
        if (mBetweenScanPeriod == 0) {
            return SystemClock.elapsedRealtime();
        }
        long fullScanCycle = mScanPeriod + mBetweenScanPeriod;
        long normalizedBetweenScanPeriod = mBetweenScanPeriod-(SystemClock.elapsedRealtime() % fullScanCycle);
        LogManager.d(TAG, "Normalizing between scan period from %s to %s", mBetweenScanPeriod,
                normalizedBetweenScanPeriod);

        return SystemClock.elapsedRealtime()+normalizedBetweenScanPeriod;
    }

    private boolean checkLocationPermission() {
        return checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION) || checkPermission(Manifest.permission.ACCESS_FINE_LOCATION);
    }

    private boolean checkPermission(final String permission) {
        return mContext.checkPermission(permission, android.os.Process.myPid(), android.os.Process.myUid()) == PackageManager.PERMISSION_GRANTED;
    }

    /**
     * On Android N and later, a scan that runs for more than 30 minutes will be automatically
     * stopped by the OS and converted to an "opportunistic" scan, meaning that they will only yield
     * detections if another app is scanning.  This is inteneded to save battery.  This can be
     * prevented by stopping scanning and restarting.  This method returns true if:
     *   * this is Android N or later
     *   * we are close to the 30 minute boundary since the last scan started
     *   * The app developer has explicitly enabled long-running scans
     * @return true if we must stop scanning to prevent
     */
    private boolean mustStopScanToPreventAndroidNScanTimeout() {
        long timeOfNextScanCycleEnd = SystemClock.elapsedRealtime() +  mBetweenScanPeriod +
                mScanPeriod;
        boolean timeoutAtRisk = android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
                mCurrentScanStartTime > 0 &&
                (timeOfNextScanCycleEnd - mCurrentScanStartTime > ANDROID_N_MAX_SCAN_DURATION_MILLIS);

        if (timeoutAtRisk) {
            LogManager.d(TAG, "The next scan cycle would go over the Android N max duration.");
            if  (mLongScanForcingEnabled) {
                LogManager.d(TAG, "Stopping scan to prevent Android N scan timeout.");
                return true;
            }
            else {
                LogManager.w(TAG, "Allowing a long running scan to be stopped by the OS.  To " +
                        "prevent this, set longScanForcingEnabled in the AndroidBeaconLibrary.");
            }
        }
        return false;
    }
}