Home | History | Annotate | Download | only in bluetoothadvertisements
      1 package com.example.android.bluetoothadvertisements;
      2 
      3 import android.app.Notification;
      4 import android.app.PendingIntent;
      5 import android.app.Service;
      6 import android.bluetooth.BluetoothAdapter;
      7 import android.bluetooth.BluetoothManager;
      8 import android.bluetooth.le.AdvertiseCallback;
      9 import android.bluetooth.le.AdvertiseData;
     10 import android.bluetooth.le.AdvertiseSettings;
     11 import android.bluetooth.le.BluetoothLeAdvertiser;
     12 import android.content.Context;
     13 import android.content.Intent;
     14 import android.os.Handler;
     15 import android.os.IBinder;
     16 import android.util.Log;
     17 import android.widget.Toast;
     18 
     19 import java.util.concurrent.TimeUnit;
     20 
     21 /**
     22  * Manages BLE Advertising independent of the main app.
     23  * If the app goes off screen (or gets killed completely) advertising can continue because this
     24  * Service is maintaining the necessary Callback in memory.
     25  */
     26 public class AdvertiserService extends Service {
     27 
     28     private static final String TAG = AdvertiserService.class.getSimpleName();
     29 
     30     private static final int FOREGROUND_NOTIFICATION_ID = 1;
     31 
     32     /**
     33      * A global variable to let AdvertiserFragment check if the Service is running without needing
     34      * to start or bind to it.
     35      * This is the best practice method as defined here:
     36      * https://groups.google.com/forum/#!topic/android-developers/jEvXMWgbgzE
     37      */
     38     public static boolean running = false;
     39 
     40     public static final String ADVERTISING_FAILED =
     41         "com.example.android.bluetoothadvertisements.advertising_failed";
     42 
     43     public static final String ADVERTISING_FAILED_EXTRA_CODE = "failureCode";
     44 
     45     public static final int ADVERTISING_TIMED_OUT = 6;
     46 
     47     private BluetoothLeAdvertiser mBluetoothLeAdvertiser;
     48 
     49     private AdvertiseCallback mAdvertiseCallback;
     50 
     51     private Handler mHandler;
     52 
     53     private Runnable timeoutRunnable;
     54 
     55     /**
     56      * Length of time to allow advertising before automatically shutting off. (10 minutes)
     57      */
     58     private long TIMEOUT = TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES);
     59 
     60     @Override
     61     public void onCreate() {
     62         running = true;
     63         initialize();
     64         startAdvertising();
     65         setTimeout();
     66         super.onCreate();
     67     }
     68 
     69     @Override
     70     public void onDestroy() {
     71         /**
     72          * Note that onDestroy is not guaranteed to be called quickly or at all. Services exist at
     73          * the whim of the system, and onDestroy can be delayed or skipped entirely if memory need
     74          * is critical.
     75          */
     76         running = false;
     77         stopAdvertising();
     78         mHandler.removeCallbacks(timeoutRunnable);
     79         stopForeground(true);
     80         super.onDestroy();
     81     }
     82 
     83     /**
     84      * Required for extending service, but this will be a Started Service only, so no need for
     85      * binding.
     86      */
     87     @Override
     88     public IBinder onBind(Intent intent) {
     89         return null;
     90     }
     91 
     92     /**
     93      * Get references to system Bluetooth objects if we don't have them already.
     94      */
     95     private void initialize() {
     96         if (mBluetoothLeAdvertiser == null) {
     97             BluetoothManager mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
     98             if (mBluetoothManager != null) {
     99                 BluetoothAdapter mBluetoothAdapter = mBluetoothManager.getAdapter();
    100                 if (mBluetoothAdapter != null) {
    101                     mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
    102                 } else {
    103                     Toast.makeText(this, getString(R.string.bt_null), Toast.LENGTH_LONG).show();
    104                 }
    105             } else {
    106                 Toast.makeText(this, getString(R.string.bt_null), Toast.LENGTH_LONG).show();
    107             }
    108         }
    109 
    110     }
    111 
    112     /**
    113      * Starts a delayed Runnable that will cause the BLE Advertising to timeout and stop after a
    114      * set amount of time.
    115      */
    116     private void setTimeout(){
    117         mHandler = new Handler();
    118         timeoutRunnable = new Runnable() {
    119             @Override
    120             public void run() {
    121                 Log.d(TAG, "AdvertiserService has reached timeout of "+TIMEOUT+" milliseconds, stopping advertising.");
    122                 sendFailureIntent(ADVERTISING_TIMED_OUT);
    123                 stopSelf();
    124             }
    125         };
    126         mHandler.postDelayed(timeoutRunnable, TIMEOUT);
    127     }
    128 
    129     /**
    130      * Starts BLE Advertising.
    131      */
    132     private void startAdvertising() {
    133         goForeground();
    134 
    135         Log.d(TAG, "Service: Starting Advertising");
    136 
    137         if (mAdvertiseCallback == null) {
    138             AdvertiseSettings settings = buildAdvertiseSettings();
    139             AdvertiseData data = buildAdvertiseData();
    140             mAdvertiseCallback = new SampleAdvertiseCallback();
    141 
    142             if (mBluetoothLeAdvertiser != null) {
    143                 mBluetoothLeAdvertiser.startAdvertising(settings, data,
    144                     mAdvertiseCallback);
    145             }
    146         }
    147     }
    148 
    149     /**
    150      * Move service to the foreground, to avoid execution limits on background processes.
    151      *
    152      * Callers should call stopForeground(true) when background work is complete.
    153      */
    154     private void goForeground() {
    155         Intent notificationIntent = new Intent(this, MainActivity.class);
    156         PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
    157             notificationIntent, 0);
    158         Notification n = new Notification.Builder(this)
    159             .setContentTitle("Advertising device via Bluetooth")
    160             .setContentText("This device is discoverable to others nearby.")
    161             .setSmallIcon(R.drawable.ic_launcher)
    162             .setContentIntent(pendingIntent)
    163             .build();
    164         startForeground(FOREGROUND_NOTIFICATION_ID, n);
    165     }
    166 
    167     /**
    168      * Stops BLE Advertising.
    169      */
    170     private void stopAdvertising() {
    171         Log.d(TAG, "Service: Stopping Advertising");
    172         if (mBluetoothLeAdvertiser != null) {
    173             mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
    174             mAdvertiseCallback = null;
    175         }
    176     }
    177 
    178     /**
    179      * Returns an AdvertiseData object which includes the Service UUID and Device Name.
    180      */
    181     private AdvertiseData buildAdvertiseData() {
    182 
    183         /**
    184          * Note: There is a strict limit of 31 Bytes on packets sent over BLE Advertisements.
    185          *  This includes everything put into AdvertiseData including UUIDs, device info, &
    186          *  arbitrary service or manufacturer data.
    187          *  Attempting to send packets over this limit will result in a failure with error code
    188          *  AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE. Catch this error in the
    189          *  onStartFailure() method of an AdvertiseCallback implementation.
    190          */
    191 
    192         AdvertiseData.Builder dataBuilder = new AdvertiseData.Builder();
    193         dataBuilder.addServiceUuid(Constants.Service_UUID);
    194         dataBuilder.setIncludeDeviceName(true);
    195 
    196         /* For example - this will cause advertising to fail (exceeds size limit) */
    197         //String failureData = "asdghkajsghalkxcjhfa;sghtalksjcfhalskfjhasldkjfhdskf";
    198         //dataBuilder.addServiceData(Constants.Service_UUID, failureData.getBytes());
    199 
    200         return dataBuilder.build();
    201     }
    202 
    203     /**
    204      * Returns an AdvertiseSettings object set to use low power (to help preserve battery life)
    205      * and disable the built-in timeout since this code uses its own timeout runnable.
    206      */
    207     private AdvertiseSettings buildAdvertiseSettings() {
    208         AdvertiseSettings.Builder settingsBuilder = new AdvertiseSettings.Builder();
    209         settingsBuilder.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER);
    210         settingsBuilder.setTimeout(0);
    211         return settingsBuilder.build();
    212     }
    213 
    214     /**
    215      * Custom callback after Advertising succeeds or fails to start. Broadcasts the error code
    216      * in an Intent to be picked up by AdvertiserFragment and stops this Service.
    217      */
    218     private class SampleAdvertiseCallback extends AdvertiseCallback {
    219 
    220         @Override
    221         public void onStartFailure(int errorCode) {
    222             super.onStartFailure(errorCode);
    223 
    224             Log.d(TAG, "Advertising failed");
    225             sendFailureIntent(errorCode);
    226             stopSelf();
    227 
    228         }
    229 
    230         @Override
    231         public void onStartSuccess(AdvertiseSettings settingsInEffect) {
    232             super.onStartSuccess(settingsInEffect);
    233             Log.d(TAG, "Advertising successfully started");
    234         }
    235     }
    236 
    237     /**
    238      * Builds and sends a broadcast intent indicating Advertising has failed. Includes the error
    239      * code as an extra. This is intended to be picked up by the {@code AdvertiserFragment}.
    240      */
    241     private void sendFailureIntent(int errorCode){
    242         Intent failureIntent = new Intent();
    243         failureIntent.setAction(ADVERTISING_FAILED);
    244         failureIntent.putExtra(ADVERTISING_FAILED_EXTRA_CODE, errorCode);
    245         sendBroadcast(failureIntent);
    246     }
    247 
    248 }
    249