Called on first reception of low-accuracy location data from the network. Should be
* available almost instantly if the user device has network-based or other non-GNSS location
@@ -32,4 +39,14 @@ public interface GNSSStatusUpdateListener {
* adequate GNSS signal reception.
*/
void onAccurateLocationReceived();
+
+ /**
+ * Called if the backend server is unreachable.
+ */
+ void onServerConnectionLost();
+
+ /**
+ * Called if the backend server was unreachable, but is now reachable again.
+ */
+ void onServerConnectionRestored();
}
diff --git a/android/app/src/main/java/info/varden/hauk/manager/ServiceRelauncher.java b/android/app/src/main/java/info/varden/hauk/manager/ServiceRelauncher.java
new file mode 100644
index 0000000..b586acf
--- /dev/null
+++ b/android/app/src/main/java/info/varden/hauk/manager/ServiceRelauncher.java
@@ -0,0 +1,48 @@
+package info.varden.hauk.manager;
+
+import android.content.Context;
+
+import info.varden.hauk.caching.ResumableSessions;
+import info.varden.hauk.caching.ResumeHandler;
+import info.varden.hauk.struct.Session;
+import info.varden.hauk.struct.Share;
+import info.varden.hauk.utils.Log;
+
+/**
+ * {@link ResumeHandler} implementation used by {@link SessionManager} to automatically resume
+ * sessions following a service relaunch. This can happen if the main activity is terminated, but
+ * the share itself keeps running in the background.
+ *
+ * @author Marius Lindvall
+ */
+public final class ServiceRelauncher implements ResumeHandler {
+ /**
+ * The session manager to call to resume the shares.
+ */
+ private final SessionManager manager;
+
+ /**
+ * The manager's resumption handler. This is used to clear the resumption data before the shares
+ * are resumed by the session manager, as the session manager will re-flag the shares as
+ * resumable when it adds them to its internal share list.
+ */
+ private final ResumableSessions resumptionHandler;
+
+ ServiceRelauncher(SessionManager manager, ResumableSessions resumptionHandler) {
+ this.manager = manager;
+ this.resumptionHandler = resumptionHandler;
+ }
+
+ @Override
+ public void onSharesFetched(Context ctx, Session session, Share[] shares) {
+ Log.i("Resuming %s share(s) automatically found for session %s", shares.length, session); //NON-NLS
+ // The shares provided by ResumableSessions do not have a session attached to them. Attach
+ // it to the shares so that they can be shown properly by the prompt and so that the updates
+ // have a backend to be broadcast to when the shares are resumed.
+ this.resumptionHandler.clearResumableSession();
+ for (Share share : shares) {
+ share.setSession(session);
+ this.manager.shareLocation(share, SessionInitiationReason.SERVICE_RELAUNCH);
+ }
+ }
+}
diff --git a/android/app/src/main/java/info/varden/hauk/manager/SessionInitiationReason.java b/android/app/src/main/java/info/varden/hauk/manager/SessionInitiationReason.java
new file mode 100644
index 0000000..eb771bd
--- /dev/null
+++ b/android/app/src/main/java/info/varden/hauk/manager/SessionInitiationReason.java
@@ -0,0 +1,34 @@
+package info.varden.hauk.manager;
+
+import info.varden.hauk.struct.Session;
+import info.varden.hauk.struct.Share;
+
+/**
+ * Describes the reason a session was initiated.
+ *
+ * @author Marius Lindvall
+ */
+public enum SessionInitiationReason {
+ /**
+ * The user requested to start a new sharing session.
+ */
+ USER_STARTED,
+
+ /**
+ * The user requested to resume a previous sharing session.
+ */
+ USER_RESUMED,
+
+ /**
+ * The sharing session is automatically resumed as a result of a relaunch of the location
+ * sharing service.
+ */
+ SERVICE_RELAUNCH,
+
+ /**
+ * The session was created because a share was added to it. This should never be received by
+ * {@link SessionListener#onSessionCreated(Session, Share, SessionInitiationReason)} under any
+ * normal circumstances.
+ */
+ SHARE_ADDED
+}
diff --git a/android/app/src/main/java/info/varden/hauk/manager/SessionListener.java b/android/app/src/main/java/info/varden/hauk/manager/SessionListener.java
index 6f08c12..21cff76 100644
--- a/android/app/src/main/java/info/varden/hauk/manager/SessionListener.java
+++ b/android/app/src/main/java/info/varden/hauk/manager/SessionListener.java
@@ -15,8 +15,9 @@ public interface SessionListener {
*
* @param session The session that was created.
* @param share The share that the session was created for.
+ * @param reason The reason the session was created.
*/
- void onSessionCreated(Session session, Share share);
+ void onSessionCreated(Session session, Share share, SessionInitiationReason reason);
/**
* Called if the session could not be initiated due to missing location permissions.
diff --git a/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java b/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java
index ade2c23..2cf388e 100644
--- a/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java
+++ b/android/app/src/main/java/info/varden/hauk/manager/SessionManager.java
@@ -63,6 +63,12 @@ public abstract class SessionManager {
*/
private final StopSharingCallback stopCallback;
+ /**
+ * Intent for the location pusher, so that it can be stopped if already running when launching
+ * the app.
+ */
+ private static Intent pusher = null;
+
/**
* Android application context.
*/
@@ -121,6 +127,7 @@ public abstract class SessionManager {
// Called when sharing ends. Clear the active session, and all collections of active
// shares present in this class, then propagate this stop message upstream to all
// session listeners.
+ Log.d("Performing stop task cleanup for task %s and stopping timed callback on handler %s", this, SessionManager.this.handler); //NON-NLS
SessionManager.this.activeSession = null;
SessionManager.this.handler.removeCallbacksAndMessages(null);
SessionManager.this.resumable.clearResumableSession();
@@ -182,7 +189,21 @@ public abstract class SessionManager {
* any are found in storage.
*/
public final void resumeShares(ResumePrompt prompt) {
- this.resumable.tryResumeShare(new AutoResumptionPrompter(this, this.resumable, prompt));
+ // Check if the location push service is already running. This will happen if the main UI
+ // activity is killed/stopped, but the app itself and the pushing service keeps running in
+ // the background. If this happens, the push service should be silently restarted to ensure
+ // it behaves properly with new instances of GNSSActiveHandler and StopSharingTask that will
+ // be created and attached when creating a new SessionManager in MainActivity. There is
+ // probably a cleaner way to do this.
+ if (pusher != null) {
+ Log.d("Pusher is non-null (%s), stopping and nulling it and calling service relauncher", pusher); //NON-NLS
+ this.ctx.stopService(pusher);
+ pusher = null;
+ this.resumable.tryResumeShare(new ServiceRelauncher(this, this.resumable));
+ } else {
+ Log.d("Pusher is null, calling resumption prompter"); //NON-NLS
+ this.resumable.tryResumeShare(new AutoResumptionPrompter(this, this.resumable, prompt));
+ }
}
/**
@@ -194,7 +215,7 @@ public abstract class SessionManager {
* @throws LocationServicesDisabledException if location services are disabled.
* @throws LocationPermissionsNotGrantedException if location permissions have not been granted.
*/
- private SessionInitiationPacket.ResponseHandler preSessionInitiation(final SessionInitiationResponseHandler upstreamCallback) throws LocationServicesDisabledException, LocationPermissionsNotGrantedException {
+ private SessionInitiationPacket.ResponseHandler preSessionInitiation(final SessionInitiationResponseHandler upstreamCallback, final SessionInitiationReason reason) throws LocationServicesDisabledException, LocationPermissionsNotGrantedException {
// Check for location permission and prompt the user if missing. This returns because the
// checking function creates async dialogs here - the user is prompted to press the button
// again instead.
@@ -215,7 +236,7 @@ public abstract class SessionManager {
Log.i("Session was initiated for share %s; setting session resumable", share); //NON-NLS
// Proceed with the location share.
- shareLocation(share);
+ shareLocation(share, reason);
upstreamCallback.onSuccess();
}
@@ -250,7 +271,7 @@ public abstract class SessionManager {
* @throws LocationPermissionsNotGrantedException if location permissions have not been granted.
*/
public final void shareLocation(SessionInitiationPacket.InitParameters initParams, SessionInitiationResponseHandler upstreamCallback, AdoptabilityPreference allowAdoption) throws LocationPermissionsNotGrantedException, LocationServicesDisabledException {
- SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback);
+ SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback, SessionInitiationReason.USER_STARTED);
// Create a handshake request and handle the response. The handshake transmits the duration
// and interval to the server and waits for the server to return a session ID to confirm
@@ -269,7 +290,7 @@ public abstract class SessionManager {
* @throws LocationPermissionsNotGrantedException if location permissions have not been granted.
*/
public final void shareLocation(SessionInitiationPacket.InitParameters initParams, SessionInitiationResponseHandler upstreamCallback, String nickname) throws LocationPermissionsNotGrantedException, LocationServicesDisabledException {
- SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback);
+ SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback, SessionInitiationReason.USER_STARTED);
// Create a handshake request and handle the response. The handshake transmits the duration
// and interval to the server and waits for the server to return a session ID to confirm
@@ -289,7 +310,7 @@ public abstract class SessionManager {
* @throws LocationPermissionsNotGrantedException if location permissions have not been granted.
*/
public final void shareLocation(SessionInitiationPacket.InitParameters initParams, SessionInitiationResponseHandler upstreamCallback, String nickname, String groupPin) throws LocationPermissionsNotGrantedException, LocationServicesDisabledException {
- SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback);
+ SessionInitiationPacket.ResponseHandler handler = preSessionInitiation(upstreamCallback, SessionInitiationReason.USER_STARTED);
// Create a handshake request and handle the response. The handshake transmits the duration
// and interval to the server and waits for the server to return a session ID to confirm
@@ -304,10 +325,10 @@ public abstract class SessionManager {
*
* @param share The share to run against the server.
*/
- public final void shareLocation(Share share) {
+ public final void shareLocation(Share share, SessionInitiationReason reason) {
// If we are not already sharing our location, initiate a new session.
if (this.activeSession == null) {
- initiateSessionForExistingShare(share);
+ initiateSessionForExistingShare(share, reason);
}
Log.i("Attaching to share, share=%s", share); //NON-NLS
@@ -349,7 +370,7 @@ public abstract class SessionManager {
*
* @param share The share whose session should be pushed to.
*/
- private void initiateSessionForExistingShare(Share share) {
+ private void initiateSessionForExistingShare(Share share, SessionInitiationReason reason) {
this.activeSession = share.getSession();
this.resumable.setSessionResumable(this.activeSession);
@@ -366,6 +387,7 @@ public abstract class SessionManager {
pusher.setAction(LocationPushService.ACTION_ID);
pusher.putExtra(Constants.EXTRA_SHARE, ReceiverDataRegistry.register(share));
pusher.putExtra(Constants.EXTRA_STOP_TASK, ReceiverDataRegistry.register(this.stopTask));
+ pusher.putExtra(Constants.EXTRA_HANDLER, ReceiverDataRegistry.register(this.handler));
pusher.putExtra(Constants.EXTRA_GNSS_ACTIVE_TASK, ReceiverDataRegistry.register(statusUpdateHandler));
// Android O and higher require the service to be started as a foreground service for it
@@ -382,10 +404,15 @@ public abstract class SessionManager {
// these so that they can be canceled when the location share ends.
this.stopTask.updateTask(pusher);
+ // Required for session relaunches
+ Log.d("Setting static pusher %s (was %s)", pusher, SessionManager.pusher); //NON-NLS
+ //noinspection AssignmentToStaticFieldFromInstanceMethod
+ SessionManager.pusher = pusher;
+
// stopTask is scheduled for expiration, but it could also be called if the user
// manually stops the share, or if the app is destroyed.
long expireIn = share.getSession().getRemainingMillis();
- Log.i("Scheduling session expiration in %s milliseconds", expireIn); //NON-NLS
+ Log.i("Scheduling session task %s for expiration in %s milliseconds on handler %s", this.stopTask, expireIn, this.handler); //NON-NLS
this.handler.postDelayed(this.stopTask, expireIn);
// Push the start event to upstream listeners.
@@ -393,7 +420,7 @@ public abstract class SessionManager {
listener.onStarted();
}
for (SessionListener listener : this.upstreamSessionListeners) {
- listener.onSessionCreated(share.getSession(), share);
+ listener.onSessionCreated(share.getSession(), share, reason);
}
} else {
Log.w("Location permission has not been granted; sharing will not commence"); //NON-NLS
@@ -435,6 +462,13 @@ public abstract class SessionManager {
this.session = session;
}
+ @Override
+ public void onCoarseRebound() {
+ for (GNSSStatusUpdateListener listener : SessionManager.this.upstreamUpdateHandlers) {
+ listener.onGNSSConnectionLost();
+ }
+ }
+
@Override
public void onCoarseLocationReceived() {
for (GNSSStatusUpdateListener listener : SessionManager.this.upstreamUpdateHandlers) {
@@ -449,6 +483,20 @@ public abstract class SessionManager {
}
}
+ @Override
+ public void onServerConnectionLost() {
+ for (GNSSStatusUpdateListener listener : SessionManager.this.upstreamUpdateHandlers) {
+ listener.onServerConnectionLost();
+ }
+ }
+
+ @Override
+ public void onServerConnectionRestored() {
+ for (GNSSStatusUpdateListener listener : SessionManager.this.upstreamUpdateHandlers) {
+ listener.onServerConnectionRestored();
+ }
+ }
+
@Override
public void onShareListReceived(String linkFormat, String[] shareIDs) {
List