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.

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 {
    def notificare_version = 'REPLACE_WITH_LATEST_VERSION'

    implementation "re.notifica:notificare-geo:$notificare_version"
    implementation "re.notifica:notificare-geo-gms:$notificare_version"         // Enable support for Google Mobile Services.
    implementation "re.notifica:notificare-geo-hms:$notificare_version"         // Enable support for Huawei Mobile Services.
    implementation "re.notifica:notificare-geo-beacons:$notificare_version"     // Enable support for beacons detection.
}

Requesting Permissions

Additional to the manifest permissions, 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.

To allow for an easier handling of permissions between versions, your app needs to make a distinction between foreground and background location updates permissions.

It is important to note the Notificare library is not notified of authorization status changes, therefore you have to call Notificare.geo().enableLocationUpdates() on every authorization status change. This allows us to check the latest authorization and act accordingly by enabling location tracking, geofencing or clearing up location data based on the user's decision.

In order to achieve what was mentioned above, here is a rough implementation to serve as a guide. You'll have to adjust this sample to match your application.

class MainActivity : AppCompatActivity() {

    private val foregroundLocationPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        if (permissions.all { !it.value }) {
            Log.i(TAG, "User denied foreground location permissions.")
            return@registerForActivityResult
        }

        enableLocationUpdates()
    }

    private val backgroundLocationPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        if (!granted) {
            Log.i(TAG, "User denied background location permissions.")
            return@registerForActivityResult
        }

        enableLocationUpdates()
    }

    private val bluetoothScanPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        if (!granted) {
            Log.i(TAG, "User denied bluetooth scan permissions.")
            return@registerForActivityResult
        }

        enableLocationUpdates()
    }


    // ...


    private fun enableLocationUpdates() {
        ensureForegroundLocationPermission()
            && ensureBackgroundLocationPermission()
            && ensureBluetoothScanPermission()

        Notificare.geo().enableLocationUpdates()
    }

    private fun ensureForegroundLocationPermission(): Boolean {
        val permission = Manifest.permission.ACCESS_FINE_LOCATION
        val granted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
        if (granted) return true

        if (shouldShowRequestPermissionRationale(permission)) {
            AlertDialog.Builder(this)
                .setTitle(R.string.app_name)
                .setMessage(R.string.main_foreground_permission_rationale)
                .setPositiveButton(android.R.string.ok) { _, _ ->
                    foregroundLocationPermissionLauncher.launch(
                        arrayOf(
                            Manifest.permission.ACCESS_COARSE_LOCATION,
                            Manifest.permission.ACCESS_FINE_LOCATION
                        )
                    )
                }
                .show()

            return false
        }

        foregroundLocationPermissionLauncher.launch(
            arrayOf(
                Manifest.permission.ACCESS_COARSE_LOCATION,
                Manifest.permission.ACCESS_FINE_LOCATION
            )
        )

        return false
    }

    private fun ensureBackgroundLocationPermission(): Boolean {
        val permission = when {
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Manifest.permission.ACCESS_BACKGROUND_LOCATION
            else -> Manifest.permission.ACCESS_FINE_LOCATION
        }

        val granted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
        if (granted) return true

        if (shouldShowRequestPermissionRationale(permission)) {
            AlertDialog.Builder(this)
                .setTitle(R.string.app_name)
                .setMessage(R.string.main_background_permission_rationale)
                .setPositiveButton(android.R.string.ok) { _, _ ->
                    backgroundLocationPermissionLauncher.launch(permission)
                }
                .show()

            return false
        }

        backgroundLocationPermissionLauncher.launch(permission)
        return false
    }

    private fun ensureBluetoothScanPermission(): Boolean {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true

        val permission = Manifest.permission.BLUETOOTH_SCAN
        val granted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
        if (granted) return true

        if (shouldShowRequestPermissionRationale(permission)) {
            AlertDialog.Builder(this)
                .setTitle(R.string.app_name)
                .setMessage(R.string.main_bluetooth_scan_permission_rationale)
                .setPositiveButton(android.R.string.ok) { _, _ ->
                    bluetoothScanPermissionLauncher.launch(permission)
                }
                .show()

            return false
        }

        bluetoothScanPermissionLauncher.launch(permission)
        return false
    }
}
public class MainActivity extends AppCompatActivity {

    private final ActivityResultLauncher<String[]> foregroundLocationPermissionLauncher = registerForActivityResult(
            new ActivityResultContracts.RequestMultiplePermissions(), permissions -> {
                for (Map.Entry<String, Boolean> entry : permissions.entrySet()) {
                    if (!entry.getValue()) {
                        Log.i(TAG, "User denied foreground location permissions.");
                        return;
                    }

                    enableLocationUpdates();
                }
            }
    );

    private final ActivityResultLauncher<String> backgroundLocationPermissionLauncher = registerForActivityResult(
            new ActivityResultContracts.RequestPermission(), granted -> {
                if (!granted) {
                    Log.i(TAG, "User denied background location permissions.");
                    return;
                }

                enableLocationUpdates();
            }
    );

    private final ActivityResultLauncher<String> bluetoothScanPermissionLauncher = registerForActivityResult(
            new ActivityResultContracts.RequestPermission(), granted -> {
                if (!granted) {
                    Log.i(TAG, "User denied bluetooth scan permissions.");
                    return;
                }

                enableLocationUpdates();
            }
    );


    // ...


    private void enableLocationUpdates() {
        boolean hasFullCapabilities = ensureForegroundLocationPermission()
                && ensureBackgroundLocationPermission()
                && ensureBluetoothScanPermission();

        NotificareGeoCompat.enableLocationUpdates();
    }

    private Boolean ensureForegroundLocationPermission() {
        String permission = Manifest.permission.ACCESS_FINE_LOCATION;
        boolean granted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED;
        if (granted) return true;

        if (shouldShowRequestPermissionRationale(permission)) {
            new AlertDialog.Builder(this)
                    .setTitle(R.string.app_name)
                    .setMessage(R.string.main_foreground_permission_rationale)
                    .setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
                            foregroundLocationPermissionLauncher.launch(
                                    new String[]{
                                            Manifest.permission.ACCESS_COARSE_LOCATION,
                                            Manifest.permission.ACCESS_FINE_LOCATION
                                    }
                            )
                    )
                    .show();

            return false;
        }

        foregroundLocationPermissionLauncher.launch(
                new String[]{
                        Manifest.permission.ACCESS_COARSE_LOCATION,
                        Manifest.permission.ACCESS_FINE_LOCATION
                }
        );

        return false;
    }

    private Boolean ensureBackgroundLocationPermission() {
        String permission;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            permission = Manifest.permission.ACCESS_BACKGROUND_LOCATION;
        } else {
            permission = Manifest.permission.ACCESS_FINE_LOCATION;
        }

        boolean granted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED;
        if (granted) return true;

        if (shouldShowRequestPermissionRationale(permission)) {
            new AlertDialog.Builder(this)
                    .setTitle(R.string.app_name)
                    .setMessage(R.string.main_background_permission_rationale)
                    .setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
                            backgroundLocationPermissionLauncher.launch(permission)
                    )
                    .show();

            return false;
        }

        backgroundLocationPermissionLauncher.launch(permission);
        return false;
    }

    private Boolean ensureBluetoothScanPermission() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true;

        String permission = Manifest.permission.BLUETOOTH_SCAN;
        boolean granted = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED;
        if (granted) return true;

        if (shouldShowRequestPermissionRationale(permission)) {
            new AlertDialog.Builder(this)
                    .setTitle(R.string.app_name)
                    .setMessage(R.string.main_bluetooth_scan_permission_rationale)
                    .setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
                            bluetoothScanPermissionLauncher.launch(permission)
                    )
                    .show();

            return false;
        }

        bluetoothScanPermissionLauncher.launch(permission);
        return false;
    }
}

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 ready event. Once you have requested the appropriate permissions, and they have been granted, you can call Notificare.geo().enableLocationUpdates() to start receiving location updates. Our library will automatically collect the user location and start monitoring for regions you've created via the dashboard or API.

You can also check whether the user enrolled on location updates.

// Check if the user has previously enabled location updates.
Notificare.geo().hasLocationServicesEnabled
// Check if the user has previously enabled location updates.
NotificareGeoCompat.getHasLocationServicesEnabled()

Receiving geo events

Once you start monitoring location updates in your app, you can subscribe to certain events and perform additional operations as necessary.

Start by creating a subclass of the NotificareGeoIntentReceiver and implement the methods you need.

class CustomGeoIntentReceiver : NotificareGeoIntentReceiver() {
    override fun onLocationUpdated(context: Context, location: NotificareLocation) {
        // more code ...
    }

    override fun onRegionEntered(context: Context, region: NotificareRegion) {
        // more code ...
    }

    override fun onRegionExited(context: Context, region: NotificareRegion) {
        // more code ...
    }

    override fun onBeaconEntered(context: Context, beacon: NotificareBeacon) {
        // more code ...
    }

    override fun onBeaconExited(context: Context, beacon: NotificareBeacon) {
        // more code ...
    }

    override fun onBeaconsRanged(context: Context, region: NotificareRegion, beacons: List<NotificareBeacon>) {
        // more code ...
    }
}
public class CustomGeoIntentReceiver extends NotificareGeoIntentReceiver {
    @Override
    protected void onLocationUpdated(@NonNull Context context, @NonNull NotificareLocation location) {
        // more core
    }

    @Override
    protected void onRegionEntered(@NonNull Context context, @NonNull NotificareRegion region) {
        // more core
    }

    @Override
    protected void onRegionExited(@NonNull Context context, @NonNull NotificareRegion region) {
        // more core
    }

    @Override
    protected void onBeaconEntered(@NonNull Context context, @NonNull NotificareBeacon beacon) {
        // more core
    }

    @Override
    protected void onBeaconExited(@NonNull Context context, @NonNull NotificareBeacon beacon) {
        // more core
    }

    @Override
    protected void onBeaconsRanged(@NonNull Context context, @NonNull NotificareRegion region, @NonNull List<NotificareBeacon> beacons) {
        // more core
    }
}

In order to receive those intents in your receiver you need to let Notificare know about your class.

Notificare.geo().intentReceiver = CustomGeoIntentReceiver::class.java
NotificareGeoCompat.setIntentReceiver(CustomGeoIntentReceiver.class);

Alternatively, you can subscribe to those events by implementing a NotificareGeo.Listener.

class MainActivity : AppCompatActivity(), NotificareGeo.Listener {

    override fun onCreate(savedInstanceState: Bundle?) {
        // more code ...

        Notificare.geo().addListener(this)
    }

    override fun onDestroy() {
        // more code ...

        Notificare.geo().removeListener(this)
    }

    // more code ...

    override fun onLocationUpdated(location: NotificareLocation) {

    }

    override fun onRegionEntered(region: NotificareRegion) {

    }

    override fun onRegionExited(region: NotificareRegion) {

    }

    override fun onBeaconEntered(beacon: NotificareBeacon) {

    }

    override fun onBeaconExited(beacon: NotificareBeacon) {

    }

    override fun onBeaconsRanged(region: NotificareRegion, beacons: List<NotificareBeacon>) {

    }
}
public class MainActivity extends AppCompatActivity implements NotificareGeo.Listener {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        // more code ...

        NotificareGeoCompat.addListener(this);
    }

    @Override
    protected void onDestroy() {
        // more code ...

        NotificareGeoCompat.removeListener(this);
    }

    // more code ...

    @Override
    public void onLocationUpdated(@NonNull NotificareLocation location) {

    }

    @Override
    public void onRegionEntered(@NonNull NotificareRegion region) {

    }

    @Override
    public void onRegionExited(@NonNull NotificareRegion region) {

    }

    @Override
    public void onBeaconEntered(@NonNull NotificareBeacon beacon) {

    }

    @Override
    public void onBeaconExited(@NonNull NotificareBeacon beacon) {

    }

    @Override
    public void onBeaconsRanged(@NonNull NotificareRegion region, @NonNull List<NotificareBeacon> beacons) {

    }
}

Using Bluetooth Low-Energy Beacons

Once you've implemented the location functionality, to automatically monitor BTLE beacons in the user's vicinity, make sure to include the notificare-geo-beacons dependency in your app/build.gradle file.

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.

However, you might want to retrieve the proximity level of any beacons around you. This is only possible while the app is in foreground. To get this information you will need to implement the following listener:

class MainActivity : AppCompatActivity(), NotificareGeo.Listener {

    override fun onCreate(savedInstanceState: Bundle?) {
        // more code ...

        Notificare.geo().addListener(this)
    }

    override fun onDestroy() {
        // more code ...

        Notificare.geo().removeListener(this)
    }

    override fun onBeaconsRanged(region: NotificareRegion, beacons: List<NotificareBeacon>) {

    }
}
public class MainActivity extends AppCompatActivity implements NotificareGeo.Listener {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        // more code ...

        NotificareGeoCompat.addListener(this);
    }

    @Override
    protected void onDestroy() {
        // more code ...

        NotificareGeoCompat.removeListener(this);
    }

    // more code ...

    @Override
    public void onBeaconsRanged(@NonNull NotificareRegion region, @NonNull List<NotificareBeacon> beacons) {

    }
}

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.

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. You can opt in to this feature by adding the following to your AndroidManifest.xml:

<application>
  <meta-data
      android:name="re.notifica.geo.beacons.foreground_service_enabled"
      android:value="true" />
</application>

By default, the notification will be shown in the default channel from the Push module. If you are not using it, or would like to customise how the notification looks like, you can use the following meta-data properties.

<application>
    <meta-data
        android:name="re.notifica.geo.beacons.service_notification_channel"
        android:value="custom_beacon_notification_channel" />

    <meta-data
        android:name="re.notifica.geo.beacons.service_notification_small_icon"
        android:resource="@drawable/ic_baseline_bluetooth_searching_24" />

    <meta-data
        android:name="re.notifica.geo.beacons.service_notification_content_title"
        android:value="Scanning for beacons" />

    <meta-data
        android:name="re.notifica.geo.beacons.service_notification_content_text"
        android:value="A relevant piece of text informing the user why the app is scanning for beacons." />

    <meta-data
        android:name="re.notifica.geo.beacons.service_notification_progress"
        android:value="true" />
</application>

You app can react to the user tapping on the beacon foreground service notification by adding an intent filter to an activity and handling the intent.

<application>
    <activity name=".MainActivity">
        <intent-filter>
            <action android:name="re.notifica.intent.action.BeaconNotificationOpened" />

            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
</application>

Checking the monitoring state

There are some use cases when it's useful to know what is the current state of the monitoring service. This can be particularly helpful during development. You can check, on-demand, which regions are being monitored like the following.

Notificare.geo().monitoredRegions
NotificareGeoCompat.getMonitoredRegions();

Similarly, you can also check which regions the device is currently inside of.

Notificare.geo().enteredRegions
NotificareGeoCompat.getEnteredRegions();

Disable Location

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

Notificare.geo().disableLocationUpdates()
NotificareGeoCompat.disableLocationUpdates();