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); } } }