SDK

Location Services

In this page you'll dive deeper into functionality like using GPS signals to get the user's location or monitor their visits to regions and proximity to BTLE devices.

These services will bring a new level of contextuality to your app, allowing you to create geo-triggers to send notifications or categorize users based on their location behaviour.

The Notificare SDK will ask your users for permission to use location. Users will only allow location updates if you clearly state what you need the location for. In a real app, you should only ask for location updates after it makes sense to the user. Your app should explain that you are going to ask for location and why your app needs it.

As mentioned in the Implementation page, if you are going to use location services, you must include the following dependency in your app/build.gradle:

dependencies {
    implementation 're.notifica:notificare-location:2.7.0' // make sure you always use the latest version
    // implementation 're.notifica:notificare-location-hms:2.7.0' // if this app needs to run on HSM devices
}

Requesting Permission

Additionally to these manifest permissions, since Android 6 (a.k.a Marshmallow), applications will need to request the user permission to use location.

These permissions need to be requested from an activity in your app, after which you can safely enable location updates.

Since Android 10, there is a distinction between foreground (when app is used) and background (always) location updates. If the user only allows foreground location updates, Geofences will not work and location will only be updated when the app is running. Since Android 11, you have to explicitly ask for foreground permission before you can ask for background permission. It is a best practice to do this also in apps running Android 10
Since Android 12, there is a distinction between permissions given for Precise and Approximate location. To be able to use Geofences, the user will need to grant both background and precise location permission.

By default, Notificare.shared().requestLocationPermission will now only ask the user for foreground location updates. Likewise, Notificare.shared().hasLocationPermissionGranted() will return true if the user has allowed foreground location updates.

To allow for an easier handling of permissions between versions, your app needs to make a distinction between foreground and background location updates permissions. This can be done by using standard Android permission request methods, but the Notificare SDK offers a few extra helper methods that make this a bit more convenient:

  • requestForegroundLocationPermission
  • hasForegroundLocationPermissionGranted
  • checkRequestForegroundLocationPermissionResult
  • shouldShowForegroundRequestPermissionRationale

to request and check foreground location permission.

  • requestBackgroundLocationPermission
  • hasBackgroundLocationPermissionGranted
  • checkRequestBackgroundLocationPermissionResult
  • shouldShowBackgroundRequestPermissionRationale

to request and check background location permission.

To make sure the location updates aren't started before the library is ready to be used, you should wait until it is safe to do so, by listening to the OnNotificareReady event.

Let your activity implement the OnNotificareReadyListener interface and use that listener to ask for foreground permissions first, background permissions later. The following code example will ask for background permissions immediately after foreground permissions were granted, but this could be done explicitly during onboarding. In Android versions before 10, this code will only ask for foreground location permission, and automatically grant background permission, since there is no distinction before Android 10.

public class MyMainActivity extends ActionBarBaseActivity implements Notificare.OnNotificareReadyListener {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //...more code

        Notificare.shared().addNotificareReadyListener(this);
    }


    @Override
    public void onNotificareReady(NotificareApplicationInfo info) {
        askForegroundLocationPermission();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        //...more code

        Notificare.shared().removeNotificareReadyListener(this);
    }

    public void askForegroundLocationPermission() {
        if (!Notificare.shared().hasForegroundLocationPermissionGranted()) {
            Log.i(TAG, "permission not granted");
            if (Notificare.shared().shouldShowForegroundRequestPermissionRationale(this)) {
                // Here we should show a dialog explaining location updates
                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setMessage(R.string.alert_location_permission_rationale)
                        .setTitle(R.string.app_name)
                        .setCancelable(true)
                        .setNegativeButton(R.string.button_location_permission_rationale_cancel, (dialog, id) -> {
                            Log.i(TAG, "foreground location not agreed");
                        })
                        .setPositiveButton(R.string.button_location_permission_rationale_ok, (dialog, id) -> Notificare.shared().requestForegroundLocationPermission(this, LOCATION_PERMISSION_REQUEST_CODE))
                        .create()
                        .show();
            } else {
                Notificare.shared().requestForegroundLocationPermission(this, LOCATION_PERMISSION_REQUEST_CODE);
            }
        } else if (Notificare.shared().isLocationUpdatesEnabled()) {
            Log.i(TAG, "foreground location permission granted, we can update location");
            Notificare.shared().enableLocationUpdates();
            askBackgroundLocationPermission();
        }
    }

    public void askBackgroundLocationPermission() {
        if (!Notificare.shared().hasBackgroundLocationPermissionGranted()) {
            Log.i(TAG, "permission not granted");
            if (Notificare.shared().shouldShowBackgroundRequestPermissionRationale(this)) {
                // Here we should show a dialog explaining location updates
                AlertDialog.Builder builder = new AlertDialog.Builder(this);
                builder.setMessage(R.string.alert_background_location_permission_rationale)
                        .setTitle(R.string.app_name)
                        .setCancelable(true)
                        .setNegativeButton(R.string.button_location_permission_rationale_cancel, (dialog, id) -> {
                            Log.i(TAG, "background location not agreed");
                        });
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                    // In Android 11, the system gives us the localized label of the corresponding setting in system settings
                    builder.setPositiveButton(getPackageManager().getBackgroundPermissionOptionLabel(), (dialog, id) -> Notificare.shared().requestBackgroundLocationPermission(this, BACKGROUND_LOCATION_PERMISSION_REQUEST_CODE));
                } else {
                    builder.setPositiveButton(R.string.button_location_permission_rationale_ok, (dialog, id) -> Notificare.shared().requestBackgroundLocationPermission(this, BACKGROUND_LOCATION_PERMISSION_REQUEST_CODE));
                }
                builder.create();
                builder.show();
            } else {
                Notificare.shared().requestBackgroundLocationPermission(this, BACKGROUND_LOCATION_PERMISSION_REQUEST_CODE);
            }
        } else {
            Log.i(TAG, "background location permission granted, we can update location");
            Notificare.shared().enableLocationUpdates();
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case LOCATION_PERMISSION_REQUEST_CODE:
                if (Notificare.shared().checkRequestForegroundLocationPermissionResult(permissions, grantResults)) {
                    Log.i(TAG, "foreground locations permission granted");
                    Notificare.shared().enableLocationUpdates();
                    askBackgroundLocationPermission();
                }
                break;
            case BACKGROUND_LOCATION_PERMISSION_REQUEST_CODE:
                if (Notificare.shared().checkRequestBackgroundLocationPermissionResult(permissions, grantResults)) {
                    Log.i(TAG, "background location permission granted");
                    Notificare.shared().enableLocationUpdates();
                }
                break;
        }

    }

}
class Test : ActionBarBaseActivity(), OnNotificareReadyListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //...more code
        Notificare.shared().addNotificareReadyListener(this)
    }

    override fun onNotificareReady(info: NotificareApplicationInfo) {
        askForegroundLocationPermission()
    }

    override fun onDestroy() {
        super.onDestroy()

        //...more code
        Notificare.shared().removeNotificareReadyListener(this)
    }

    fun askForegroundLocationPermission() {
        if (!Notificare.shared().hasForegroundLocationPermissionGranted()) {
            Log.i(TAG, "permission not granted")
            if (Notificare.shared().shouldShowForegroundRequestPermissionRationale(this)) {
                // Here we should show a dialog explaining location updates
                val builder = AlertDialog.Builder(this)
                builder.setMessage(R.string.alert_location_permission_rationale)
                        .setTitle(R.string.app_name)
                        .setCancelable(true)
                        .setNegativeButton(R.string.button_location_permission_rationale_cancel) { dialog: DialogInterface?, id: Int -> Log.i(TAG, "foreground location not agreed") }
                        .setPositiveButton(R.string.button_location_permission_rationale_ok) { dialog: DialogInterface?, id: Int -> Notificare.shared().requestForegroundLocationPermission(this, LOCATION_PERMISSION_REQUEST_CODE) }
                        .create()
                        .show()
            } else {
                Notificare.shared().requestForegroundLocationPermission(this, LOCATION_PERMISSION_REQUEST_CODE)
            }
        } else if (Notificare.shared().isLocationUpdatesEnabled) {
            Log.i(TAG, "foreground location permission granted, we can update location")
            Notificare.shared().enableLocationUpdates()
            askBackgroundLocationPermission()
        }
    }

    fun askBackgroundLocationPermission() {
        if (!Notificare.shared().hasBackgroundLocationPermissionGranted()) {
            Log.i(TAG, "permission not granted")
            if (Notificare.shared().shouldShowBackgroundRequestPermissionRationale(this)) {
                // Here we should show a dialog explaining location updates
                val builder = AlertDialog.Builder(this)
                builder.setMessage(R.string.alert_background_location_permission_rationale)
                        .setTitle(R.string.app_name)
                        .setCancelable(true)
                        .setNegativeButton(R.string.button_location_permission_rationale_cancel) { dialog: DialogInterface?, id: Int -> Log.i(TAG, "background location not agreed") }
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                    // In Android 11, the system gives us the localized label of the corresponding setting in system settings
                    builder.setPositiveButton(packageManager.backgroundPermissionOptionLabel) { dialog: DialogInterface?, id: Int -> Notificare.shared().requestBackgroundLocationPermission(this, BACKGROUND_LOCATION_PERMISSION_REQUEST_CODE) }
                } else {
                    builder.setPositiveButton(R.string.button_location_permission_rationale_ok) { dialog: DialogInterface?, id: Int -> Notificare.shared().requestBackgroundLocationPermission(this, BACKGROUND_LOCATION_PERMISSION_REQUEST_CODE) }
                }
                builder.create()
                builder.show()
            } else {
                Notificare.shared().requestBackgroundLocationPermission(this, BACKGROUND_LOCATION_PERMISSION_REQUEST_CODE)
            }
        } else {
            Log.i(TAG, "background location permission granted, we can update location")
            Notificare.shared().enableLocationUpdates()
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            LOCATION_PERMISSION_REQUEST_CODE -> if (Notificare.shared().checkRequestForegroundLocationPermissionResult(permissions, grantResults)) {
                Log.i(TAG, "foreground location permission granted")
                Notificare.shared().enableLocationUpdates()
                askBackgroundLocationPermission()
            }
            BACKGROUND_LOCATION_PERMISSION_REQUEST_CODE -> if (Notificare.shared().checkRequestBackgroundLocationPermissionResult(permissions, grantResults)) {
                Log.i(TAG, "background location permission granted")
                Notificare.shared().enableLocationUpdates()
            }
        }
    }
}

Once you have implemented the code above, if permission is granted, our library will automatically collect the user location and start monitoring for regions you've created via the dashboard or API.

Accessing the user location is as easy as invoking the following method:

Notificare.shared().getCurrentLocation();
Notificare.shared().currentLocation

If authorized, you will also want to start using location data whenever your app is launched, you simply have to add the following in your Intent Receiver:

public class MyIntentReceiver extends DefaultIntentReceiver {

    @Override
    public void onReady() {

        //...more code

        if (Notificare.shared().isLocationUpdatesEnabled()) {

            Notificare.shared().enableLocationUpdates();

        }

        //...more code
    }

    //...more code
}
class MyIntentReceiver: DefaultIntentReceiver() {

    override fun onReady() {

        //...more code

        if (Notificare.shared().isLocationUpdatesEnabled) {

            Notificare.shared().enableLocationUpdates()

        }

        //...more code
    }

    //...more code
}

Using Bluetooth Low-Energy beacons

Once you've implemented GPS location in your app, you can also listen to Bluetooth signals from BTLE beacons in your app. Simply add the following to your /app/build.gradle file:

dependencies {
    implementation 're.notifica:notificare-core:2.7.0'
    // implementation 're.notifica:notificare-core-hms:2.7.0' // if this app needs to run on HMS devices
    implementation 're.notifica:notificare-beacon:2.7.0' // Include this line
}
Since Android 12, your app needs explicit permission for Bluetooth scanning in order to detect beacons. On devices running older versions of Android, the permission will be granted automatically.

After the user has granted permission for tracking location (see above), you can request permission for Bluetooth scanning

public class MyMainActivity extends ActionBarBaseActivity implements Notificare.OnNotificareReadyListener {

    //...more code

    public void askBluetoothScanPermission() {
        if (BuildConfig.ENABLE_BEACONS && Notificare.shared().hasBeaconSupport()) {
            if (!Notificare.shared().hasBluetoothScanPermissionGranted()) {
                Log.i(TAG, "permission not granted");
                if (Notificare.shared().shouldShowBluetoothScanRequestPermissionRationale(this)) {
                    // Here we should show a dialog explaining bluetooth scanning
                    AlertDialog.Builder builder = new AlertDialog.Builder(this);
                    builder.setMessage(R.string.alert_bluetooth_scan_permission_rationale)
                            .setTitle(R.string.app_name)
                            .setCancelable(true)
                            .setNegativeButton(R.string.button_bluetooth_scan_permission_rationale_cancel, (dialog, id) -> {
                                Log.i(TAG, "bluetooth scan not agreed");
                            });
                    builder.setPositiveButton(R.string.button_bluetooth_scan_permission_rationale_ok, (dialog, id) -> Notificare.shared().requestBluetoothScanPermission(this, BLUETOOTH_SCAN_PERMISSION_REQUEST_CODE));
                    builder.create();
                    builder.show();
                } else {
                    Notificare.shared().requestBluetoothScanPermission(this, BLUETOOTH_SCAN_PERMISSION_REQUEST_CODE);
                }
            } else {
                Log.i(TAG, "bluetooth scan permission granted, we can scan beacons");
                Notificare.shared().enableBeacons();
            }
        }
    }
class MyMainActivity : ActionBarBaseActivity(), OnNotificareReadyListener {

    //...more code

    fun askBluetoothScanPermission() {
        if (!Notificare.shared().hasBluetoothScanPermissionGranted()) {
            Log.i(TAG, "permission not granted")
            if (Notificare.shared().shouldShowBluetoothScanPermissionRationale(this)) {
                // Here we should show a dialog explaining bluetooth scanning
                val builder = AlertDialog.Builder(this)
                builder.setMessage(R.string.alert_bluetooth_scan_permission_rationale)
                        .setTitle(R.string.app_name)
                        .setCancelable(true)
                        .setNegativeButton(R.string.button_bluetooth_scan_permission_rationale_cancel) { dialog: DialogInterface?, id: Int -> Log.i(TAG, "bluetooth scan not agreed") }
                builder.setPositiveButton(R.string.button_bluetooth_scan_permission_rationale_ok) { dialog: DialogInterface?, id: Int -> Notificare.shared().requestBluetoothScanPermission(this, BLUETOOTH_SCAN_PERMISSION_REQUEST_CODE) }
                builder.create()
                builder.show()
            } else {
                Notificare.shared().requestBluetoothScanPermission(this, BLUETOOTH_SCAN_PERMISSION_REQUEST_CODE)
            }
        } else {
            Log.i(TAG, "bluetooth scan permission granted, we can scan beacons")
            Notificare.shared().enableBeacons()
        }
    }

}

After that you can enable beacons signals by simply invoking the following method, right after a user has granted permission:

public class MyMainActivity extends ActionBarBaseActivity implements Notificare.OnNotificareReadyListener {

    //...more code

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case BLUETOOTH_SCAN_PERMISSION_REQUEST_CODE:
                if (Notificare.shared().checkRequestBluetoothScanPermissionResult(permissions, grantResults)) {
                    Log.i(TAG, "permission granted");
                    Notificare.shared().enableBeacons();
                }
                break;
        }
    }

    //...more code

}
class MyMainActivity : ActionBarBaseActivity(), OnNotificareReadyListener {

    //...more code

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        when (requestCode) {
            BLUETOOTH_SCAN_PERMISSION_REQUEST_CODE -> if (Notificare.shared().checkRequestBluetoothScanPermissionResult(permissions, grantResults)) {
                Log.i(TAG, "permission granted")
                Notificare.shared().enableBeacons()
            }
        }
    }

    //...more code

}

You will also want to enable beacons from your Intent Receiver pretty much the same way you do with location:

public class MyIntentReceiver extends DefaultIntentReceiver {

    @Override
    public void onReady() {

        //...more code

    if (Notificare.shared().isLocationUpdatesEnabled()) {

        Notificare.shared().enableLocationUpdates();
        Notificare.shared().enableBeacons();

        //...more code
    }

    //...more code
}
class MyIntentReceiver: DefaultIntentReceiver() {

    override fun onReady() {

        //...more code

        if (Notificare.shared().isLocationUpdatesEnabled) {

            Notificare.shared().enableLocationUpdates()
            Notificare.shared().enableBeacons()

        }

        //...more code
    }

    //...more code
}

By doing this, you app will start monitoring for any beacons you inserted via the dashboard or API, in any of your regions.

By default, the app will scan at least every 5 minutes in the background in devices running Android < 8 (Oreo).

If you want your app to be more responsive, e.g. once per minute, you can set the interval yourself. Be aware, though, that a shorter interval will mean more power consumption.

Notificare.shared().enableBeacons(60000);
Notificare.shared().enableBeacons(60000)

Beacon scanning with a foreground service

In Oreo and up, background scans are more limited. First scans of a beacon in a region will come in very quickly, but detection of changes or leaving the beacon's range will take up to 15 minutes when in background. This limitation is posed by Android itself and there is no workaround to do this in background mode.

The only way to have your app scan for beacons more often on Android version Oreo and up, is by starting the scan as a foreground service. Foreground services will be shown to the user as an ongoing notification. The Notificare SDK provides you with a default notification which will show a text, a progress indicator and a Cancel action to allow the user to stop the foreground scan.

It can be started (and stopped) at any time, but will only be shown if enableBeacons is already called.

Notificare.shared().enableBeaconForegroundService();
Notificare.shared().enableBeaconForegroundService()
Notificare.shared().disableBeaconForegroundService();
Notificare.shared().disableBeaconForegroundService()

You can customise the text by translating the R.strings.notificare_beacon_scanning resource.

The Cancel action will send an intent to your intent receiver, which you can override if you want to catch it

@Override
public void onBeaconStopForegroundScan() {
    super.onBeaconStopForegroundScan();

    // Update your app's preferences
    MyPrefs.setBeaconsForeground(false);
}
override fun onBeaconStopForegroundScan() {
    super.onBeaconStopForegroundScan()

    // Update your app's preferences
    MyPrefs.setBeaconsForeground(false)
}

There's also a couple of methods that allow you to further customize the notification

// Override icon, notification channel, message string resource, action label string resource, don't show progress indicator
Notificare.shared().enableBeaconForegroundService(R.drawable.my_icon, myChannel, R.strings.my_beacon_text, R.string.my_beacon_action_label, false);

// Override icon, notification channel, message, action label, show progress indicator
Notificare.shared().enableBeaconForegroundService(R.drawable.my_icon, myChannel, myBeaconText, myActionLabel, true);

// Override with your own notification
Notificare.shared().enableBeaconForegroundService(myNotification, myNotificationId);
// Override icon, notification channel, message string resource, action label string resource, don't show progress indicator
Notificare.shared().enableBeaconForegroundService(R.drawable.my_icon, myChannel, R.strings.my_beacon_text, R.string.my_beacon_action_label, false)

// Override icon, notification channel, message, action label, show progress indicator
Notificare.shared().enableBeaconForegroundService(R.drawable.my_icon, myChannel, myBeaconText, myActionLabel, true)

// Override with your own notification
Notificare.shared().enableBeaconForegroundService(myNotification, myNotificationId)

Ranging beacons

Additionally, if you wish to get information about the beacons in the vicinity, when your app is in the foreground, here's an example of how you would go about in order to create a list of beacons in a fragment:

public class BeaconsFragment extends Fragment implements BeaconRangingListener {

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        //...more code

        if (Notificare.shared().getBeaconClient() != null) {
            Notificare.shared().getBeaconClient().addRangingListener(this);
        }

        //...more code
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (Notificare.shared().getBeaconClient() != null) {
            Notificare.shared().getBeaconClient().addRangingListener(this);
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        if (Notificare.shared().getBeaconClient() != null) {
            Notificare.shared().getBeaconClient().removeRangingListener(this);
        }
    }

    @Override
    public void onRangingBeacons(final List<NotificareBeacon> notificareBeacons) {
        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                beaconListAdapter.clear();
                for (NotificareBeacon beacon : notificareBeacons) {
                    beaconListAdapter.add(beacon);
                }
            }
    }


    //...more code
}
class BeaconsFragment : Fragment(), BeaconRangingListener {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

        //...more code
        if (Notificare.shared().beaconClient != null) {
            Notificare.shared().beaconClient.addRangingListener(this)
        }

        //...more code
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        if (Notificare.shared().beaconClient != null) {
            Notificare.shared().beaconClient.addRangingListener(this)
        }
    }

    override fun onDetach() {
        super.onDetach()
        if (Notificare.shared().beaconClient != null) {
            Notificare.shared().beaconClient.removeRangingListener(this)
        }
    }

    override fun onRangingBeacons(notificareBeacons: List<NotificareBeacon>) {
        activity!!.runOnUiThread {
            beaconListAdapter.clear()
            for (beacon in notificareBeacons) {
                beaconListAdapter.add(beacon)
            }
        }

        //...more code
    }
}

This is not a mandatory step in order to use beacons, as geo-triggers created in the dashboard will trigger notifications whenever you are in the vicinity of a beacon, even when your app is not being used.

Disable Location

Pretty much the same way you enable location, you can also stop tracking the user location by invoking the following method:

Notificare.shared().disableLocationUpdates();
Notificare.shared().disableLocationUpdates()

Disable Beacons

Although beacons will not work without using location, you can also stop listening to beacon advertising signals by invoking the following method:

Notificare.shared().disableBeacons();
Notificare.shared().disableBeacons()