Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Add the following keys to `Info.plist.`:

Set the the `android.useLegacyBridge` option to `true` in your Capacitor configuration. This prevents location updates halting after 5 minutes in the background. See https://capacitorjs.com/docs/config and https://github.com/capacitor-community/background-geolocation/issues/89.

On Android 13+, the app needs the `POST_NOTIFICATIONS` runtime permission to show the persistent notification informing the user that their location is being used in the background. You may need to [request this permission](https://developer.android.com/develop/ui/views/notifications/notification-permission) from the user, this can be accomplished [using the `@capacitor/local-notifications` plugin](https://capacitorjs.com/docs/apis/local-notifications#checkpermissions).
On Android 13+, the app needs the `POST_NOTIFICATIONS` runtime permission to show the persistent notification informing the user that their location is being used in the background. This plugin will [request this permission](https://developer.android.com/develop/ui/views/notifications/notification-permission) from the user if `backgroundMessage` is defined in the `addWatcher` options.

If your app forwards location updates to a server in real time, be aware that after 5 minutes in the background Android will throttle HTTP requests initiated from the WebView. The solution is to use a native HTTP plugin such as [CapacitorHttp](https://capacitorjs.com/docs/apis/http). See https://github.com/capacitor-community/background-geolocation/issues/14.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
Expand Down Expand Up @@ -45,7 +44,14 @@
Manifest.permission.ACCESS_FINE_LOCATION
},
alias = "location"
),
@Permission(
strings = {
Manifest.permission.POST_NOTIFICATIONS,
},
alias = "notifications"
)

}
)
public class BackgroundGeolocation extends Plugin {
Expand Down Expand Up @@ -76,90 +82,106 @@ public void addWatcher(final PluginCall call) {
call.reject("Service not running.");
return;
}
call.setKeepAlive(true);
if (getPermissionState("location") != PermissionState.GRANTED && !call.getBoolean("requestPermissions", true)) {
call.reject("Permission denied.", "NOT_AUTHORIZED");
return;
}

if (getPermissionState("location") != PermissionState.GRANTED) {
if (call.getBoolean("requestPermissions", true)) {
requestPermissionForAlias("location", call, "locationPermissionsCallback");
} else {
call.reject("Permission denied.", "NOT_AUTHORIZED");
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these nested if blocks are easier to understand. Why did you change it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a hard time understanding the flow... I guess it's a matter of taste...
I also saw that keepAlive was called in a place it didn't need to be called due to the nesting of the ifs...

} else if (!isLocationEnabled(getContext())) {
call.reject("Location services disabled.", "NOT_AUTHORIZED");
if (getPermissionState("location") != PermissionState.GRANTED && call.getBoolean("requestPermissions", true)) {
call.setKeepAlive(true);
requestPermissionForAlias("location", call, "locationPermissionsCallback");
return;
}
if (call.getBoolean("stale", false)) {
fetchLastLocation(call);

if (!isLocationEnabled(getContext())) {
call.reject("Location services disabled.", "NOT_AUTHORIZED");
return;
}
Notification backgroundNotification = null;
String backgroundMessage = call.getString("backgroundMessage");

if (backgroundMessage != null) {
Notification.Builder builder = new Notification.Builder(getContext())
.setContentTitle(
call.getString(
call.setKeepAlive(true);
addWatcherAfterLocationPermissionGranted(call);
}

private Notification createNotification(PluginCall call) {
String backgroundMessage = call.getString("backgroundMessage");
if (backgroundMessage == null) {
return null;
}
Notification.Builder builder = new Notification.Builder(getContext())
.setContentTitle(
call.getString(
"backgroundTitle",
"Using your location"
)
)
.setContentText(backgroundMessage)
.setOngoing(true)
.setPriority(Notification.PRIORITY_HIGH)
.setWhen(System.currentTimeMillis());

try {
String name = getAppString(
"capacitor_background_geolocation_notification_icon",
"mipmap/ic_launcher"
);
String[] parts = name.split("/");
// It is actually necessary to set a valid icon for the notification to behave
// correctly when tapped. If there is no icon specified, tapping it will open the
// app's settings, rather than bringing the application to the foreground.
builder.setSmallIcon(
getAppResourceIdentifier(parts[1], parts[0])
);
} catch (Exception e) {
Logger.error("Could not set notification icon", e);
}
)
)
.setContentText(backgroundMessage)
.setOngoing(true)
.setPriority(Notification.PRIORITY_HIGH)
.setWhen(System.currentTimeMillis());

try {
String color = getAppString(
"capacitor_background_geolocation_notification_color",
null
);
if (color != null) {
builder.setColor(Color.parseColor(color));
}
} catch (Exception e) {
Logger.error("Could not set notification color", e);
}
try {
String name = getAppString(
"capacitor_background_geolocation_notification_icon",
"mipmap/ic_launcher"
);
String[] parts = name.split("/");
// It is actually necessary to set a valid icon for the notification to behave
// correctly when tapped. If there is no icon specified, tapping it will open the
// app's settings, rather than bringing the application to the foreground.
builder.setSmallIcon(
getAppResourceIdentifier(parts[1], parts[0])
);
} catch (Exception e) {
Logger.error("Could not set notification icon", e);
}

Intent launchIntent = getContext().getPackageManager().getLaunchIntentForPackage(
getContext().getPackageName()
try {
String color = getAppString(
"capacitor_background_geolocation_notification_color",
null
);
if (launchIntent != null) {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
builder.setContentIntent(
PendingIntent.getActivity(
getContext(),
0,
launchIntent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE
)
);
if (color != null) {
builder.setColor(Color.parseColor(color));
}
} catch (Exception e) {
Logger.error("Could not set notification color", e);
}

// Set the Channel ID for Android O.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(BackgroundGeolocationService.class.getPackage().getName());
}
Intent launchIntent = getContext().getPackageManager().getLaunchIntentForPackage(
getContext().getPackageName()
);
if (launchIntent != null) {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
builder.setContentIntent(
PendingIntent.getActivity(
getContext(),
0,
launchIntent,
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE
)
);
}

backgroundNotification = builder.build();
// Set the Channel ID for Android O.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(BackgroundGeolocationService.class.getPackage().getName());
}

return builder.build();
}

private void addWatcherAfterLocationPermissionGranted(PluginCall call) {
if (call.getBoolean("stale", false)) {
fetchLastLocation(call);
}
if (call.getString("backgroundMessage") != null && getPermissionState("notification") != PermissionState.GRANTED) {
requestPermissionForAlias("notifications", call, "notificationsPermissionsCallback");
return;
}
service.addWatcher(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be replaced with a direct call to notificationsPermissionsCallback?

call.getCallbackId(),
backgroundNotification,
call.getFloat("distanceFilter", 0f)
call.getCallbackId(),
createNotification(call),
call.getFloat("distanceFilter", 0f)
);
}

Expand All @@ -170,15 +192,22 @@ private void locationPermissionsCallback(PluginCall call) {
call.reject("User denied location permission", "NOT_AUTHORIZED");
return;
}
if (call.getBoolean("stale", false)) {
fetchLastLocation(call);
}
if (service != null) {
service.onPermissionsGranted();
// The handleOnResume method will now be called, and we don't need it to call
// service.onPermissionsGranted again so we reset this flag.
stoppedWithoutPermissions = false;
}
addWatcherAfterLocationPermissionGranted(call);
}

@PermissionCallback
private void notificationsPermissionsCallback(PluginCall call) {
service.addWatcher(
call.getCallbackId(),
createNotification(call),
call.getFloat("distanceFilter", 0f)
);
}

@PluginMethod()
Expand Down