content: package com.uriio.beacons.model;
  
  import android.annotation.SuppressLint;
  import android.app.PendingIntent;
  import android.bluetooth.le.AdvertiseCallback;
  import android.bluetooth.le.AdvertiseSettings;
  import android.content.Context;
  import android.content.Intent;
  import android.database.Cursor;
  import android.os.Build;
  import android.os.SystemClock;
  import android.support.annotation.NonNull;
  import android.support.v4.content.LocalBroadcastManager;
  import android.util.Log;
  
  import com.uriio.beacons.Beacons;
  import com.uriio.beacons.BleService;
  import com.uriio.beacons.BuildConfig;
  import com.uriio.beacons.Receiver;
  import com.uriio.beacons.Storage;
  import com.uriio.beacons.ble.Advertiser;
  
  import java.util.UUID;
  
  /**
   * Base container for an item.
   */
  public abstract class Beacon implements Advertiser.SettingsProvider {
      private static final String TAG = "Beacon";
      private static boolean D = BuildConfig.DEBUG;
  
      /**
       * Beacon is active and should be enabled if Bluetooth is available.
       */
      public static final int ACTIVE_STATE_ENABLED = 0;
      /**
       * Beacon is active but paused.
       */
      public static final int ACTIVE_STATE_PAUSED  = 1;
      /**
       * Beacon is stopped. This is the default initial state.
       */
      public static final int ACTIVE_STATE_STOPPED = 2;
  
      // Advertise state constants that reflect current BLE status
      public static final int ADVERTISE_STOPPED       = 0;
      public static final int ADVERTISE_RUNNING       = 1;
      public static final int ADVERTISE_NO_BLUETOOTH  = 2;
  
      @SuppressLint("InlinedApi")
      private static final int DEFAULT_ADVERTISE_MODE = AdvertiseSettings.ADVERTISE_MODE_BALANCED;
  
      @SuppressLint("InlinedApi")
      private static final int DEFAULT_ADVERTISE_TX_POWER = AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM;
  
      /** Associated BLE object, if any. **/
      private Advertiser mAdvertiser = null;
  
      private String mName = null;
  
      /**
       * Unique beacon ID, needed for finding existing UNSAVED beacons. Cannot use a simple counter
       * for this because it will be different between restarts and we will find the wrong beacon when
       * an Alarm is triggered and we search for the beacon with that exact ID, while a random UUID will not
       * find any beacon at all (but since the beacon was unsaved that's to be expected).
       */
      private final UUID mUUID;
  
      /** Persistent ID for database purpose **/
      private long mStorageId = 0;
  
      private static int _lastStableId = 0;
      private final int mStableId = ++_lastStableId;
  
      private int mFlags;
  
      private int mAdvertiseMode;
      private int mTxPowerLevel;
  
      /** Current advertise status. This is the state of the BLE advertising, not of the beacon. **/
      private int mAdvertiseState = ADVERTISE_STOPPED;
  
      private int mActiveState = ACTIVE_STATE_STOPPED;
  
      private boolean mConnectable = false;
      private int mErrorCode;
      private String mErrorDetsils;
  
      /**
       * Creates a Beacon instance using the specified Cursor. Useful for
       * deserializing from a persistent layer such as a database.
       * @param cursor The Cursor to use, positioned at the index of the data to read.
       * @return A Beacon instance, or null in case it could not be created.
       */
      @SuppressWarnings("unused")
      public static Beacon fromCursor(@NonNull Cursor cursor) {
          return Storage.fromCursor(cursor);
      }
  
      /**
       * Constructs a beacon container.
       * @param advertiseMode        BLE advertising mode
       * @param txPowerLevel         BLE Transmit power level
       * @param name                 An optional name. Not used in actual BLE packets.
       */
      public Beacon(@Advertiser.Mode int advertiseMode,
                    @Advertiser.Power int txPowerLevel, int flags, String name) {
          mUUID = UUID.randomUUID();
          init(0, advertiseMode, txPowerLevel, flags, name);
      }
  
      public Beacon(@Advertiser.Mode int advertiseMode, @Advertiser.Power int txPowerLevel, int flags) {
          this(advertiseMode, txPowerLevel, flags, null);
      }
  
      public Beacon(int flags, String name) {
          this(DEFAULT_ADVERTISE_MODE, DEFAULT_ADVERTISE_TX_POWER, flags, name);
      }
  
      public Beacon(int flags) {
          this(flags, null);
      }
  
      public Beacon() {
          this(0);
      }
  
      @NonNull
      @Override
      public String toString() {
          if(BuildConfig.DEBUG) {
              return "stableID " + mStableId + " storeId " + mStorageId + " uuid " + mUUID + " name " + mName;
          }
  
          return super.toString();
      }
  
      /**
       * Sets some basic properties. Should only be called immediately after creation, and before save().
       */
      public void init(long storageId,
                       @Advertiser.Mode int advertiseMode,
                       @Advertiser.Power int txPowerLevel, int flags, String name) {
          mStorageId = storageId;
          mFlags = flags;
          mAdvertiseMode = advertiseMode;
          mTxPowerLevel = txPowerLevel;
          mName = name;
      }
  
      /**
       * Saves this beacon to persistent storage and optionally starts advertising.
       * @param startAdvertising    Enables the beacon to advertise, if not started already.
       * @return Same instance.
       */
      public Beacon save(boolean startAdvertising) {
          // don't save an already persisted beacon
          if (getSavedId() > 0) return this;
  
          Storage.getInstance().insert(this);
  
          if (startAdvertising) {
              start();
          }
  
          return this;
      }
  
      /**
       * Saves the beacon and enables BLE advertising.
       * @return Same instance.
       */
      @SuppressWarnings("unused")
      public Beacon save() {
          return save(true);
      }
  
      private void onEditDone(boolean needRestart) {
          if (getSavedId() > 0) {
              Storage.getInstance().update(this);
          }
  
          if (needRestart) {
              restartBeacon();
          }
      }
  
      private void restartBeacon() {
          if (ADVERTISE_RUNNING == getAdvertiseState()) {
              setActiveState(ACTIVE_STATE_PAUSED);
              setState(ACTIVE_STATE_ENABLED, false);  // already persisted as enabled
          }
      }
  
      /**
       * Enables BLE advertising for this beacon.
       * @return True on success. Note that the actual advertising may fail later, this call only transitions the beacon into enabled state.
       */
      public boolean start() {
          if(D) Log.d(TAG, "start() called");
  
          if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
              // can't advertise below L
              return false;
          }
  
          if (Beacons.getActive().size() == 0) {
              Context context = Beacons.getContext();
              if (null == context) return false;
  
              if(D) Log.d(TAG, "no active beacons, starting service");
  
              context.startService(new Intent(context, BleService.class));
          }
          return setState(Beacon.ACTIVE_STATE_ENABLED, true);
      }
  
      /** Stops the beacon and deletes it from storage. */
      public void delete() {
          if (getActiveState() != ACTIVE_STATE_STOPPED) {
              setState(ACTIVE_STATE_STOPPED, false);
          }
  
          if (getSavedId() > 0) {
              Storage.getInstance().delete(this);
          }
      }
  
      public void pause() {
          setState(ACTIVE_STATE_PAUSED, true);
      }
  
      /** Stops the beacon from advertising. */
      public void stop() {
          // change state and save the new state if needed
          setState(ACTIVE_STATE_STOPPED, true);
      }
  
      private boolean setState(int state, boolean persist) {
          if(D) Log.d(TAG, "setState() called with: state = [" + state + "], persist = [" + persist + "]");
  
          if (state < 0 || state > 2) {
              return false;
          }
  
          Beacon targetBeacon = getSavedId() > 0 ? Beacons.findActive(getSavedId()) : Beacons.findActive(getUUID());
  
          if (null == targetBeacon) {
              // this beacon is not known to be active
              if (state != Beacon.ACTIVE_STATE_STOPPED) {
                  // the new state is not 'stopped', so that means it will switch to active
  
                  // if the Beacons singleton is initialized we need to mark ourself as active.
                  // If it's not, beacon might get added a second time on Beacons init, if the service
                  // is not started at this point (example: stopping last beacon -> starting a new one)
                  // If we are not being persisted before Beacons.init, beacon gets discarded.
                  // usecase - an app starts its first beacon, service gets created,
                  // only the persisted beacons are known and activated.
                  // TL;DR - make sure a new unpersisted beacon is kept track of
                  if (Beacons.isInitialized()
                          || (0 == mStorageId))
                  {
                      Beacons.getActive().add(this);
                      Beacons.onActiveBeaconAdded(this);
                  }
              }
  
              targetBeacon = this;
          }
  
          if (state != targetBeacon.getActiveState()) {
              if(D) Log.d(TAG, "new state! " + state + " old " + targetBeacon.getActiveState());
  
              // item changed state
              targetBeacon.setActiveState(state);
              if (persist && targetBeacon.getSavedId() > 0) {
                  Storage.getInstance().updateState(targetBeacon, state);
              }
  
              sendStateBroadcast(targetBeacon);
          }
  
          return true;
      }
  
      private static void sendStateBroadcast(Beacon beacon) {
          Context context = Beacons.getContext();
          if (null != context) {
              Intent intent = new Intent(BleService.ACTION_ITEM_STATE);
  
              if (beacon.getSavedId() > 0) {
                  intent.putExtra(BleService.EXTRA_ITEM_STORAGE_ID, beacon.getSavedId());
              }
              else {
                  intent.putExtra(BleService.EXTRA_ITEM_ID, beacon.getUUID());
              }
  
              LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
          }
      }
  
      public void setActiveState(int state) {
          mActiveState = state;
      }
  
      public int getActiveState() {
          return mActiveState;
      }
  
      /**
       * @return Persistent ID of this beacon. If 0, then the beacon is not yet saved.
       */
      public long getSavedId() {
          return mStorageId;
      }
  
      /**
       * @return A non-persistent unique ID different than any other beacon.
       * The ID is not stable between service restarts and can easily collide.
       */
      @SuppressWarnings("unused")
      public long getStableId() {
          return mStableId;
      }
  
      /**
       * @return A non-persistent UUID different than any other beacon.
       * The UUID is not stable between service restarts, but it will not collide.
       */
      public UUID getUUID() {
          return mUUID;
      }
  
      public Advertiser getAdvertiser() {
          return mAdvertiser;
      }
  
      // region Advertiser.SettingsProvider
  
      @Override
      @Advertiser.Mode
      public int getAdvertiseMode() {
          return mAdvertiseMode;
      }
  
      @Override
      @Advertiser.Power
      public int getTxPowerLevel() {
          return mTxPowerLevel;
      }
  
      @Override
      public int getTimeout() {
          return 0;
      }
  
      @Override
      public boolean isConnectable() {
          return mConnectable;
      }
  
      // endregion
  
      public int getFlags() {
          return mFlags;
      }
  
      public int getAdvertiseState() {
          return mAdvertiseState;
      }
  
      public void setAdvertiseState(int status) {
          mAdvertiseState = status;
      }
  
      public Advertiser recreateAdvertiser(BleService bleService) {
          mErrorCode = 0;
          mErrorDetsils = null;
          return (mAdvertiser = createAdvertiser(bleService));
      }
  
      protected abstract Advertiser createAdvertiser(BleService advertisersManager);
  
      public abstract int getKind();
  
      /**
       * Descriptive name. Not used for BLE advertising purposes.
       */
      public String getName() {
          return mName;
      }
  
      /** Called by the service when Bluetooth enters disabled state. Never call this directly. */
      public long onBluetoothDisabled(BleService bleService) {
          cancelRefresh(bleService);
  
          long pduCount = 0;
          setAdvertiseState(ADVERTISE_NO_BLUETOOTH);
  
          if (null != mAdvertiser) {
              pduCount = mAdvertiser.clearPDUCount();
  
              // do not attempt to re-use the same callback for future broadcasts
              mAdvertiser = null;
          }
  
          return pduCount;
      }
  
      public void onBluetoothEnabled(BleService service) {
          if (ACTIVE_STATE_ENABLED == mActiveState) {
              onAdvertiseEnabled(service);
          } else {
              // beacon was active but not enabled, aka PAUSED
              setAdvertiseState(ADVERTISE_STOPPED);
          }
      }
  
      public void cancelRefresh(BleService bleService) {
          if(getScheduledRefreshElapsedTime() > 0) {
              // cancel the scheduled beacon recreation
              bleService.cancelAlarm(getAlarmPendingIntent(bleService));
          }
      }
  
      public void onAdvertiseFailed(int errorCode) {
          if (AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS == errorCode){
              // don't stop - we could attempt to start the beacon again if we free a slot
              pause();
          }
          else {
              // fatal, no point in keeping the beacon in active state
              stop();
          }
  
          mAdvertiser = null;
          mErrorCode = errorCode;
          setErrorDetails(Advertiser.getErrorName(errorCode));
      }
  
      public int getErrorCode() {
          return mErrorCode;
      }
  
      public String getErrorDetsils() {
          return mErrorDetsils;
      }
  
      /**
       * @return [Display purposes] UNIX timestamp at which the beacon should refresh. This value may
       * be wrong if the beacon must be synced with an external service because the system time may be
       * wrong. Subclasses that need to present an absolute correct time should override this method.
       */
      public long getScheduledRefreshTime() {
          long refreshElapsedTime = getScheduledRefreshElapsedTime();
          return 0 == refreshElapsedTime ? 0 : System.currentTimeMillis() - SystemClock.elapsedRealtime() + refreshElapsedTime;
      }
  
      /**
       * @return The SystemClock.elapsedRealtime() value on which this beacon should be restarted.
       * This should NOT be computed based on the current system time.
       */
      public long getScheduledRefreshElapsedTime() {
          return 0;
      }
  
      /**
       * Called when the item should start advertising a new BLE beacon.
       * Default implementation starts a new beacon advertiser; subclasses may override with other behaviour.
       * @param service    BLE Service
       */
      public void onAdvertiseEnabled(BleService service) {
          // (re)create the beacon
          if (!service.startBeaconAdvertiser(this)) {
              if(D) Log.e(TAG, "startBeaconAdvertiser failed");
          }
      }
  
      public BaseEditor edit() {
          return new BaseEditor();
      }
  
      /**
       * Sets the persistent item ID. Has no effect if the item already has an ID.
       * @param id    The item ID
       */
      public void setStorageId(long id) {
          if (0 == mStorageId) {
              mStorageId = id;
          }
      }
  
      public PendingIntent getAlarmPendingIntent(Context context) {
          Intent intent = new Intent(BleService.ACTION_ALARM, null, context, Receiver.class);
  
          if (getSavedId() > 0) intent.putExtra(BleService.EXTRA_ITEM_STORAGE_ID, getSavedId());
          else intent.putExtra(BleService.EXTRA_ITEM_ID, getUUID());
  
          // use a unique private request code, or else the returned PendingIntent is "identical" for all beacons, being reused
          return PendingIntent.getBroadcast(context, mStableId, intent, 0);
      }
  
      public void setErrorDetails(String error) {
          mErrorDetsils = error;
      }
  
      public CharSequence getNotificationSubject() {
          return null == mName ? "<unnamed>" : mName.substring(0, Math.min(30, mName.length()));
      }
  
      public class BaseEditor<T> {
          private boolean mNeedsRestart = false;
  
          public BaseEditor<T> setAdvertiseMode(@Advertiser.Mode int mode) {
              if (mode != mAdvertiseMode) {
                  mAdvertiseMode = mode;
                  setNeedsRestart();
              }
              return this;
          }
  
          public BaseEditor<T> setAdvertiseTxPower(@Advertiser.Power int txPowerLevel) {
              if (txPowerLevel != mTxPowerLevel) {
                  mTxPowerLevel = txPowerLevel;
                  setNeedsRestart();
              }
              return this;
          }
  
          public BaseEditor<T> setConnectable(boolean connectable) {
              if (connectable != mConnectable) {
                  mConnectable = connectable;
                  setNeedsRestart();
              }
              return this;
          }
  
          public BaseEditor<T> setName(String name) {
              if (null == name || !name.equals(mName)) {
                  mName = name;
              }
              return this;
          }
  
          public BaseEditor<T> setFlags(int flags) {
              if (mFlags != flags) {
                  mFlags = flags;
                  setNeedsRestart();  // ?...
              }
              return this;
          }
  
          public void apply() {
              onEditDone(mNeedsRestart);
          }
  
          /**
           * Indicates that the beacon should be restarted
           * after the editor's changes are applied.
           */
          @SuppressWarnings("WeakerAccess")
          protected void setNeedsRestart() {
              mNeedsRestart = true;
          }
      }
  }
  