content: package com.uriio.beacons;
  
  import android.annotation.TargetApi;
  import android.app.AlarmManager;
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.app.Service;
  import android.bluetooth.BluetoothAdapter;
  import android.bluetooth.BluetoothManager;
  import android.bluetooth.le.AdvertiseCallback;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.PackageManager;
  import android.os.Binder;
  import android.os.Build;
  import android.os.IBinder;
  import android.os.SystemClock;
  import android.support.v4.content.LocalBroadcastManager;
  import android.util.Log;
  
  import com.uriio.beacons.ble.Advertiser;
  import com.uriio.beacons.ble.AdvertisersManager;
  import com.uriio.beacons.model.Beacon;
  
  import java.util.List;
  import java.util.UUID;
  
  /**
   * Advertiser service, that persists and restarts in case of a crash by restoring its previous state.
   */
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  public class BleService extends Service implements AdvertisersManager.Listener {
      private static final String TAG = "BleService";
      private static boolean D = BuildConfig.DEBUG;
  
      private static final String META_KEY_NOTIFICATION_PROVIDER = "com.uriio.provider";
  
      public static final String ACTION_BEACONS = BuildConfig.APPLICATION_ID + ".ACTION_BEACONS";
  
      /** Item state changed*/
      public static final String ACTION_ITEM_STATE = BuildConfig.APPLICATION_ID + ".ACTION_ITEM_STATE";
  
      /**
       * AlarmManager PendingIntent - a beacon is asking to be recreated with updated advertising data
       */
      public static final String ACTION_ALARM     = BuildConfig.APPLICATION_ID + ".ACTION_ALARM";
  
      /** Notification actions pending intents */
      static final String ACTION_PAUSE_ALL = BuildConfig.APPLICATION_ID + ".ACTION_PAUSE_ALL";
      static final String ACTION_STOP_ALL  = BuildConfig.APPLICATION_ID + ".ACTION_STOP_ALL";
  
      /** Notification content tapped */
      static final String ACTION_NOTIFICATION_CONTENT = BuildConfig.APPLICATION_ID + ".ACTION_NOTIF_CONTENT";
  
      public static final int EVENT_ADVERTISER_ADDED      = 1;
      public static final int EVENT_ADVERTISER_STARTED    = 2;
      public static final int EVENT_ADVERTISER_STOPPED    = 3;
      public static final int EVENT_ADVERTISER_FAILED     = 4;
      public static final int EVENT_ADVERTISE_UNSUPPORTED = 5;
  
      /**
       * Sent when the beacon was not started because of a missing precondition or error.
       * Example: retrieving the URL to be advertised from a back-end server failed.
       */
      public static final int EVENT_START_FAILED          = 6;
  
      /** Intent extra - beacon UUID */
      public static final String EXTRA_ITEM_ID         = "id";
      /** Intent extra - beacon storage ID */
      public static final String EXTRA_ITEM_STORAGE_ID = "dbid";
  
      public static final String EXTRA_BEACON_EVENT    = "type";
      public static final String EXTRA_ERROR           = "error";
      public static final String EXTRA_ERROR_CODE      = "code";
  
      public class LocalBinder extends Binder {
          public BleService getUriioService() {
              return BleService.this;
          }
      }
  
      private AdvertisersManager mAdvertisersManager = null;
      private AlarmManager mAlarmManager = null;
      private NotificationManager mNotificationManager = null;
  
      /** App-provided (or default) notification provider. */
      private NotificationProvider mNotificationProvider = null;
  
      /** System clock time in milliseconds, when service was created */
      private long mPowerOnStartTime = 0;
  
      /** Estimated total broadcasted advertisements since power-on time  */
      private long mEstimatedPDUCount = 0;
  
      /** Keeps track whether the service was started. */
      private boolean mStarted = false;
  
      /**
       * Receiver for Bluetooth events and beacon actions
       */
      private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              String action = intent.getAction();
              if (null == action) {
                  return;
              }
  
              switch (action) {
                  case BluetoothAdapter.ACTION_STATE_CHANGED:
                      handleBluetoothStateChanged(intent);
                      break;
                  case ACTION_ITEM_STATE:
                      handleItemState(intent);
                      break;
              }
          }
      };
  
      //region Service
  
      @Override
      public IBinder onBind(Intent intent) {
          if(D) Log.d(TAG, "onBind " + intent);
  
          return new LocalBinder();
      }
  
      @Override
      public void onCreate() {
          if(D) Log.d(TAG, "onCreate");
  
          super.onCreate();
  
          mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
  
          mNotificationProvider = getNotificationProvider();
          mPowerOnStartTime = SystemClock.elapsedRealtime();
          mEstimatedPDUCount = 0;
      }
  
      @Override
      public int onStartCommand(Intent intent, int flags, int startId) {
          if(D) Log.d(TAG, "onStartCommand intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
  
          if (!mStarted) {
              initializeService();
              mStarted = true;
          }
  
          // intent is null if service restarted
          if (null != intent) {
              Receiver.completeWakefulIntent(intent);
  
              Beacon beacon = Beacons.findActive(intent.getLongExtra(EXTRA_ITEM_STORAGE_ID, 0));
  
              // unsaved beacon, try finding by UUID
              if (null == beacon) beacon = Beacons.findActive((UUID) intent.getSerializableExtra(EXTRA_ITEM_ID));
  
              if (null != beacon) {
                  beacon.onAdvertiseEnabled(this);
              }
          }
  
          return super.onStartCommand(intent, flags, startId);
      }
  
      @Override
      public void onDestroy() {
          if(D) Log.d(TAG, "onDestroy");
  
          // we can end up destroyed without actually ever being started, and since we didn't
          // register our receiver, the app would crash on unregister
          if (mStarted) {
              if (null != mBroadcastReceiver) {
                  LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
  
                  // we registered also in normal way
                  unregisterReceiver(mBroadcastReceiver);
              }
  
              if (null != mAdvertisersManager) {
                  mAdvertisersManager.close();
                  mAdvertisersManager = null;
              }
  
              List<Beacon> activeBeacons = Beacons.getActiveIfAny();
              if (null != activeBeacons) {
                  for (int i = activeBeacons.size() - 1; i >= 0; i--) {
                      activeBeacons.get(i).cancelRefresh(this);
                  }
              }
  
              if (null != mNotificationManager) {
                  mNotificationManager.cancel(mNotificationProvider.getNotificationId());
              }
  
              mStarted = false;
          }
  
          Beacons.onBleServiceDestroyed();
  
          super.onDestroy();
  
          mNotificationManager = null;
          mAlarmManager = null;
      }
  
      //endregion
  
      private void handleBluetoothStateChanged(Intent intent) {
          int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
  
          if(D) Log.d(TAG, "Bluetooth state changed: " + state);
  
          if (BluetoothAdapter.STATE_TURNING_OFF == state) {
              mAdvertisersManager.onBluetoothOff();
  
              List<Beacon> activeBeacons = Beacons.getActiveIfAny();
              if (null != activeBeacons) {
                  for (int i = activeBeacons.size() - 1; i >= 0; i--) {
                      mEstimatedPDUCount += activeBeacons.get(i).onBluetoothDisabled(this);
                  }
              }
  
              // when an advertiser will start, the service will start in foreground again
              // fixme - what if the service is killed while BT is off and we had RUNNING beacons?
              stopForeground(true);
  
              broadcastBeaconEvent(EVENT_ADVERTISER_STOPPED, null);
          } else if (BluetoothAdapter.STATE_ON == state) {
              List<Beacon> activeBeacons = Beacons.getActiveIfAny();
              if (null != activeBeacons) {
                  for (int i = activeBeacons.size() - 1; i >= 0; i--) {
                      activeBeacons.get(i).onBluetoothEnabled(this);
                  }
              }
          }
      }
  
      private void handleItemState(Intent intent) {
          if(D) Log.d(TAG, "handleItemState() called with: intent = [" + intent + "]");
  
          Beacon beacon = Beacons.findActive(intent.getLongExtra(EXTRA_ITEM_STORAGE_ID, 0));
  
          // unsaved beacon, try finding by UUID
          if (null == beacon) beacon = Beacons.findActive((UUID) intent.getSerializableExtra(EXTRA_ITEM_ID));
  
          if (null != beacon) {
              if (D) Log.d(TAG, "Received itemState intent for " + beacon);
  
              switch (beacon.getActiveState()) {
                  case Beacon.ACTIVE_STATE_ENABLED:
                      beacon.onAdvertiseEnabled(this);
                      break;
                  case Beacon.ACTIVE_STATE_PAUSED:
                      stopBeacon(beacon, false);
                      break;
                  case Beacon.ACTIVE_STATE_STOPPED:
                      stopBeacon(beacon, true);
                      break;
              }
          }
      }
  
      private void initializeService() {
          if(D) Log.d(TAG, "initializeService");
  
          Beacons.initialize(this);
  
          mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
  
          IntentFilter localIntentFilter = new IntentFilter(ACTION_ITEM_STATE);
          LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver, localIntentFilter);
  
          // Bluetooth events are not received when using LocalBroadcastManager
          IntentFilter systemIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
          registerReceiver(mBroadcastReceiver, systemIntentFilter);
  
          BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
          if (null != bluetoothManager) {
              mAdvertisersManager = new AdvertisersManager(bluetoothManager, this);
  
              restoreSavedState();
          }
      }
  
      private void restoreSavedState() {
          // restore advertisers
          for (Beacon beacon : Beacons.getActive()) {
              if (beacon.getActiveState() == Beacon.ACTIVE_STATE_ENABLED) {
                  beacon.onAdvertiseEnabled(this);
              }
          }
      }
  
      /**
       * Start or restart the advertising of an item's BLE beacon.
       * @param beacon    The item to (re)start.
       * @return True if the beacon was started, false otherwise.
       */
      public boolean startBeaconAdvertiser(Beacon beacon) {
          if (null == mAdvertisersManager) {
              return false;
          }
  
          if (!mAdvertisersManager.isBluetoothEnabled()) {
              return false;
          }
  
          if (!mAdvertisersManager.canAdvertise()) {
              beacon.onAdvertiseFailed(AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED);
              broadcastBeaconEvent(EVENT_ADVERTISE_UNSUPPORTED, beacon);
              return false;
          }
  
          // stop current advertiser for this beacon
          Advertiser existingAdvertiser = beacon.getAdvertiser();
          if (null != existingAdvertiser) {
              mAdvertisersManager.stopAdvertiser(existingAdvertiser);
              mEstimatedPDUCount += existingAdvertiser.clearPDUCount();
          }
  
          Advertiser advertiser = beacon.recreateAdvertiser(this);
          if (null == advertiser) {
              return false;
          }
  
          advertiser.setManager(mAdvertisersManager);
          return mAdvertisersManager.startAdvertiser(advertiser);
      }
  
      private void broadcastBeaconEvent(int event, Beacon beacon) {
          Beacons.broadcastLocalIntent(makeBeaconEventIntent(event, beacon));
      }
  
      public void broadcastError(Beacon beacon, int event, String error) {
          Beacons.broadcastLocalIntent(makeBeaconEventIntent(event, beacon).putExtra(EXTRA_ERROR, error));
      }
  
      private void broadcastError(Beacon beacon, int event, int errorCode) {
          Beacons.broadcastLocalIntent(makeBeaconEventIntent(event, beacon).putExtra(EXTRA_ERROR_CODE, errorCode));
      }
  
      public static Intent makeBeaconEventIntent(int event, Beacon beacon) {
          Intent intent = new Intent(ACTION_BEACONS).putExtra(EXTRA_BEACON_EVENT, event);
          if (null != beacon) {
              intent.putExtra(EXTRA_ITEM_ID, beacon.getUUID());
              intent.putExtra(EXTRA_ITEM_STORAGE_ID, beacon.getSavedId());
              intent.putExtra("kind", beacon.getKind());
          }
          return intent;
      }
  
      private Beacon findActiveBeacon(Advertiser advertiser) {
          for (Beacon item : Beacons.getActive()) {
              if (item.getAdvertiser() == advertiser) return item;
          }
          return null;
      }
  
      //region AdvertisersManager.Listener
      @Override
      public void onAdvertiserStarted(Advertiser advertiser) {
          Beacon beacon = findActiveBeacon(advertiser);
          if (null != beacon) {
              beacon.setAdvertiseState(Beacon.ADVERTISE_RUNNING);
  
              long scheduledRefresh = beacon.getScheduledRefreshElapsedTime();
  
              if (scheduledRefresh > 0) {
                  // schedule alarm for next onAdvertiseEnabled
                  if(D) Log.d(TAG, "Scheduling alarm for " + beacon.getUUID() + " in " + scheduledRefresh);
                  scheduleElapsedTimeAlarm(scheduledRefresh, beacon.getAlarmPendingIntent(this));
              }
  
              broadcastBeaconEvent(EVENT_ADVERTISER_STARTED, beacon);
  
              updateForegroundNotification(true);
          }
      }
  
      @Override
      public void onAdvertiserFailed(Advertiser advertiser, int errorCode) {
          Beacon beacon = findActiveBeacon(advertiser);
          if (null != beacon) {
              // mark beacon as paused so we can try to start it again
              beacon.onAdvertiseFailed(errorCode);
              broadcastError(beacon, EVENT_ADVERTISER_FAILED, errorCode);
          }
      }
      //endregion
  
      private void updateForegroundNotification(boolean newAdvertiserStarted) {
          int totalRunning = 0;
          for (Beacon beacon : Beacons.getActive()) {
              if (beacon.getAdvertiseState() == Beacon.ADVERTISE_RUNNING) {
                  ++totalRunning;
              }
          }
  
          if (totalRunning > 0) {
              Notification notification = mNotificationProvider.makeNotification(mNotificationManager, totalRunning);
              if (null != notification) {
                  if (newAdvertiserStarted && 1 == totalRunning) {
                      startForeground(mNotificationProvider.getNotificationId(), notification);
                  } else {
                      mNotificationManager.notify(mNotificationProvider.getNotificationId(), notification);
                  }
              }
          }
          else {
              stopForeground(mNotificationProvider.onStoppedForeground());
          }
      }
  
      private void stopBeacon(Beacon beacon, boolean remove) {
          Advertiser advertiser = beacon.getAdvertiser();
  
          // stop the beacon's advertising
          if (null != advertiser && advertiser.getStatus() == Advertiser.STATUS_RUNNING) {
              mAdvertisersManager.stopAdvertiser(advertiser);
              mEstimatedPDUCount += advertiser.clearPDUCount();
          }
          // cancel any pending alarm
          beacon.cancelRefresh(this);
          beacon.setAdvertiseState(Beacon.ADVERTISE_STOPPED);
  
          if (remove) {
              Beacons.getActive().remove(beacon);
              if (0 == Beacons.getActive().size()) {
  //                Toast.makeText(this, "BLE service stopped, no beacons are enabled.", Toast.LENGTH_LONG).show();
                  stopSelf();
              }
          }
          broadcastBeaconEvent(EVENT_ADVERTISER_STOPPED, beacon);
  
          updateForegroundNotification(false);
      }
  
      private void scheduleElapsedTimeAlarm(long triggerAtMillis, PendingIntent operation) {
          if(D) Log.d(TAG, "scheduleElapsedTimeAlarm at " + triggerAtMillis + " now: " + SystemClock.elapsedRealtime());
  
          if (null != mAlarmManager) {
              mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, operation);
          }
      }
  
      public void cancelAlarm(PendingIntent operation) {
          if (null != mAlarmManager) {
              mAlarmManager.cancel(operation);
          }
      }
  
      /**
       * @return Power-on time since service is up, in milliseconds
       */
      public long getPowerOnTime() {
          return SystemClock.elapsedRealtime() - mPowerOnStartTime;
      }
  
      public long updateEstimatedPDUCount() {
          List<Beacon> activeBeacons = Beacons.getActive();
          for (int i = activeBeacons.size() - 1; i >= 0; i--) {
              Beacon beacon = activeBeacons.get(i);
  
              if (null != beacon.getAdvertiser()) {
                  mEstimatedPDUCount += beacon.getAdvertiser().clearPDUCount();
              }
          }
  
          return mEstimatedPDUCount;
      }
  
      private NotificationProvider getNotificationProvider() {
          ApplicationInfo appInfo;
          try {
              appInfo = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
          } catch (PackageManager.NameNotFoundException e) {
              throw new RuntimeException("App package not found");
          }
  
          String providerClassName = null;
  
          // metadata is null when no entries exist
          if (null != appInfo && null != appInfo.metaData) {
              providerClassName = appInfo.metaData.getString(META_KEY_NOTIFICATION_PROVIDER);
          }
  
          if (null != providerClassName) {
              try {
                  return (NotificationProvider) Class.forName(providerClassName).getConstructor(Context.class).newInstance(this);
              } catch (ReflectiveOperationException e) {
                  throw new RuntimeException("Invalid provider class name", e);
              }
          } else {
              return new NotificationProvider(this);
          }
      }
  }
  