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 {
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.
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
}
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()