content: 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 = 10 * 60 * 1000l; // 10 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;
  
      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");
          }
      }
  
      @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() || Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                                  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()  || Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                          if (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 {
                              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 | PendingIntent.FLAG_IMMUTABLE);
          }
          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() {
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && checkPermission(Manifest.permission.BLUETOOTH_SCAN)) {
              return true;
          }
  
          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;
      }
  }
  