content: package org.altbeacon.bluetooth;
  
  import android.Manifest;
  import android.annotation.SuppressLint;
  import android.app.NotificationChannel;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.app.TaskStackBuilder;
  import android.app.job.JobScheduler;
  import android.bluetooth.BluetoothAdapter;
  import android.bluetooth.BluetoothManager;
  import android.bluetooth.le.AdvertiseCallback;
  import android.bluetooth.le.AdvertiseData;
  import android.bluetooth.le.AdvertiseSettings;
  import android.bluetooth.le.BluetoothLeAdvertiser;
  import android.bluetooth.le.BluetoothLeScanner;
  import android.bluetooth.le.ScanCallback;
  import android.bluetooth.le.ScanResult;
  import android.bluetooth.le.AdvertiseSettings.Builder;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.os.Build;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.PersistableBundle;
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.annotation.RequiresApi;
  import androidx.core.app.ActivityCompat;
  import androidx.core.app.NotificationCompat;
  import java.util.List;
  import org.altbeacon.beacon.logging.LogManager;
  
  /**
   *
   * Utility class for checking the health of the bluetooth stack on the device by running two kinds
   * of tests: scanning and transmitting.  The class looks for specific failure codes from these
   * tests to determine if the bluetooth stack is in a bad state and if so, optionally cycle power to
   * bluetooth to try and fix the problem.  This is known to work well on some Android devices.
   *
   * The tests may be called directly, or set up to run automatically approximately every 15 minutes.
   * To set up in an automated way:
   *
   * <code>
   *   BluetoothMedic medic = BluetoothMedic.getInstance();
   *   medic.enablePowerCycleOnFailures(context);
   *   medic.enablePeriodicTests(context, BluetoothMedic.SCAN_TEST | BluetoothMedic.TRANSMIT_TEST);
   * </code>
   *
   * To set up in a manual way:
   *
   * <code>
   *   BluetoothMedic medic = BluetoothMedic.getInstance();
   *   medic.enablePowerCycleOnFailures(context);
   *   if (!medic.runScanTest(context)) {
   *     // Bluetooth stack is in a bad state
   *   }
   *   if (!medic.runTransmitterTest(context)) {
   *     // Bluetooth stack is in a bad state
   *   }
   *
   */
  
  @SuppressWarnings("javadoc")
  public class BluetoothMedic {
  
      /**
       * Indicates that no test should be run by the BluetoothTestJob
       */
      @SuppressWarnings("WeakerAccess")
      public static final int NO_TEST = 0;
      /**
       * Indicates that the transmitter test should be run by the BluetoothTestJob
       */
      @SuppressWarnings("WeakerAccess")
      public static final int TRANSMIT_TEST = 2;
      /**
       * Indicates that the bluetooth scan test should be run by the BluetoothTestJob
       */
      @SuppressWarnings("WeakerAccess")
      public static final int SCAN_TEST = 1;
      private static final String TAG = BluetoothMedic.class.getSimpleName();
      @Nullable
      private BluetoothAdapter mAdapter;
      @NonNull
      private Handler mHandler = new Handler(Looper.getMainLooper());
      private int mTestType = 0;
      @Nullable
      private Boolean mTransmitterTestResult = null;
      @Nullable
      private Boolean mScanTestResult = null;
      private boolean mNotificationsEnabled = false;
      private boolean mNotificationChannelCreated = false;
      private int mNotificationIcon = 0;
      private long mLastBluetoothPowerCycleTime = 0L;
      private static final long MIN_MILLIS_BETWEEN_BLUETOOTH_POWER_CYCLES = 60000L;
      @Nullable
      private static BluetoothMedic sInstance;
      private boolean powerCycleOnFailureEnabled = false;
      @Nullable
      private Context mContext = null;
  
      public void processMedicAction(String action, int errorCode) {
          if (powerCycleOnFailureEnabled) {
              if(action.equalsIgnoreCase("onScanFailed")) {
                  if(errorCode == 2) {
                      LogManager.i(BluetoothMedic.TAG,
                              "Detected a SCAN_FAILED_APPLICATION_REGISTRATION_FAILED.  We need to cycle bluetooth to recover");
                      BluetoothMedic.this.sendScreenNotification( "scan failed",
                              "Power cycling bluetooth");
                      if(!BluetoothMedic.this.cycleBluetoothIfNotTooSoon()) {
                          BluetoothMedic.this.sendScreenNotification( "scan failed", "" +
                                  "Cannot power cycle bluetooth again");
                      }
                  }
              } else if(action.equalsIgnoreCase("onStartFailed")) {
                  if(errorCode == 4) {
                      LogManager.i(BluetoothMedic.TAG, "advertising failed: Expected failure.  Power cycling.");
                      BluetoothMedic.this.sendScreenNotification("advertising failed",
                              "Expected failure.  Power cycling.");
                      if(!BluetoothMedic.this.cycleBluetoothIfNotTooSoon()) {
                          BluetoothMedic.this.sendScreenNotification("advertising failed",
                                  "Cannot power cycle bluetooth again");
                      }
                  }
              } else {
                  LogManager.d(BluetoothMedic.TAG, "Unknown event.");
              }
          }
      }
  
      /**
       * Get a singleton instance of the BluetoothMedic
       * @return
       */
      public static BluetoothMedic getInstance() {
          if(sInstance == null) {
              sInstance = new BluetoothMedic();
          }
          return sInstance;
      }
  
      private BluetoothMedic() {
      }
  
      @RequiresApi(21)
      private void initializeWithContext(Context context) {
          if (this.mAdapter == null) {
              BluetoothManager manager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
              if(manager == null) {
                  throw new NullPointerException("Cannot get BluetoothManager");
              } else {
                  this.mAdapter = manager.getAdapter();
              }
          }
      }
  
  
  
      /**
       * If set to true, bluetooth will be power cycled on any tests run that determine bluetooth is
       * in a bad state.  This only works on Anroid 4.3-12.x devices, as the ability to power cycle
       * Bluetooth has been blocked from 3rd party apps on Android 13.
       *
       * @param context
       * @deprecated See legacyEnablePowerCycleOnFailures(Context context)
       */
      @SuppressWarnings("unused")
      @RequiresApi(21)
      @Deprecated
      public void enablePowerCycleOnFailures(Context context) {
          legacyEnablePowerCycleOnFailures(context);
      }
  
  
      /**
       * If set to true, bluetooth will be power cycled on any tests run that determine bluetooth is
       * in a bad state.  This only works on Anroid 4.3-12.x devices, as the ability to power cycle
       * Bluetooth has been blocked from 3rd party apps on Android 13.
       *
       * @param context
       */
      @SuppressWarnings("unused")
      @RequiresApi(21)
      public void legacyEnablePowerCycleOnFailures(Context context) {
          mContext = context.getApplicationContext();
          powerCycleOnFailureEnabled = true;
          initializeWithContext(context);
          LogManager.d(TAG,
                  "Medic monitoring for transmission and scan failure notifications");
      }
  
      /**
       * Calling this method starts a scheduled job that will run tests of the specified type to
       * make sure bluetooth is OK and cycle power to bluetooth if needed and configured by
       * enablePowerCycleOnFailures
       *
       * @param context
       * @param testType e.g. BluetoothMedic.TRANSMIT_TEST | BluetoothMedic.SCAN_TEST
       */
      @SuppressWarnings("unused")
      @RequiresApi(21)
      public void enablePeriodicTests(Context context, int testType) {
          initializeWithContext(context);
          this.mTestType = testType;
          LogManager.d(TAG, "Medic scheduling periodic tests of types " + testType);
          this.scheduleRegularTests(context);
      }
  
      /**
       * Starts up a brief blueooth scan with the intent of seeing if it results in an error condition
       * indicating the bluetooth stack may be in a bad state.
       *
       * If the failure error code matches a pattern known to be associated with a bad bluetooth stack
       * state, then the bluetooth stack is turned off and then back on after a short delay in order
       * to try to recover.
       *
       * @return false if the test indicates a failure indicating a bad state of the bluetooth stack
       */
      @SuppressLint("MissingPermission")
      @SuppressWarnings({"unused","WeakerAccess"})
      @RequiresApi(21)
      public boolean runScanTest(final Context context) {
          initializeWithContext(context);
          if (isBleScanPermissionDenied()) {
              LogManager.i(TAG, "Cant run scan test -- required scan permission is denied");
              return true;
          }
  
          this.mScanTestResult = null;
          LogManager.i(TAG, "Starting scan test");
          final long testStartTime = System.currentTimeMillis();
          if (this.mAdapter != null) {
              final BluetoothLeScanner scanner = this.mAdapter.getBluetoothLeScanner();
              final ScanCallback callback = new ScanCallback() {
                  @SuppressLint("MissingPermission")
                  public void onScanResult(int callbackType, ScanResult result) {
                      super.onScanResult(callbackType, result);
                      BluetoothMedic.this.mScanTestResult = true;
                      LogManager.i(BluetoothMedic.TAG, "Scan test succeeded");
                      try {
                          scanner.stopScan(this);
                      }
                      catch (IllegalStateException e) { /* do nothing */ } // caught if bluetooth is off here
                  }
  
                  public void onBatchScanResults(List<ScanResult> results) {
                      super.onBatchScanResults(results);
                  }
  
                  public void onScanFailed(int errorCode) {
                      super.onScanFailed(errorCode);
                      LogManager.d(BluetoothMedic.TAG, "Sending onScanFailed event");
                      BluetoothMedic.this.processMedicAction("onScanFailed", errorCode);
                      if(errorCode == 2) {
                          LogManager.w(BluetoothMedic.TAG,
                                  "Scan test failed in a way we consider a failure");
                          BluetoothMedic.this.sendScreenNotification(
                                  "scan failed", "bluetooth not ok");
                          BluetoothMedic.this.mScanTestResult = false;
                      } else {
                          LogManager.i(BluetoothMedic.TAG,
                                  "Scan test failed in a way we do not consider a failure");
                          BluetoothMedic.this.mScanTestResult = true;
                      }
  
                  }
              };
              if(scanner != null) {
                  try {
                      scanner.startScan(callback);
                      while (this.mScanTestResult == null) {
                          LogManager.d(TAG, "Waiting for scan test to complete...");
  
                          try {
                              Thread.sleep(1000L);
                          } catch (InterruptedException e) { /* do nothing */ }
  
                          if (System.currentTimeMillis() - testStartTime > 5000L) {
                              LogManager.d(TAG, "Timeout running scan test");
                              break;
                          }
                      }
                      scanner.stopScan(callback);
                  } catch (IllegalStateException e) {
                      LogManager.d(TAG, "Bluetooth is off.  Cannot run scan test.");
                  } catch (NullPointerException e) {
                      // Needed to stop a crash caused by internal NPE thrown by Android.  See issue #636
                      LogManager.e(TAG, "NullPointerException. Cannot run scan test.", e);
                  }
              }
              else {
                  LogManager.d(TAG, "Cannot get scanner");
              }
          }
  
          LogManager.d(TAG, "scan test complete");
          return this.mScanTestResult == null || this.mScanTestResult;
      }
  
      /**
       * Starts up a beacon transmitter with the intent of seeing if it results in an error condition
       * indicating the bluetooth stack may be in a bad state.
       *
       * If the failure error code matches a pattern known to be associated with a bad bluetooth stack
       * state, then the bluetooth stack is turned off and then back on after a short delay in order
       * to try to recover.
       *
       * @return false if the test indicates a failure indicating a bad state of the bluetooth stack
       */
      @SuppressLint("MissingPermission")
      @SuppressWarnings({"unused","WeakerAccess"})
      @RequiresApi(21)
      public boolean runTransmitterTest(final Context context) {
          initializeWithContext(context);
          if (isBleAdvertisePermissionDenied()) {
              LogManager.i(TAG, "Cannot run transmitter test -- advertise permission not granted");
              return true;
          }
          this.mTransmitterTestResult = null;
          long testStartTime = System.currentTimeMillis();
          if (mAdapter != null) {
              final BluetoothLeAdvertiser advertiser = getAdvertiserSafely(mAdapter);
              if(advertiser != null) {
                  AdvertiseSettings settings = (new Builder()).setAdvertiseMode(0).build();
                  AdvertiseData data = (new android.bluetooth.le.AdvertiseData.Builder())
                          .addManufacturerData(0, new byte[]{0}).build();
                  LogManager.i(TAG, "Starting transmitter test");
                  advertiser.startAdvertising(settings, data, new AdvertiseCallback() {
                      public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                          super.onStartSuccess(settingsInEffect);
                          LogManager.i(BluetoothMedic.TAG, "Transmitter test succeeded");
                          advertiser.stopAdvertising(this);
                          BluetoothMedic.this.mTransmitterTestResult = true;
                      }
  
                      public void onStartFailure(int errorCode) {
                          super.onStartFailure(errorCode);
                          LogManager.d(BluetoothMedic.TAG, "Sending onStartFailure event");
                          BluetoothMedic.this.processMedicAction("onStartFailed", errorCode);
                          if(errorCode == 4) {
                              BluetoothMedic.this.mTransmitterTestResult = false;
                              LogManager.w(BluetoothMedic.TAG,
                                      "Transmitter test failed in a way we consider a test failure");
                          } else {
                              BluetoothMedic.this.mTransmitterTestResult = true;
                              LogManager.i(BluetoothMedic.TAG,
                                      "Transmitter test failed, but not in a way we consider a test failure");
                          }
                      }
                  });
              } else {
                  LogManager.d(TAG, "Cannot get advertiser");
              }
              while(this.mTransmitterTestResult == null) {
                  LogManager.d(TAG, "Waiting for transmitter test to complete...");
  
                  try {
                      Thread.sleep(1000L);
                  } catch (InterruptedException e) { /* do nothing */ }
  
                  if(System.currentTimeMillis() - testStartTime > 5000L) {
                      LogManager.d(TAG, "Timeout running transmitter test");
                      break;
                  }
              }
          }
  
          LogManager.d(TAG, "transmitter test complete");
          return this.mTransmitterTestResult != null && this.mTransmitterTestResult;
      }
  
      /**
       *
       * Configure whether to send user-visible notification warnings when bluetooth power is cycled.
       *
       * @param enabled if true, a user-visible notification is sent to tell the user when
       * @param icon the icon drawable to use in notifications (e.g. R.drawable.notification_icon)
       */
      @SuppressWarnings("unused")
      @RequiresApi(21)
      public void setNotificationsEnabled(boolean enabled, int icon) {
          this.mNotificationsEnabled = enabled;
          this.mNotificationIcon = icon;
      }
  
      @RequiresApi(21)
      private boolean cycleBluetoothIfNotTooSoon() {
          long millisSinceLastCycle = System.currentTimeMillis() - this.mLastBluetoothPowerCycleTime;
          if(millisSinceLastCycle < MIN_MILLIS_BETWEEN_BLUETOOTH_POWER_CYCLES) {
              LogManager.d(TAG, "Not cycling bluetooth because we just did so " +
                      millisSinceLastCycle + " milliseconds ago.");
              return false;
          } else {
              this.mLastBluetoothPowerCycleTime = System.currentTimeMillis();
              LogManager.d(TAG, "Power cycling bluetooth");
              this.cycleBluetooth();
              return true;
          }
      }
  
      @SuppressLint("MissingPermission")
      @RequiresApi(21)
      private void cycleBluetooth() {
          LogManager.i(TAG, "Power cycling bluetooth");
          if (isBleConnectPermissionDenied()) {
              LogManager.i(TAG, "Can't power cycle bleutooth.  Connect permisison is denied.");
              return;
          }
          LogManager.i(TAG, "Turning Bluetooth off.");
          if (mAdapter != null) {
              this.mAdapter.disable();
              this.mHandler.postDelayed(new Runnable() {
                  public void run() {
                      LogManager.d(BluetoothMedic.TAG, "Turning Bluetooth back on.");
                      if (BluetoothMedic.this.mAdapter != null) {
                          BluetoothMedic.this.mAdapter.enable();
                      }
                  }
              }, 1000L);
          }
          else {
              LogManager.w(TAG, "Cannot cycle bluetooth.  Manager is null.");
          }
      }
  
      @RequiresApi(21)
      private void sendScreenNotification(String message, String detail) {
          Context context = mContext;
          if (context == null) {
              LogManager.e(TAG, "congtext is unexpectedly null");
              return;
          }
          initializeWithContext(context);
          if(this.mNotificationsEnabled) {
              if (!this.mNotificationChannelCreated) {
                  createNotificationChannel(context, "err");
              }
              NotificationCompat.Builder builder =
                      (new NotificationCompat.Builder(context, "err"))
                              .setContentTitle("BluetoothMedic: " + message)
                              .setSmallIcon(mNotificationIcon)
                              .setVibrate(new long[]{200L, 100L, 200L}).setContentText(detail);
              TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
              stackBuilder.addNextIntent(new Intent("NoOperation"));
  
              PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(
                      0,
                      PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
              );
              builder.setContentIntent(resultPendingIntent);
              NotificationManager notificationManager =
                      (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
              if (notificationManager != null) {
                  notificationManager.notify(1, builder.build());
              }
          }
      }
  
      private void createNotificationChannel(Context context, String channelId) {
          // On Android 8.0 and above posting a notification without a
          // channel is an error. So create a notification channel 'err'
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
              String channelName = "Errors";
              String description = "Scan errors";
              int importance = NotificationManager.IMPORTANCE_DEFAULT;
              NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
              channel.setDescription(description);
              NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
              notificationManager.createNotificationChannel(channel);
              mNotificationChannelCreated = true;
          }
      }
  
      @RequiresApi(21)
      private void scheduleRegularTests(Context context) {
          initializeWithContext(context);
          ComponentName serviceComponent = new ComponentName(context, BluetoothTestJob.class);
          android.app.job.JobInfo.Builder builder =
                  new android.app.job.JobInfo.Builder(BluetoothTestJob.getJobId(context), serviceComponent);
          builder.setRequiresCharging(false);
          builder.setRequiresDeviceIdle(false);
          builder.setPeriodic(900000L); // 900 secs is 15 minutes -- the minimum time on Android
          builder.setPersisted(true);
          PersistableBundle bundle = new PersistableBundle();
          bundle.putInt("test_type", this.mTestType);
          builder.setExtras(bundle);
          JobScheduler jobScheduler = (JobScheduler)
                  context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
          if (jobScheduler != null) {
              jobScheduler.schedule(builder.build());
          }
      }
  
      @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
      private BluetoothLeAdvertiser getAdvertiserSafely(BluetoothAdapter adapter) {
          try {
              // This can sometimes throw a NullPointerException as reported here:
              // https://github.com/AltBeacon/android-beacon-library/issues/672
              return adapter.getBluetoothLeAdvertiser();
          }
          catch (Exception e) {
              LogManager.w(TAG, "Cannot get bluetoothLeAdvertiser", e);
          }
          return null;
      }
  
      private boolean isAndroidSPermissionDenied(String androidSPermission) {
          // Android S permissions only exists phones starting with Android S
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
              // Android S permissions are not used unless the app targets Android S or higher
              if (mContext != null && mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S) {
                  return ActivityCompat.checkSelfPermission(
                          mContext, androidSPermission
                  ) != PackageManager.PERMISSION_GRANTED;
              }
          }
          return false;
      }
  
      private boolean isBleScanPermissionDenied() {
          return isAndroidSPermissionDenied(Manifest.permission.BLUETOOTH_SCAN);
      }
      private boolean isBleAdvertisePermissionDenied() {
          return isAndroidSPermissionDenied(Manifest.permission.BLUETOOTH_ADVERTISE);
      }
      private boolean isBleConnectPermissionDenied() {
          return isAndroidSPermissionDenied(Manifest.permission.BLUETOOTH_CONNECT);
      }
  
  }
  