From d7416bd045a4f5e94f0f59c4368cb3db1e4c0635 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Thu, 31 May 2018 22:08:23 -0400 Subject: [PATCH 01/16] Add geofencing components --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 10 ++++ .../tasks/AddGeofencesBroadcastReceiver.java | 12 +++++ .../app/tasks/GeofenceTransitionWorker.java | 13 +++++ .../gophillygo/app/tasks/GeofenceWorker.java | 52 +++++++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java create mode 100644 app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java create mode 100644 app/src/main/java/com/gophillygo/app/tasks/GeofenceWorker.java diff --git a/app/build.gradle b/app/build.gradle index d6b0167b..73aa66b9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -55,6 +55,8 @@ dependencies { def playServicesVersion = '15.0.1' implementation "com.google.android.gms:play-services-location:$playServicesVersion" implementation "com.google.android.gms:play-services-maps:$playServicesVersion" + // Android WorkManager + implementation "android.arch.work:work-runtime:1.0.0-alpha02" // Carousel implementation 'com.github.flibbertigibbet:carouselview:1.0.5' // Badges diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cb121af0..8c2666b2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,9 @@ --> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java new file mode 100644 index 00000000..22c3abbf --- /dev/null +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -0,0 +1,12 @@ +package com.gophillygo.app.tasks; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +public class AddGeofencesBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + // TODO: schedule one-time job to set up geofences + } +} diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java new file mode 100644 index 00000000..28c163bc --- /dev/null +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -0,0 +1,13 @@ +package com.gophillygo.app.tasks; + +import android.support.annotation.NonNull; + +import androidx.work.Worker; + +public class GeofenceTransitionWorker extends Worker { + @NonNull + @Override + public WorkerResult doWork() { + return null; + } +} diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceWorker.java new file mode 100644 index 00000000..8d106610 --- /dev/null +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceWorker.java @@ -0,0 +1,52 @@ +package com.gophillygo.app.tasks; + +import android.Manifest; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; + +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingClient; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.LocationServices; + + +import androidx.work.Worker; + +public class GeofenceWorker extends Worker { + + @NonNull + @Override + public WorkerResult doWork() { + Context context = getApplicationContext(); + GeofencingClient geofencingClient = LocationServices.getGeofencingClient(context); + GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); + builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL); + + + builder.addGeofence(new Geofence.Builder().setRequestId("gophillygo") + .setCircularRegion() + .setExpirationDuration() + .setTransitionTypes()) + if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + + + // FIXME: different intent for the transition worker + Intent intent = new Intent(context, AddGeofencesBroadcastReceiver.class); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 007, intent , 0); + geofencingClient.addGeofences(builder.build(), pendingIntent); + } + + return null; + } +} From 644dfbb29ddda1cd71780d6dd4d59cf0c07361a1 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Sat, 2 Jun 2018 20:54:56 -0400 Subject: [PATCH 02/16] Set up geofence workers Listen to broadcasts to add geofences or handle geofence transitions. --- app/src/main/AndroidManifest.xml | 2 +- .../app/tasks/AddGeofenceWorker.java | 99 +++++++++++++++++++ .../tasks/AddGeofencesBroadcastReceiver.java | 37 ++++++- .../GeofenceTransitionBroadcastReceiver.java | 59 +++++++++++ .../app/tasks/GeofenceTransitionWorker.java | 87 +++++++++++++++- .../gophillygo/app/tasks/GeofenceWorker.java | 52 ---------- 6 files changed, 281 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java create mode 100644 app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java delete mode 100644 app/src/main/java/com/gophillygo/app/tasks/GeofenceWorker.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c2666b2..8701659f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -98,7 +98,7 @@ android:value="@string/google_maps_key" /> - + diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java new file mode 100644 index 00000000..8e1c0a49 --- /dev/null +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java @@ -0,0 +1,99 @@ +package com.gophillygo.app.tasks; + +import android.Manifest; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.location.Location; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.util.Log; + +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingClient; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.LocationServices; +import com.gophillygo.app.utils.GpgLocationUtils; + +import androidx.work.Data; +import androidx.work.Worker; + +public class AddGeofenceWorker extends Worker { + + // https://developer.android.com/training/location/geofencing + // Minimum radius should be at least 100 - 150, more for outdoors areas with no WiFi. + // Greater values reduce battery consumption. + private static final int GEOFENCE_RADIUS_METERS = 300; + + // FIXME: configure testing vs production values for these responsiveness values + // Send alert roughly after device has been in geofence for this long. + // Since we are using the DWELL filter, this is about when we will receive notifications. + private static final int GEOFENCE_LOITERING_DELAY = 0; // 300000; // 5 minutes + + // Set responsiveness high to save battery + private static final int GEOFENCE_RESPONSIVENESS = 0; // 300000; // 5 minutes + + private static final String LOG_LABEL = "AddGeofenceWorker"; + private static final int TRANSITION_BROADCAST_REQUEST_CODE = 42; + + public static final String LATITUDES_KEY = "latitudes"; + public static final String LONGITUDES_KEY = "longitudes"; + public static final String GEOFENCE_LABELS_KEY = "geofence_labels"; + + @NonNull + @Override + public WorkerResult doWork() { + Context context = getApplicationContext(); + GeofencingClient geofencingClient = LocationServices.getGeofencingClient(context); + GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); + builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL); + + // Get the data for the locations to add as primitive arrays with label and coordinates at + // matching offsets. + Data data = getInputData(); + double[] latitudes = data.getDoubleArray(LATITUDES_KEY); + double[] longitudes = data.getDoubleArray(LONGITUDES_KEY); + String[] geofenceLabels = data.getStringArray(GEOFENCE_LABELS_KEY); + + if (latitudes.length != longitudes.length || latitudes.length != geofenceLabels.length) { + Log.e(LOG_LABEL, "Location data for geofences to add should be arrays of the same length."); + return WorkerResult.FAILURE; + } + + for (int i = 0; i < latitudes.length; i++) { + builder.addGeofence(new Geofence.Builder() + .setCircularRegion(latitudes[i], longitudes[i], GEOFENCE_RADIUS_METERS) + .setExpirationDuration(Geofence.NEVER_EXPIRE) + .setRequestId(geofenceLabels[i]) + .setLoiteringDelay(GEOFENCE_LOITERING_DELAY) + .setNotificationResponsiveness(GEOFENCE_RESPONSIVENESS) + .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL | Geofence.GEOFENCE_TRANSITION_EXIT) + .build()); + } + + // Check permissions here, but do not prompt if they are missing. + // Location access permissions prompting is handled by `GpgLocationUtils`. + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + Intent intent = new Intent(context, GeofenceTransitionBroadcastReceiver.class); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, + TRANSITION_BROADCAST_REQUEST_CODE, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + try { + geofencingClient.addGeofences(builder.build(), pendingIntent); + return WorkerResult.SUCCESS; + } catch (Exception ex) { + Log.e(LOG_LABEL, "Failed to add geofences"); + Log.e(LOG_LABEL, ex.getMessage()); + return WorkerResult.FAILURE; + } + + } else { + Log.e(LOG_LABEL, "Cannot add geofences because permissions are missing"); + return WorkerResult.FAILURE; + } + } +} diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index 22c3abbf..e2893b1a 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -3,10 +3,45 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.util.Log; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; + +/** + * Add geofences. Expects calling intent to set geofence data as three arrays with + * labels and coordinates. + */ public class AddGeofencesBroadcastReceiver extends BroadcastReceiver { + + private static final String LOG_LABEL = "AddGeofenceBroadcast"; @Override public void onReceive(Context context, Intent intent) { - // TODO: schedule one-time job to set up geofences + // Schedule one-time job to set up geofences + + if (intent.hasExtra(AddGeofenceWorker.LATITUDES_KEY) && intent.hasExtra(AddGeofenceWorker.LONGITUDES_KEY) + && intent.hasExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY)) { + + double[] latitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LATITUDES_KEY); + double[] longitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LONGITUDES_KEY); + String[] labels = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY); + + Data data = new Data.Builder() + .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) + .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) + .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) + .build(); + + // Start a worker to send notifications from a background thread. + OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(GeofenceTransitionWorker.class); + workRequestBuilder.setInputData(data); + // TODO: set constraints and backoff on builder + WorkRequest workRequest = workRequestBuilder.build(); + WorkManager.getInstance().enqueue(workRequest); + } else { + Log.e(LOG_LABEL, "Missing data to add geofences."); + } } } diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java new file mode 100644 index 00000000..b8311479 --- /dev/null +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java @@ -0,0 +1,59 @@ +package com.gophillygo.app.tasks; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingEvent; + +import java.util.ArrayList; +import java.util.List; + +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; + +public class GeofenceTransitionBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + // Start a worker to send notifications from a background thread. + OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(GeofenceTransitionWorker.class); + workRequestBuilder.setInputData(getGeofenceData(intent)); + // TODO: set constraints and backoff on builder + WorkRequest workRequest = workRequestBuilder.build(); + WorkManager.getInstance().enqueue(workRequest); + } + + private Data getGeofenceData(Intent intent) { + GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); + Data.Builder builder = new Data.Builder(); + + Boolean hasError = geofencingEvent.hasError(); + builder.putBoolean(GeofenceTransitionWorker.HAS_ERROR_KEY, hasError); + + if (!geofencingEvent.hasError()) { + int geofenceTransition = geofencingEvent.getGeofenceTransition(); + builder.putInt(GeofenceTransitionWorker.TRANSITION_KEY, geofenceTransition); + + if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL || + geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { + + List triggeringGeofences = geofencingEvent.getTriggeringGeofences(); + ArrayList geofences = new ArrayList<>(triggeringGeofences.size()); + for (Geofence fence : triggeringGeofences) { + geofences.add(fence.getRequestId()); + } + + builder.putStringArray(GeofenceTransitionWorker.TRIGGERING_GEOFENCES, + (String[]) geofences.toArray()); + + } + } else { + builder.putInt(GeofenceTransitionWorker.ERROR_CODE_KEY, geofencingEvent.getErrorCode()); + } + + return builder.build(); + } +} diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java index 28c163bc..8b66e16c 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -1,13 +1,98 @@ package com.gophillygo.app.tasks; import android.support.annotation.NonNull; +import android.util.Log; +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofenceStatusCodes; + +import androidx.work.Data; import androidx.work.Worker; public class GeofenceTransitionWorker extends Worker { + + public static final String HAS_ERROR_KEY = "has_error"; + public static final String ERROR_CODE_KEY = "error_code"; + public static final String TRANSITION_KEY = "transition"; + public static final String TRIGGERING_GEOFENCES = "triggering_geofences"; + + private static final String LOG_LABEL = "GeofenceTransition"; + @NonNull @Override public WorkerResult doWork() { - return null; + + // Geofence event data passed along as primitives + Data data = getInputData(); + + if (data.getBoolean(HAS_ERROR_KEY, true)) { + int error = data.getInt(ERROR_CODE_KEY, GeofenceStatusCodes.DEVELOPER_ERROR); + + switch (error) { + case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE: + Log.e(LOG_LABEL, "Geofence not available"); + // This typically happens after NLP (Android's Network Location Provider) is disabled. + // https://developer.android.com/training/location/geofencing + // FIXME: is the default backoff appropriate? + // TODO: geofences should be re-registered + return WorkerResult.RETRY; + case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES: + // FIXME: ensure there are not more than 100 geofences. Clear them here? + Log.e(LOG_LABEL, "Too many geofences!"); + break; + case GeofenceStatusCodes.TIMEOUT: + // FIXME: what could cause this? + Log.w(LOG_LABEL, "Geofence timeout"); + return WorkerResult.RETRY; + case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS: + Log.e(LOG_LABEL, "Too many geofence pending intents"); + break; + case GeofenceStatusCodes.API_NOT_CONNECTED: + // FIXME: what could cause this? + Log.e(LOG_LABEL, "Geofencing prevented because API not connected"); + return WorkerResult.RETRY; + case GeofenceStatusCodes.CANCELED: + Log.w(LOG_LABEL, "Geofencing cancelled"); + break; + case GeofenceStatusCodes.ERROR: + Log.w(LOG_LABEL, "Geofencing error"); + break; + case GeofenceStatusCodes.DEVELOPER_ERROR: + Log.e(LOG_LABEL, "Geofencing encountered a developer error"); + break; + case GeofenceStatusCodes.INTERNAL_ERROR: + Log.e(LOG_LABEL, "Geofencing encountered an internal error"); + break; + case GeofenceStatusCodes.INTERRUPTED: + // FIXME: what could cause this? + Log.w(LOG_LABEL, "Geofencing interrupted"); + return WorkerResult.RETRY; + default: + Log.w(LOG_LABEL, "Unrecognized GeofenceStatusCodes value: " + error); + } + return WorkerResult.FAILURE; + } + + // Get the transition type. + int geofenceTransition = data.getInt(TRANSITION_KEY, Geofence.GEOFENCE_TRANSITION_EXIT); + Boolean dwellingInGeofence = geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL; + String[] geofences = data.getStringArray(TRIGGERING_GEOFENCES); + + if (geofences.length > 0) { + for (String geofenceID : geofences) { + // TODO: send notification + if (dwellingInGeofence) { + Log.d(LOG_LABEL, "Dwelling in geofence ID " + geofenceID); + } else { + Log.d(LOG_LABEL, "Exited geofence ID " + geofenceID); + } + + } + + return WorkerResult.SUCCESS; + } else { + Log.w(LOG_LABEL, "Received a geofence transition event with no triggering geofences."); + return WorkerResult.SUCCESS; + } } } diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceWorker.java deleted file mode 100644 index 8d106610..00000000 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceWorker.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.gophillygo.app.tasks; - -import android.Manifest; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; - -import com.google.android.gms.location.Geofence; -import com.google.android.gms.location.GeofencingClient; -import com.google.android.gms.location.GeofencingRequest; -import com.google.android.gms.location.LocationServices; - - -import androidx.work.Worker; - -public class GeofenceWorker extends Worker { - - @NonNull - @Override - public WorkerResult doWork() { - Context context = getApplicationContext(); - GeofencingClient geofencingClient = LocationServices.getGeofencingClient(context); - GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); - builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL); - - - builder.addGeofence(new Geofence.Builder().setRequestId("gophillygo") - .setCircularRegion() - .setExpirationDuration() - .setTransitionTypes()) - if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - // TODO: Consider calling - // ActivityCompat#requestPermissions - // here to request the missing permissions, and then overriding - // public void onRequestPermissionsResult(int requestCode, String[] permissions, - // int[] grantResults) - // to handle the case where the user grants the permission. See the documentation - // for ActivityCompat#requestPermissions for more details. - - - // FIXME: different intent for the transition worker - Intent intent = new Intent(context, AddGeofencesBroadcastReceiver.class); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 007, intent , 0); - geofencingClient.addGeofences(builder.build(), pendingIntent); - } - - return null; - } -} From c010f96a91c34d6554d77d8dafeeac8c1e96eea6 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Sat, 2 Jun 2018 20:55:52 -0400 Subject: [PATCH 03/16] Add background location fetch Add method that does not require Activity context to get last known location. Not needed by any background workers yet. --- .../app/utils/GpgLocationUtils.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java b/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java index 62deb45e..356e5582 100644 --- a/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java +++ b/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java @@ -7,6 +7,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.location.Location; +import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.util.Log; import android.widget.Toast; @@ -16,6 +17,8 @@ import com.google.android.gms.location.FusedLocationProviderClient; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; import com.gophillygo.app.R; import java.lang.ref.WeakReference; @@ -86,6 +89,40 @@ public static boolean checkFineLocationPermissions(WeakReference calle return true; } + /** + * Get the last known device location, without requesting to update it or prompting the user + * for permissions if they haven't been granted. + * + * Intended for use by background tasks that do not have an Activity. + * + * @param context Calling context + * @param listener Callback for when location found. Must implement {@link LocationUpdateListener} + * + * @return True if permissions have been already granted + */ + public static boolean getLastKnownLocation(Context context, LocationUpdateListener listener) { + FusedLocationProviderClient client = LocationServices.getFusedLocationProviderClient(context); + + // If permissions haven't been granted already, do not ask for them. + // This is useful for accessing location from background tasks. + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(context, + Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + + client.getLastLocation().addOnCompleteListener(lastLocationTask -> { + if (lastLocationTask.isSuccessful() && lastLocationTask.getResult() != null) { + Log.d(LOG_LABEL, "Found location " + lastLocationTask.getResult()); + listener.locationFound(lastLocationTask.getResult()); + } + }); + + return true; + } + + return false; + } + /** * Check if app has permission and access to device location, and that GPS is present and enabled. * If so, start receiving location updates. From d2acf8ef1e863f14e8640c68f38e140067a04439 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Sat, 2 Jun 2018 22:21:45 -0400 Subject: [PATCH 04/16] Get destinations from DB when adding geofences Set up dependency injection for reading from database to find geofences to add when setting them up again after reboot. --- .idea/misc.xml | 7 +- .../com/gophillygo/app/GoPhillyGoApp.java | 14 ++- .../gophillygo/app/data/DestinationDao.java | 10 ++ .../com/gophillygo/app/di/AppComponent.java | 2 + .../app/tasks/AddGeofenceWorker.java | 9 +- .../tasks/AddGeofencesBroadcastReceiver.java | 117 +++++++++++++++--- 6 files changed, 131 insertions(+), 28 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 5ef8e446..e99bcb2b 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,8 +1,11 @@ - - + + + + + diff --git a/app/src/main/java/com/gophillygo/app/GoPhillyGoApp.java b/app/src/main/java/com/gophillygo/app/GoPhillyGoApp.java index e8be38bd..577592e8 100644 --- a/app/src/main/java/com/gophillygo/app/GoPhillyGoApp.java +++ b/app/src/main/java/com/gophillygo/app/GoPhillyGoApp.java @@ -2,14 +2,17 @@ import android.app.Activity; import android.app.Application; +import android.content.BroadcastReceiver; import android.util.Log; import com.gophillygo.app.di.AppInjector; import javax.inject.Inject; +import dagger.android.AndroidInjector; import dagger.android.DispatchingAndroidInjector; import dagger.android.HasActivityInjector; +import dagger.android.HasBroadcastReceiverInjector; /** * Based on: @@ -17,7 +20,7 @@ */ -public class GoPhillyGoApp extends Application implements HasActivityInjector { +public class GoPhillyGoApp extends Application implements HasActivityInjector, HasBroadcastReceiverInjector { private static final String LOG_LABEL = "GPGApp"; @@ -25,6 +28,10 @@ public class GoPhillyGoApp extends Application implements HasActivityInjector { @Inject DispatchingAndroidInjector dispatchingAndroidInjector; + @SuppressWarnings("WeakerAccess") + @Inject + DispatchingAndroidInjector broadcastReceiverInjector; + @Override public void onCreate() { super.onCreate(); @@ -38,4 +45,9 @@ public void onCreate() { public DispatchingAndroidInjector activityInjector() { return dispatchingAndroidInjector; } + + @Override + public AndroidInjector broadcastReceiverInjector() { + return broadcastReceiverInjector; + } } \ No newline at end of file diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationDao.java b/app/src/main/java/com/gophillygo/app/data/DestinationDao.java index 22e0cbde..e904a169 100644 --- a/app/src/main/java/com/gophillygo/app/data/DestinationDao.java +++ b/app/src/main/java/com/gophillygo/app/data/DestinationDao.java @@ -43,4 +43,14 @@ public void bulkUpdate(List destinations) { update(destination); } } + + /** + * Find those destinations flagged "Want to Go", which are those that should be geofenced. + * + * @return Destination objects, without related event info + */ + @Query("SELECT destination.* FROM destination INNER JOIN attractionflag " + + "ON destination.id == attractionflag.attractionID AND attractionflag.is_event = 0 " + + "WHERE attractionflag.option == 'want_to_go'") + public abstract LiveData> getGeofenceDestinations(); } diff --git a/app/src/main/java/com/gophillygo/app/di/AppComponent.java b/app/src/main/java/com/gophillygo/app/di/AppComponent.java index cc2bb40f..c49885e1 100644 --- a/app/src/main/java/com/gophillygo/app/di/AppComponent.java +++ b/app/src/main/java/com/gophillygo/app/di/AppComponent.java @@ -3,6 +3,8 @@ import android.app.Application; import com.gophillygo.app.GoPhillyGoApp; +import com.gophillygo.app.tasks.GeofenceTransitionBroadcastReceiver; +import com.gophillygo.app.tasks.GeofenceTransitionWorker; import javax.inject.Singleton; diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java index 8e1c0a49..7befde91 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java @@ -5,7 +5,6 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.location.Location; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.util.Log; @@ -14,7 +13,7 @@ import com.google.android.gms.location.GeofencingClient; import com.google.android.gms.location.GeofencingRequest; import com.google.android.gms.location.LocationServices; -import com.gophillygo.app.utils.GpgLocationUtils; +import com.gophillygo.app.BuildConfig; import androidx.work.Data; import androidx.work.Worker; @@ -26,13 +25,13 @@ public class AddGeofenceWorker extends Worker { // Greater values reduce battery consumption. private static final int GEOFENCE_RADIUS_METERS = 300; - // FIXME: configure testing vs production values for these responsiveness values // Send alert roughly after device has been in geofence for this long. // Since we are using the DWELL filter, this is about when we will receive notifications. - private static final int GEOFENCE_LOITERING_DELAY = 0; // 300000; // 5 minutes + // In development (DEBUG build), use no delay. + private static final int GEOFENCE_LOITERING_DELAY = BuildConfig.DEBUG ? 0 : 300000; // 5 minutes // Set responsiveness high to save battery - private static final int GEOFENCE_RESPONSIVENESS = 0; // 300000; // 5 minutes + private static final int GEOFENCE_RESPONSIVENESS = BuildConfig.DEBUG ? 0 : 300000; // 5 minutes private static final String LOG_LABEL = "AddGeofenceWorker"; private static final int TRANSITION_BROADCAST_REQUEST_CODE = 42; diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index e2893b1a..b746690f 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -1,47 +1,124 @@ package com.gophillygo.app.tasks; +import android.arch.lifecycle.Lifecycle; +import android.arch.lifecycle.LifecycleObserver; +import android.arch.lifecycle.LifecycleOwner; +import android.arch.lifecycle.Observer; +import android.arch.lifecycle.ViewModelProviders; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; +import com.gophillygo.app.BuildConfig; +import com.gophillygo.app.data.DestinationDao; +import com.gophillygo.app.data.DestinationViewModel; +import com.gophillygo.app.data.models.Destination; +import com.gophillygo.app.data.models.DestinationLocation; +import com.gophillygo.app.di.GpgViewModelFactory; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + import androidx.work.Data; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import androidx.work.WorkRequest; +import dagger.android.AndroidInjection; /** - * Add geofences. Expects calling intent to set geofence data as three arrays with - * labels and coordinates. + * Add geofences by scheduling one-time worker job. + * + * Expects calling intent to set geofence data as three arrays with labels and coordinates. + * + * Per dagger docs: + * `DaggerBroadcastReceiver` should only be used when the BroadcastReceiver is registered in the AndroidManifest.xml. + * When the BroadcastReceiver is created in your own code, prefer constructor injection instead. + * https://google.github.io/dagger/android.html + * + * This is registered in the manifest to listen for reboots, so we should use the dagger library. */ public class AddGeofencesBroadcastReceiver extends BroadcastReceiver { + @Inject + DestinationDao destinationDao; + private static final String LOG_LABEL = "AddGeofenceBroadcast"; + private static final int MAX_GEOFENCES = 100; // cannot set up more than these + private static final String ADD_GEOFENCE_TAG = "gpg-add-geofences"; + @Override public void onReceive(Context context, Intent intent) { - // Schedule one-time job to set up geofences + AndroidInjection.inject(this, context); + + double[] latitudes; + double[] longitudes; + String[] labels; + // Use intent extras when adding geofence(s) in response to user action. if (intent.hasExtra(AddGeofenceWorker.LATITUDES_KEY) && intent.hasExtra(AddGeofenceWorker.LONGITUDES_KEY) && intent.hasExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY)) { - double[] latitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LATITUDES_KEY); - double[] longitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LONGITUDES_KEY); - String[] labels = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY); - - Data data = new Data.Builder() - .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) - .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) - .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) - .build(); - - // Start a worker to send notifications from a background thread. - OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(GeofenceTransitionWorker.class); - workRequestBuilder.setInputData(data); - // TODO: set constraints and backoff on builder - WorkRequest workRequest = workRequestBuilder.build(); - WorkManager.getInstance().enqueue(workRequest); + latitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LATITUDES_KEY); + longitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LONGITUDES_KEY); + labels = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY); + } else { - Log.e(LOG_LABEL, "Missing data to add geofences."); + Log.d(LOG_LABEL, "Reading data to add geofences from database."); + // Read datatabase instead of relying on an intent with extras; on boot, have no extras set + List destinations = destinationDao.getGeofenceDestinations().getValue(); + if (destinations == null || destinations.isEmpty()) { + Log.d(LOG_LABEL, "Have no destinations with geofences to add."); + return; + } + + int destinationsCount = destinations.size(); + if (destinationsCount > MAX_GEOFENCES) { + // FIXME: handle + Log.e(LOG_LABEL, "Too many destinations with geofences to add."); + return; + } + + // TODO: remove any existing geofences first before adding all from database? + // If got here from reboot event, they shouldn't exist anyways. + + latitudes = new double[destinationsCount]; + longitudes = new double[destinationsCount]; + labels = new String[destinationsCount]; + + for (int i = 0; i < destinationsCount; i++) { + Destination destination = destinations.get(i); + labels[i] = String.valueOf(destination.getId()); + DestinationLocation location = destination.getLocation(); + // FIXME: is this even right + latitudes[i] = location.getX(); + longitudes[i] = location.getY(); + } } + + // Sanity check the data before starting the worker + if (latitudes.length == 0 || latitudes.length != longitudes.length || + latitudes.length != labels.length) { + Log.e(LOG_LABEL, "Extras data of zero or mismatched length found"); + return; + } + + Data data = new Data.Builder() + .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) + .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) + .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) + .build(); + + // Start a worker to add geofences from a background thread. + OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(AddGeofenceWorker.class); + workRequestBuilder.setInputData(data); + workRequestBuilder.addTag(ADD_GEOFENCE_TAG); + // TODO: set constraints and backoff on builder + WorkRequest workRequest = workRequestBuilder.build(); + WorkManager.getInstance().enqueue(workRequest); } } From 86c73bfee98f0004ea6a1db1b5e023a15dfe01f6 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Sun, 3 Jun 2018 02:22:16 -0400 Subject: [PATCH 05/16] Add geofences Start listening to geofence on flag set in UI. --- app/src/main/AndroidManifest.xml | 4 ++ .../app/activities/MapsActivity.java | 34 +++++++++++++++ .../app/activities/PlaceDetailActivity.java | 25 +++++++++++ .../app/activities/PlacesListActivity.java | 20 +++++++++ .../gophillygo/app/data/DestinationDao.java | 2 +- .../app/data/DestinationRepository.java | 4 ++ .../app/data/DestinationViewModel.java | 1 + .../java/com/gophillygo/app/di/AppModule.java | 1 + .../app/tasks/AddGeofenceWorker.java | 43 +++++++++---------- .../tasks/AddGeofencesBroadcastReceiver.java | 43 +++++++++++++------ .../GeofenceTransitionBroadcastReceiver.java | 21 +++++++-- .../app/tasks/GeofenceTransitionWorker.java | 1 + .../app/utils/GpgLocationUtils.java | 3 +- 13 files changed, 159 insertions(+), 43 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8701659f..0ce50dd5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -103,6 +103,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java b/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java index 939fd874..37eed9bb 100644 --- a/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java @@ -27,9 +27,13 @@ import com.google.android.gms.maps.model.MarkerOptions; import com.gophillygo.app.R; import com.gophillygo.app.data.models.Attraction; +import com.gophillygo.app.data.models.AttractionFlag; import com.gophillygo.app.data.models.AttractionInfo; +import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.data.models.DestinationLocation; import com.gophillygo.app.databinding.MapPopupCardBinding; +import com.gophillygo.app.tasks.AddGeofenceWorker; +import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; import com.gophillygo.app.utils.FlagMenuUtils; import com.gophillygo.app.utils.GpgLocationUtils; @@ -37,6 +41,13 @@ import java.util.HashMap; import java.util.Map; +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; + +import static com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver.ADD_GEOFENCE_TAG; + public abstract class MapsActivity extends FilterableListActivity implements OnMapReadyCallback { @@ -136,10 +147,33 @@ public Drawable getFlagImage(AttractionInfo info) { public void optionsButtonClick(View view, T info) { PopupMenu menu = FlagMenuUtils.getFlagPopupMenu(this, view, info.getFlag()); menu.setOnMenuItemClickListener(item -> { + // TODO: support events too + + Boolean haveExistingGeofence = false; + if (!info.getAttraction().isEvent() && info.getFlag().getOption() + .api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { + + haveExistingGeofence = true; + } + info.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(info.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); popupBinding.setAttractionInfo(info); popupBinding.setAttraction(info.getAttraction()); + + if (info.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { + if (haveExistingGeofence) { + Log.d(LOG_LABEL, "No change to geofence"); + return true; // no change + } + + // add geofence + Log.d(LOG_LABEL, "Add geofence from map"); + AddGeofencesBroadcastReceiver.addOneGeofence((Destination)info.getAttraction()); + } else if (haveExistingGeofence) { + // TODO: implement removing geofence + Log.e(LOG_LABEL, "TODO: implement removing geofence"); + } return true; }); } diff --git a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java index 5f2aa76b..eb141b91 100644 --- a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java @@ -11,10 +11,14 @@ import android.view.Gravity; import android.widget.TextView; +import com.gophillygo.app.CarouselViewListener; import com.gophillygo.app.R; import com.gophillygo.app.data.DestinationViewModel; +import com.gophillygo.app.data.models.AttractionFlag; +import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.databinding.ActivityPlaceDetailBinding; import com.gophillygo.app.di.GpgViewModelFactory; +import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; import com.gophillygo.app.utils.FlagMenuUtils; import com.gophillygo.app.utils.UserUuidUtils; import com.synnapps.carouselview.CarouselView; @@ -63,6 +67,8 @@ protected void onCreate(Bundle savedInstanceState) { carouselView = findViewById(R.id.place_detail_carousel); carouselView.setIndicatorGravity(Gravity.CENTER_HORIZONTAL|Gravity.BOTTOM); + carouselView.setImageClickListener(position -> + Log.d(LOG_LABEL, "Clicked item: "+ position)); viewModel = ViewModelProviders.of(this, viewModelFactory) .get(DestinationViewModel.class); @@ -108,8 +114,27 @@ private void displayDestination() { Log.d(LOG_LABEL, "Clicked flags button"); PopupMenu menu = FlagMenuUtils.getFlagPopupMenu(this, flagOptionsCard, destinationInfo.getFlag()); menu.setOnMenuItemClickListener(item -> { + Boolean haveExistingGeofence = false; + if (!destinationInfo.getAttraction().isEvent() && destinationInfo.getFlag().getOption() + .api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { + + haveExistingGeofence = true; + } destinationInfo.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(destinationInfo.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); + if (destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { + if (haveExistingGeofence) { + Log.d(LOG_LABEL, "No change to geofence"); + return true; // no change + } + + // add geofence + Log.d(LOG_LABEL, "Add geofence from place detail"); + AddGeofencesBroadcastReceiver.addOneGeofence(destinationInfo.getAttraction()); + } else if (haveExistingGeofence) { + // TODO: implement removing geofence + Log.e(LOG_LABEL, "TODO: implement removing geofence"); + } return true; }); }); diff --git a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java index 23e0be66..40f7d071 100644 --- a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java @@ -14,10 +14,13 @@ import com.gophillygo.app.R; import com.gophillygo.app.adapters.PlacesListAdapter; +import com.gophillygo.app.data.models.AttractionFlag; import com.gophillygo.app.data.models.AttractionInfo; +import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.data.models.DestinationInfo; import com.gophillygo.app.databinding.ActivityPlacesListBinding; import com.gophillygo.app.databinding.FilterButtonBarBinding; +import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; import java.util.ArrayList; import java.util.List; @@ -66,9 +69,26 @@ public void clickedAttraction(int position) { } public boolean clickedFlagOption(MenuItem item, AttractionInfo destinationInfo, Integer position) { + Boolean haveExistingGeofence = false; + if (destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { + haveExistingGeofence = true; + } destinationInfo.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(destinationInfo.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); placesListAdapter.notifyItemChanged(position); + if (destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { + if (haveExistingGeofence) { + Log.d(LOG_LABEL, "No change to geofence"); + return true; // no change + } + + // add geofence + Log.d(LOG_LABEL, "Add geofence from places list"); + AddGeofencesBroadcastReceiver.addOneGeofence((Destination)destinationInfo.getAttraction()); + } else if (haveExistingGeofence) { + // TODO: implement removing geofence + Log.e(LOG_LABEL, "TODO: implement removing geofence"); + } return true; } diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationDao.java b/app/src/main/java/com/gophillygo/app/data/DestinationDao.java index e904a169..d74f21ed 100644 --- a/app/src/main/java/com/gophillygo/app/data/DestinationDao.java +++ b/app/src/main/java/com/gophillygo/app/data/DestinationDao.java @@ -49,7 +49,7 @@ public void bulkUpdate(List destinations) { * * @return Destination objects, without related event info */ - @Query("SELECT destination.* FROM destination INNER JOIN attractionflag " + + @Query(value = "SELECT destination.* FROM destination INNER JOIN attractionflag " + "ON destination.id == attractionflag.attractionID AND attractionflag.is_event = 0 " + "WHERE attractionflag.option == 'want_to_go'") public abstract LiveData> getGeofenceDestinations(); diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java b/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java index abd3b829..38219515 100644 --- a/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java +++ b/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java @@ -84,6 +84,10 @@ protected Void doInBackground(Void... voids) { @SuppressLint("StaticFieldLeak") public void updateAttractionFlag(AttractionFlag flag, String userUuid, String apiKey) { + + // TODO: remove geofence if should be unset + // TODO: add geofence if want to go + new AsyncTask() { @Override protected Void doInBackground(Void... voids) { diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationViewModel.java b/app/src/main/java/com/gophillygo/app/data/DestinationViewModel.java index a420e164..b65c2556 100644 --- a/app/src/main/java/com/gophillygo/app/data/DestinationViewModel.java +++ b/app/src/main/java/com/gophillygo/app/data/DestinationViewModel.java @@ -2,6 +2,7 @@ import android.arch.lifecycle.LiveData; +import com.gophillygo.app.data.models.AttractionFlag; import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.data.models.DestinationInfo; import com.gophillygo.app.data.networkresource.Resource; diff --git a/app/src/main/java/com/gophillygo/app/di/AppModule.java b/app/src/main/java/com/gophillygo/app/di/AppModule.java index 6211919f..aa5cb16d 100644 --- a/app/src/main/java/com/gophillygo/app/di/AppModule.java +++ b/app/src/main/java/com/gophillygo/app/di/AppModule.java @@ -9,6 +9,7 @@ import com.gophillygo.app.data.EventDao; import com.gophillygo.app.data.GpgDatabase; import com.gophillygo.app.data.networkresource.LiveDataCallAdapterFactory; +import com.gophillygo.app.tasks.GeofenceTransitionWorker; import javax.inject.Singleton; diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java index 7befde91..382fb3a0 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java @@ -1,12 +1,9 @@ package com.gophillygo.app.tasks; -import android.Manifest; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; import android.util.Log; import com.google.android.gms.location.Geofence; @@ -14,6 +11,7 @@ import com.google.android.gms.location.GeofencingRequest; import com.google.android.gms.location.LocationServices; import com.gophillygo.app.BuildConfig; +import com.gophillygo.app.GoPhillyGoApp; import androidx.work.Data; import androidx.work.Worker; @@ -43,7 +41,9 @@ public class AddGeofenceWorker extends Worker { @NonNull @Override public WorkerResult doWork() { + Context context = getApplicationContext(); + GeofencingClient geofencingClient = LocationServices.getGeofencingClient(context); GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL); @@ -71,28 +71,25 @@ public WorkerResult doWork() { .build()); } - // Check permissions here, but do not prompt if they are missing. // Location access permissions prompting is handled by `GpgLocationUtils`. - if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - Intent intent = new Intent(context, GeofenceTransitionBroadcastReceiver.class); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, - TRANSITION_BROADCAST_REQUEST_CODE, - intent, - PendingIntent.FLAG_CANCEL_CURRENT); - - try { - geofencingClient.addGeofences(builder.build(), pendingIntent); - return WorkerResult.SUCCESS; - } catch (Exception ex) { - Log.e(LOG_LABEL, "Failed to add geofences"); - Log.e(LOG_LABEL, ex.getMessage()); - return WorkerResult.FAILURE; - } - - } else { - Log.e(LOG_LABEL, "Cannot add geofences because permissions are missing"); + Intent intent = new Intent(context, GeofenceTransitionBroadcastReceiver.class); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, + TRANSITION_BROADCAST_REQUEST_CODE, + intent, + PendingIntent.FLAG_CANCEL_CURRENT); + + try { + geofencingClient.addGeofences(builder.build(), pendingIntent); + return WorkerResult.SUCCESS; + } catch (SecurityException ex) { + Log.e(LOG_LABEL, "Missing permissions to add geofences"); + ex.printStackTrace(); + return WorkerResult.FAILURE; + } catch (Exception ex) { + Log.e(LOG_LABEL, "Failed to add geofences"); + ex.printStackTrace(); return WorkerResult.FAILURE; } + } } diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index b746690f..129687b8 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -1,25 +1,15 @@ package com.gophillygo.app.tasks; -import android.arch.lifecycle.Lifecycle; -import android.arch.lifecycle.LifecycleObserver; -import android.arch.lifecycle.LifecycleOwner; -import android.arch.lifecycle.Observer; -import android.arch.lifecycle.ViewModelProviders; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.util.Log; -import com.gophillygo.app.BuildConfig; import com.gophillygo.app.data.DestinationDao; -import com.gophillygo.app.data.DestinationViewModel; import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.data.models.DestinationLocation; -import com.gophillygo.app.di.GpgViewModelFactory; -import java.util.ArrayList; import java.util.List; import javax.inject.Inject; @@ -47,9 +37,10 @@ public class AddGeofencesBroadcastReceiver extends BroadcastReceiver { @Inject DestinationDao destinationDao; + public static final String ADD_GEOFENCE_TAG = "gpg-add-geofences"; + private static final String LOG_LABEL = "AddGeofenceBroadcast"; private static final int MAX_GEOFENCES = 100; // cannot set up more than these - private static final String ADD_GEOFENCE_TAG = "gpg-add-geofences"; @Override public void onReceive(Context context, Intent intent) { @@ -94,9 +85,8 @@ public void onReceive(Context context, Intent intent) { Destination destination = destinations.get(i); labels[i] = String.valueOf(destination.getId()); DestinationLocation location = destination.getLocation(); - // FIXME: is this even right - latitudes[i] = location.getX(); - longitudes[i] = location.getY(); + latitudes[i] = location.getY(); + longitudes[i] = location.getX(); } } @@ -113,6 +103,30 @@ public void onReceive(Context context, Intent intent) { .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) .build(); + startWorker(data); + } + + /** + * Convenience static method to start a worker without using the broadcast receiver. + * + * @param destination Destination with a location to use for the geofence to add. + */ + public static void addOneGeofence(@NonNull Destination destination) { + double[] latitudes = {destination.getLocation().getY()}; + double[] longitudes = {destination.getLocation().getX()}; + String[] labels = {String.valueOf(destination.getId())}; + + Data data = new Data.Builder() + .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) + .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) + .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) + .build(); + + Log.d(LOG_LABEL, "addOneGeofence"); + startWorker(data); + } + + private static void startWorker(Data data) { // Start a worker to add geofences from a background thread. OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(AddGeofenceWorker.class); workRequestBuilder.setInputData(data); @@ -120,5 +134,6 @@ public void onReceive(Context context, Intent intent) { // TODO: set constraints and backoff on builder WorkRequest workRequest = workRequestBuilder.build(); WorkManager.getInstance().enqueue(workRequest); + Log.d(LOG_LABEL, "Enqueued new work request to add geofence(s)"); } } diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java index b8311479..d4be5449 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java @@ -3,24 +3,36 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.util.Log; import com.google.android.gms.location.Geofence; import com.google.android.gms.location.GeofencingEvent; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import androidx.work.Data; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import androidx.work.WorkRequest; +import dagger.Provides; +import dagger.android.AndroidInjection; +import dagger.android.ContributesAndroidInjector; public class GeofenceTransitionBroadcastReceiver extends BroadcastReceiver { + + private static final String LOG_LABEL = "GeofenceTransitionBR"; @Override public void onReceive(Context context, Intent intent) { + // TODO: is this necessary? + //AndroidInjection.inject(this, context); + + Log.d(LOG_LABEL, "Received geofence transition event"); // Start a worker to send notifications from a background thread. OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(GeofenceTransitionWorker.class); workRequestBuilder.setInputData(getGeofenceData(intent)); + workRequestBuilder.setInitialDelay(0, TimeUnit.SECONDS); // TODO: set constraints and backoff on builder WorkRequest workRequest = workRequestBuilder.build(); WorkManager.getInstance().enqueue(workRequest); @@ -41,13 +53,14 @@ private Data getGeofenceData(Intent intent) { geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { List triggeringGeofences = geofencingEvent.getTriggeringGeofences(); - ArrayList geofences = new ArrayList<>(triggeringGeofences.size()); + String[] geofences = new String[triggeringGeofences.size()]; + int i = 0; for (Geofence fence : triggeringGeofences) { - geofences.add(fence.getRequestId()); + geofences[i] = (fence.getRequestId()); + i++; } - builder.putStringArray(GeofenceTransitionWorker.TRIGGERING_GEOFENCES, - (String[]) geofences.toArray()); + builder.putStringArray(GeofenceTransitionWorker.TRIGGERING_GEOFENCES, geofences); } } else { diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java index 8b66e16c..01706527 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -8,6 +8,7 @@ import androidx.work.Data; import androidx.work.Worker; +import dagger.android.AndroidInjection; public class GeofenceTransitionWorker extends Worker { diff --git a/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java b/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java index 356e5582..2e8c929c 100644 --- a/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java +++ b/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java @@ -9,6 +9,7 @@ import android.location.Location; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; import android.util.Log; import android.widget.Toast; @@ -105,7 +106,7 @@ public static boolean getLastKnownLocation(Context context, LocationUpdateListen // If permissions haven't been granted already, do not ask for them. // This is useful for accessing location from background tasks. - if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) + if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { From 4a5665ade30549a66e3c0c82430f76b0403b395f Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Sun, 3 Jun 2018 10:32:12 -0400 Subject: [PATCH 06/16] Trigger geofence on enter in debug mode Use dwell in production. --- app/src/main/AndroidManifest.xml | 13 +++++++++---- .../di/AddGeofenceBroadcastReceiverModule.java | 13 +++++++++++++ .../java/com/gophillygo/app/di/AppComponent.java | 6 +++++- ...eofenceTransitionBroadcastReceiverModule.java | 13 +++++++++++++ .../gophillygo/app/tasks/AddGeofenceWorker.java | 16 ++++++++++++---- .../app/tasks/AddGeofencesBroadcastReceiver.java | 1 + .../GeofenceTransitionBroadcastReceiver.java | 8 ++------ .../app/tasks/GeofenceTransitionWorker.java | 8 +++++--- 8 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/gophillygo/app/di/AddGeofenceBroadcastReceiverModule.java create mode 100644 app/src/main/java/com/gophillygo/app/di/GeofenceTransitionBroadcastReceiverModule.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ce50dd5..157c0124 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -97,16 +97,21 @@ android:name="com.google.android.geo.API_KEY" android:value="@string/google_maps_key" /> - + + - - - + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/gophillygo/app/di/AddGeofenceBroadcastReceiverModule.java b/app/src/main/java/com/gophillygo/app/di/AddGeofenceBroadcastReceiverModule.java new file mode 100644 index 00000000..4a78843b --- /dev/null +++ b/app/src/main/java/com/gophillygo/app/di/AddGeofenceBroadcastReceiverModule.java @@ -0,0 +1,13 @@ +package com.gophillygo.app.di; + +import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; + +import dagger.Module; +import dagger.android.ContributesAndroidInjector; + +@Module +public abstract class AddGeofenceBroadcastReceiverModule { + + @ContributesAndroidInjector + abstract AddGeofencesBroadcastReceiver contributeAddGeofenceBroadcastReceiver(); +} diff --git a/app/src/main/java/com/gophillygo/app/di/AppComponent.java b/app/src/main/java/com/gophillygo/app/di/AppComponent.java index c49885e1..7822a7ce 100644 --- a/app/src/main/java/com/gophillygo/app/di/AppComponent.java +++ b/app/src/main/java/com/gophillygo/app/di/AppComponent.java @@ -29,7 +29,11 @@ PlaceDetailActivityModule.class, PlacesListActivityModule.class, EventsMapsActivityModule.class, - PlacesMapsActivityModule.class + PlacesMapsActivityModule.class, + + // Broadcast Receivers + AddGeofenceBroadcastReceiverModule.class, + GeofenceTransitionBroadcastReceiverModule.class }) public interface AppComponent { @Component.Builder diff --git a/app/src/main/java/com/gophillygo/app/di/GeofenceTransitionBroadcastReceiverModule.java b/app/src/main/java/com/gophillygo/app/di/GeofenceTransitionBroadcastReceiverModule.java new file mode 100644 index 00000000..e8aeead8 --- /dev/null +++ b/app/src/main/java/com/gophillygo/app/di/GeofenceTransitionBroadcastReceiverModule.java @@ -0,0 +1,13 @@ +package com.gophillygo.app.di; + +import com.gophillygo.app.tasks.GeofenceTransitionBroadcastReceiver; + +import dagger.Module; +import dagger.android.ContributesAndroidInjector; + +@Module +public abstract class GeofenceTransitionBroadcastReceiverModule { + + @ContributesAndroidInjector + abstract GeofenceTransitionBroadcastReceiver contributeGeofenceTransitionBroadcastReceiver(); +} diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java index 382fb3a0..0b0fef15 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java @@ -11,20 +11,27 @@ import com.google.android.gms.location.GeofencingRequest; import com.google.android.gms.location.LocationServices; import com.gophillygo.app.BuildConfig; -import com.gophillygo.app.GoPhillyGoApp; import androidx.work.Data; import androidx.work.Worker; public class AddGeofenceWorker extends Worker { + // Must match action name in broadcast filter in the manifest + public static final String ACTION_GEOFENCE_TRANSITION = "com.gophillygo.app.tasks.ACTION_GEOFENCE_TRANSITION"; + + // When in development (debug) trigger entry immediately + public static final int GEOFENCE_ENTER_TRIGGER = BuildConfig.DEBUG ? + GeofencingRequest.INITIAL_TRIGGER_ENTER : + GeofencingRequest.INITIAL_TRIGGER_DWELL; + // https://developer.android.com/training/location/geofencing // Minimum radius should be at least 100 - 150, more for outdoors areas with no WiFi. // Greater values reduce battery consumption. private static final int GEOFENCE_RADIUS_METERS = 300; // Send alert roughly after device has been in geofence for this long. - // Since we are using the DWELL filter, this is about when we will receive notifications. + // When we are using the DWELL filter, this is about when we will receive notifications. // In development (DEBUG build), use no delay. private static final int GEOFENCE_LOITERING_DELAY = BuildConfig.DEBUG ? 0 : 300000; // 5 minutes @@ -46,7 +53,7 @@ public WorkerResult doWork() { GeofencingClient geofencingClient = LocationServices.getGeofencingClient(context); GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); - builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL); + builder.setInitialTrigger(GEOFENCE_ENTER_TRIGGER); // Get the data for the locations to add as primitive arrays with label and coordinates at // matching offsets. @@ -67,12 +74,13 @@ public WorkerResult doWork() { .setRequestId(geofenceLabels[i]) .setLoiteringDelay(GEOFENCE_LOITERING_DELAY) .setNotificationResponsiveness(GEOFENCE_RESPONSIVENESS) - .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL | Geofence.GEOFENCE_TRANSITION_EXIT) + .setTransitionTypes(GEOFENCE_ENTER_TRIGGER | Geofence.GEOFENCE_TRANSITION_EXIT) .build()); } // Location access permissions prompting is handled by `GpgLocationUtils`. Intent intent = new Intent(context, GeofenceTransitionBroadcastReceiver.class); + intent.setAction(ACTION_GEOFENCE_TRANSITION); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, TRANSITION_BROADCAST_REQUEST_CODE, intent, diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index 129687b8..c7cfcf48 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -63,6 +63,7 @@ public void onReceive(Context context, Intent intent) { // Read datatabase instead of relying on an intent with extras; on boot, have no extras set List destinations = destinationDao.getGeofenceDestinations().getValue(); if (destinations == null || destinations.isEmpty()) { + // FIXME: this returns empty after reboot, although there are geofences to add Log.d(LOG_LABEL, "Have no destinations with geofences to add."); return; } diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java index d4be5449..49aaed14 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java @@ -8,7 +8,6 @@ import com.google.android.gms.location.Geofence; import com.google.android.gms.location.GeofencingEvent; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -16,17 +15,14 @@ import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import androidx.work.WorkRequest; -import dagger.Provides; import dagger.android.AndroidInjection; -import dagger.android.ContributesAndroidInjector; public class GeofenceTransitionBroadcastReceiver extends BroadcastReceiver { private static final String LOG_LABEL = "GeofenceTransitionBR"; @Override public void onReceive(Context context, Intent intent) { - // TODO: is this necessary? - //AndroidInjection.inject(this, context); + AndroidInjection.inject(this, context); Log.d(LOG_LABEL, "Received geofence transition event"); // Start a worker to send notifications from a background thread. @@ -49,7 +45,7 @@ private Data getGeofenceData(Intent intent) { int geofenceTransition = geofencingEvent.getGeofenceTransition(); builder.putInt(GeofenceTransitionWorker.TRANSITION_KEY, geofenceTransition); - if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL || + if (geofenceTransition == AddGeofenceWorker.GEOFENCE_ENTER_TRIGGER || geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { List triggeringGeofences = geofencingEvent.getTriggeringGeofences(); diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java index 01706527..ff0bc9eb 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -76,16 +76,18 @@ public WorkerResult doWork() { // Get the transition type. int geofenceTransition = data.getInt(TRANSITION_KEY, Geofence.GEOFENCE_TRANSITION_EXIT); - Boolean dwellingInGeofence = geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL; + Boolean enteredGeofence = geofenceTransition == AddGeofenceWorker.GEOFENCE_ENTER_TRIGGER; String[] geofences = data.getStringArray(TRIGGERING_GEOFENCES); if (geofences.length > 0) { for (String geofenceID : geofences) { // TODO: send notification - if (dwellingInGeofence) { - Log.d(LOG_LABEL, "Dwelling in geofence ID " + geofenceID); + if (enteredGeofence) { + Log.d(LOG_LABEL, "Entered geofence ID " + geofenceID); } else { Log.d(LOG_LABEL, "Exited geofence ID " + geofenceID); + // TODO: remove and re-register geofence, or else it will ignore future events + } } From 52b4f8ddcd5bb383d5ff3927ad23adba73c2738b Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Sun, 3 Jun 2018 14:39:22 -0400 Subject: [PATCH 07/16] Read geofences from database on reboot --- app/src/main/AndroidManifest.xml | 3 +- .../activities/AttractionDetailActivity.java | 1 - .../app/activities/EventDetailActivity.java | 2 +- .../app/activities/HomeActivity.java | 4 + .../app/activities/MapsActivity.java | 4 +- .../app/activities/PlaceDetailActivity.java | 4 +- .../app/activities/PlacesListActivity.java | 2 +- .../gophillygo/app/data/DestinationDao.java | 12 +- .../app/data/DestinationRepository.java | 2 +- .../AttractionNetworkBoundResource.java | 2 - .../app/tasks/AddGeofenceWorker.java | 21 +++- .../tasks/AddGeofencesBroadcastReceiver.java | 105 +++++++++++------- 12 files changed, 105 insertions(+), 57 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 157c0124..31933e04 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -101,7 +101,8 @@ - + diff --git a/app/src/main/java/com/gophillygo/app/activities/AttractionDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/AttractionDetailActivity.java index dc341e3f..4bee113a 100644 --- a/app/src/main/java/com/gophillygo/app/activities/AttractionDetailActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/AttractionDetailActivity.java @@ -74,7 +74,6 @@ public void goToMap(View view) { public void goToDirections(View view) { // pass parameters destination and destinationText to https://gophillygo.org/ Uri directionsUri = new Uri.Builder().scheme("https").authority("gophillygo.org") - // TODO: #9 send current user location as origin .appendQueryParameter("origin", "") .appendQueryParameter("originText", "") .appendQueryParameter("destination", destinationInfo.getDestination().getLocation().toString()) diff --git a/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java index bba087c0..30c96260 100644 --- a/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java @@ -92,7 +92,7 @@ protected void onCreate(Bundle savedInstanceState) { viewModel = ViewModelProviders.of(this, viewModelFactory).get(EventViewModel.class); destinationViewModel = ViewModelProviders.of(this, viewModelFactory).get(DestinationViewModel.class); viewModel.getEvent(eventId).observe(this, eventInfo -> { - // TODO: handle if event not found (go to list of events?) + // TODO: #61 handle if event not found (go to list of events?) if (eventInfo == null || eventInfo.getEvent() == null) { Log.e(LOG_LABEL, "No matching event found for ID " + eventId); return; diff --git a/app/src/main/java/com/gophillygo/app/activities/HomeActivity.java b/app/src/main/java/com/gophillygo/app/activities/HomeActivity.java index dab1ad83..ba44325e 100644 --- a/app/src/main/java/com/gophillygo/app/activities/HomeActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/HomeActivity.java @@ -12,9 +12,12 @@ import com.gophillygo.app.CarouselViewListener; import com.gophillygo.app.R; import com.gophillygo.app.adapters.PlaceCategoryGridAdapter; +import com.gophillygo.app.data.models.AttractionFlag; import com.gophillygo.app.data.models.Destination; import com.synnapps.carouselview.CarouselView; +import java.util.List; + public class HomeActivity extends BaseAttractionActivity { @@ -45,6 +48,7 @@ protected void onCreate(Bundle savedInstanceState) { // initialize carousel if destinations already loaded locationOrDestinationsChanged(); + } @Override diff --git a/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java b/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java index 37eed9bb..1050f266 100644 --- a/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java @@ -147,7 +147,7 @@ public Drawable getFlagImage(AttractionInfo info) { public void optionsButtonClick(View view, T info) { PopupMenu menu = FlagMenuUtils.getFlagPopupMenu(this, view, info.getFlag()); menu.setOnMenuItemClickListener(item -> { - // TODO: support events too + // FIXME: support geofencing events too Boolean haveExistingGeofence = false; if (!info.getAttraction().isEvent() && info.getFlag().getOption() @@ -171,7 +171,7 @@ public void optionsButtonClick(View view, T info) { Log.d(LOG_LABEL, "Add geofence from map"); AddGeofencesBroadcastReceiver.addOneGeofence((Destination)info.getAttraction()); } else if (haveExistingGeofence) { - // TODO: implement removing geofence + // FIXME: implement removing geofence Log.e(LOG_LABEL, "TODO: implement removing geofence"); } return true; diff --git a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java index eb141b91..cf43f9c0 100644 --- a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java @@ -73,7 +73,7 @@ protected void onCreate(Bundle savedInstanceState) { viewModel = ViewModelProviders.of(this, viewModelFactory) .get(DestinationViewModel.class); viewModel.getDestination(placeId).observe(this, destinationInfo -> { - // TODO: handle if destination not found (go to list of destinations?) + // TODO: #61 handle if destination not found (go to list of destinations?) if (destinationInfo == null || destinationInfo.getDestination() == null) { Log.e(LOG_LABEL, "No matching destination found for ID " + placeId); return; @@ -132,7 +132,7 @@ private void displayDestination() { Log.d(LOG_LABEL, "Add geofence from place detail"); AddGeofencesBroadcastReceiver.addOneGeofence(destinationInfo.getAttraction()); } else if (haveExistingGeofence) { - // TODO: implement removing geofence + // FIXME: implement removing geofence Log.e(LOG_LABEL, "TODO: implement removing geofence"); } return true; diff --git a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java index 40f7d071..0ad7b869 100644 --- a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java @@ -86,7 +86,7 @@ public boolean clickedFlagOption(MenuItem item, AttractionInfo destinationInfo, Log.d(LOG_LABEL, "Add geofence from places list"); AddGeofencesBroadcastReceiver.addOneGeofence((Destination)destinationInfo.getAttraction()); } else if (haveExistingGeofence) { - // TODO: implement removing geofence + // FIXME: implement removing geofence Log.e(LOG_LABEL, "TODO: implement removing geofence"); } return true; diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationDao.java b/app/src/main/java/com/gophillygo/app/data/DestinationDao.java index d74f21ed..7685f451 100644 --- a/app/src/main/java/com/gophillygo/app/data/DestinationDao.java +++ b/app/src/main/java/com/gophillygo/app/data/DestinationDao.java @@ -5,6 +5,7 @@ import android.arch.persistence.room.Query; import android.arch.persistence.room.Transaction; +import com.gophillygo.app.data.models.AttractionFlag; import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.data.models.DestinationInfo; @@ -45,12 +46,15 @@ public void bulkUpdate(List destinations) { } /** - * Find those destinations flagged "Want to Go", which are those that should be geofenced. + * Find those destinations with a given flag set, which are those that should be geofenced. + * + * Must be accessed on a background thread. * * @return Destination objects, without related event info */ + @Query(value = "SELECT destination.* FROM destination INNER JOIN attractionflag " + - "ON destination.id == attractionflag.attractionID AND attractionflag.is_event = 0 " + - "WHERE attractionflag.option == 'want_to_go'") - public abstract LiveData> getGeofenceDestinations(); + "ON destination.id = attractionflag.attractionID AND attractionflag.is_event = 0 " + + "WHERE attractionflag.option = :geofenceFlagCode") + public abstract List getGeofenceDestinations(int geofenceFlagCode); } diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java b/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java index 38219515..b27fea81 100644 --- a/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java +++ b/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java @@ -154,4 +154,4 @@ protected LiveData> loadFromDb() { } }.getAsLiveData(); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/gophillygo/app/data/networkresource/AttractionNetworkBoundResource.java b/app/src/main/java/com/gophillygo/app/data/networkresource/AttractionNetworkBoundResource.java index 44b98239..59fa4436 100644 --- a/app/src/main/java/com/gophillygo/app/data/networkresource/AttractionNetworkBoundResource.java +++ b/app/src/main/java/com/gophillygo/app/data/networkresource/AttractionNetworkBoundResource.java @@ -65,8 +65,6 @@ protected void saveCallResult(@NonNull DestinationQueryResponse response) { item.setDestination(null); } item.setTimestamp(timestamp); - // TODO: Remove - if (item.getDestination() != null && item.getDestination() == 20) { continue; } eventDao.save(item); } } diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java index 0b0fef15..3923f464 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java @@ -12,6 +12,8 @@ import com.google.android.gms.location.LocationServices; import com.gophillygo.app.BuildConfig; +import java.util.Map; + import androidx.work.Data; import androidx.work.Worker; @@ -55,12 +57,24 @@ public WorkerResult doWork() { GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); builder.setInitialTrigger(GEOFENCE_ENTER_TRIGGER); + double[] latitudes; + double[] longitudes; + String[] geofenceLabels; + // Get the data for the locations to add as primitive arrays with label and coordinates at // matching offsets. Data data = getInputData(); - double[] latitudes = data.getDoubleArray(LATITUDES_KEY); - double[] longitudes = data.getDoubleArray(LONGITUDES_KEY); - String[] geofenceLabels = data.getStringArray(GEOFENCE_LABELS_KEY); + Map map = data.getKeyValueMap(); + if (map.containsKey(LATITUDES_KEY) && map.containsKey(LONGITUDES_KEY) && + map.containsKey(GEOFENCE_LABELS_KEY)) { + + latitudes = data.getDoubleArray(LATITUDES_KEY); + longitudes = data.getDoubleArray(LONGITUDES_KEY); + geofenceLabels = data.getStringArray(GEOFENCE_LABELS_KEY); + } else { + Log.e(LOG_LABEL, "Data missing for geofences to add"); + return WorkerResult.FAILURE; + } if (latitudes.length != longitudes.length || latitudes.length != geofenceLabels.length) { Log.e(LOG_LABEL, "Location data for geofences to add should be arrays of the same length."); @@ -68,6 +82,7 @@ public WorkerResult doWork() { } for (int i = 0; i < latitudes.length; i++) { + Log.d(LOG_LABEL, "Adding geofence for " + geofenceLabels[i]); builder.addGeofence(new Geofence.Builder() .setCircularRegion(latitudes[i], longitudes[i], GEOFENCE_RADIUS_METERS) .setExpirationDuration(Geofence.NEVER_EXPIRE) diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index c7cfcf48..66079535 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -1,12 +1,15 @@ package com.gophillygo.app.tasks; +import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.os.AsyncTask; import android.support.annotation.NonNull; import android.util.Log; import com.gophillygo.app.data.DestinationDao; +import com.gophillygo.app.data.models.AttractionFlag; import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.data.models.DestinationLocation; @@ -42,6 +45,7 @@ public class AddGeofencesBroadcastReceiver extends BroadcastReceiver { private static final String LOG_LABEL = "AddGeofenceBroadcast"; private static final int MAX_GEOFENCES = 100; // cannot set up more than these + @SuppressLint("StaticFieldLeak") @Override public void onReceive(Context context, Intent intent) { AndroidInjection.inject(this, context); @@ -58,53 +62,76 @@ public void onReceive(Context context, Intent intent) { longitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LONGITUDES_KEY); labels = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY); - } else { - Log.d(LOG_LABEL, "Reading data to add geofences from database."); - // Read datatabase instead of relying on an intent with extras; on boot, have no extras set - List destinations = destinationDao.getGeofenceDestinations().getValue(); - if (destinations == null || destinations.isEmpty()) { - // FIXME: this returns empty after reboot, although there are geofences to add - Log.d(LOG_LABEL, "Have no destinations with geofences to add."); + // Sanity check the data before starting the worker + if (latitudes.length == 0 || latitudes.length != longitudes.length || + latitudes.length != labels.length) { + Log.e(LOG_LABEL, "Extras data of zero or mismatched length found"); return; } - int destinationsCount = destinations.size(); - if (destinationsCount > MAX_GEOFENCES) { - // FIXME: handle - Log.e(LOG_LABEL, "Too many destinations with geofences to add."); - return; - } + Data data = new Data.Builder() + .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) + .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) + .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) + .build(); - // TODO: remove any existing geofences first before adding all from database? - // If got here from reboot event, they shouldn't exist anyways. + startWorker(data); - latitudes = new double[destinationsCount]; - longitudes = new double[destinationsCount]; - labels = new String[destinationsCount]; + } else { + Log.d(LOG_LABEL, "Reading data to add geofences from database."); - for (int i = 0; i < destinationsCount; i++) { - Destination destination = destinations.get(i); - labels[i] = String.valueOf(destination.getId()); - DestinationLocation location = destination.getLocation(); - latitudes[i] = location.getY(); - longitudes[i] = location.getX(); - } - } + // Query database on background thread by taking broadcast receiver async briefly + // https://developer.android.com/guide/components/broadcasts + final PendingResult pendingResult = goAsync(); + // Read datatabase instead of relying on an intent with extras; on boot, have no extras set + new AsyncTask() { + @Override + protected Void doInBackground(Void... voids) { + List destinations = destinationDao + .getGeofenceDestinations(AttractionFlag.Option.WantToGo.code); + if (destinations == null || destinations.isEmpty()) { + // FIXME: this returns empty after reboot, although there are geofences to add + Log.d(LOG_LABEL, "Have no destinations with geofences to add."); + return null; + } + + int destinationsCount = destinations.size(); + if (destinationsCount > MAX_GEOFENCES) { + // FIXME: handle + Log.e(LOG_LABEL, "Too many destinations with geofences to add."); + return null; + } + + // TODO: remove any existing geofences first before adding all from database? + // If got here from reboot event, they shouldn't exist anyways. + + double[] latitudes = new double[destinationsCount]; + double[] longitudes = new double[destinationsCount]; + String[] labels = new String[destinationsCount]; + + for (int i = 0; i < destinationsCount; i++) { + Destination destination = destinations.get(i); + labels[i] = String.valueOf(destination.getId()); + DestinationLocation location = destination.getLocation(); + latitudes[i] = location.getY(); + longitudes[i] = location.getX(); + } + + Data data = new Data.Builder() + .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) + .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) + .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) + .build(); + + startWorker(data); + + // Need to release BroadcastReceiver since have gone async + pendingResult.finish(); + return null; + } + }.execute(); - // Sanity check the data before starting the worker - if (latitudes.length == 0 || latitudes.length != longitudes.length || - latitudes.length != labels.length) { - Log.e(LOG_LABEL, "Extras data of zero or mismatched length found"); - return; } - - Data data = new Data.Builder() - .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) - .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) - .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) - .build(); - - startWorker(data); } /** From 49607fd8aeee935fd84d0349c61ff6b9794e0948 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Sun, 3 Jun 2018 15:43:59 -0400 Subject: [PATCH 08/16] Prompt for network location mode settings --- .../activities/BaseAttractionActivity.java | 21 ++++++++- .../app/tasks/GeofenceTransitionWorker.java | 9 ++-- .../app/utils/GpgLocationUtils.java | 44 +++++++++++++++++-- app/src/main/res/values/strings.xml | 1 + example/api_keys.xml | 2 +- 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/gophillygo/app/activities/BaseAttractionActivity.java b/app/src/main/java/com/gophillygo/app/activities/BaseAttractionActivity.java index 05d6b00f..a3209d5e 100644 --- a/app/src/main/java/com/gophillygo/app/activities/BaseAttractionActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/BaseAttractionActivity.java @@ -1,6 +1,7 @@ package com.gophillygo.app.activities; import android.arch.lifecycle.ViewModelProviders; +import android.content.Intent; import android.content.pm.PackageManager; import android.location.Location; import android.os.Bundle; @@ -15,6 +16,8 @@ import com.gophillygo.app.data.models.DestinationLocation; import com.gophillygo.app.data.networkresource.Status; import com.gophillygo.app.di.GpgViewModelFactory; +import com.gophillygo.app.tasks.AddGeofenceWorker; +import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; import com.gophillygo.app.utils.GpgLocationUtils; import com.gophillygo.app.utils.UserUuidUtils; @@ -24,6 +27,9 @@ import javax.inject.Inject; +import androidx.work.Data; +import androidx.work.WorkManager; + /** * Base activity that requests last known location and destination data when opened; * if either change, updates the distances to the destinations and calls @@ -215,12 +221,25 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis if (requestCode == GpgLocationUtils.PERMISSION_REQUEST_ID) { if (grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Log.d(LOG_LABEL, "Re-requesting location after getting permissions"); + Log.d(LOG_LABEL, "Re-requesting location after getting fine location permissions"); fetchLastLocationOrUseDefault(); } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { GpgLocationUtils.displayPermissionRequestRationale(getApplicationContext()); } } + } else if (requestCode == GpgLocationUtils.LOCATION_SETTINGS_REQUEST_ID) { + if (grantResults.length > 0) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Log.d(LOG_LABEL, "Re-requesting location after getting location network permissions"); + fetchLastLocationOrUseDefault(); + Log.d(LOG_LABEL, "Attempting to register geofences from database again"); + Intent intent = new Intent(getApplicationContext(), AddGeofencesBroadcastReceiver.class); + intent.setAction(AddGeofenceWorker.ACTION_GEOFENCE_TRANSITION); + sendBroadcast(intent); + } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) { + Log.w(LOG_LABEL, "Location network permissions not updated; geofencing may not work"); + } + } } } } diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java index ff0bc9eb..8f5a64bb 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -31,12 +31,13 @@ public WorkerResult doWork() { switch (error) { case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE: - Log.e(LOG_LABEL, "Geofence not available"); + Log.e(LOG_LABEL, "Geofence not available; high accuracy location probably not enabled"); // This typically happens after NLP (Android's Network Location Provider) is disabled. // https://developer.android.com/training/location/geofencing - // FIXME: is the default backoff appropriate? - // TODO: geofences should be re-registered - return WorkerResult.RETRY; + // TODO: geofences should be re-registered on PROVIDERS_CHANGED + // but implicit system broadcast cannot read DB in background to find + // what to fence. + break; case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES: // FIXME: ensure there are not more than 100 geofences. Clear them here? Log.e(LOG_LABEL, "Too many geofences!"); diff --git a/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java b/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java index 2e8c929c..bfc891e5 100644 --- a/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java +++ b/app/src/main/java/com/gophillygo/app/utils/GpgLocationUtils.java @@ -5,9 +5,11 @@ import android.app.Activity; import android.app.Dialog; import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; import android.content.pm.PackageManager; import android.location.Location; -import android.support.annotation.NonNull; +import android.provider.Settings; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.util.Log; @@ -15,10 +17,14 @@ import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.common.api.ResolvableApiException; import com.google.android.gms.location.FusedLocationProviderClient; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; -import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.location.LocationSettingsRequest; +import com.google.android.gms.location.LocationSettingsResponse; +import com.google.android.gms.location.LocationSettingsStates; +import com.google.android.gms.location.SettingsClient; import com.google.android.gms.tasks.Task; import com.gophillygo.app.R; @@ -34,7 +40,8 @@ public interface LocationUpdateListener { // identifier for device location access request, if runtime prompt necessary // request code must be in lower 8 bits public static final int PERMISSION_REQUEST_ID = 11; - private static final int API_AVAILABILITY_REQUEST_ID = 22; + public static final int API_AVAILABILITY_REQUEST_ID = 22; + public static final int LOCATION_SETTINGS_REQUEST_ID = 33; private static final int LOCATION_REQUESTS_COUNT = 12; private static final int LOCATION_REQUEST_EXPIRATION_DURATION_MS = 10000; // 10s @@ -87,6 +94,37 @@ public static boolean checkFineLocationPermissions(WeakReference calle return false; // up to the activity to start this service again when permissions granted } + // Check location settings + // https://developer.android.com/training/location/change-location-settings#get-settings + LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder(); + SettingsClient client = LocationServices.getSettingsClient(callingActivity); + Task task = client.checkLocationSettings(builder.build()); + task.addOnFailureListener(e -> { + if (e instanceof ResolvableApiException) { + try { + ResolvableApiException resolvable = (ResolvableApiException) e; + resolvable.startResolutionForResult(callingActivity, LOCATION_SETTINGS_REQUEST_ID); + } catch (IntentSender.SendIntentException e1) { + Log.e(LOG_LABEL, "Failed to prompt user for location settings changes"); + e1.printStackTrace(); + } + } else { + Log.e(LOG_LABEL, "Received unresolvable location settings exception."); + e.printStackTrace(); + } + }).addOnSuccessListener(locationSettingsResponse -> { + LocationSettingsStates states = locationSettingsResponse.getLocationSettingsStates(); + if (!states.isNetworkLocationPresent() || !states.isNetworkLocationUsable()) { + Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + // TODO: a dialog would be easier to read + Toast toast = Toast.makeText(callingActivity, + callingActivity.getString(R.string.location_network_permission_rationale), + Toast.LENGTH_LONG); + toast.show(); + callingActivity.startActivity(intent); + } + }); + return true; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 206562f9..c7a3e6ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,4 +84,5 @@ gophillygo_user_uuid_shared_preferences gophillygo_user_uuid + Please set location mode to \"High Accuracy\" to receive notifications when places you want to go are nearby. diff --git a/example/api_keys.xml b/example/api_keys.xml index 04e14abf..a0c0ea82 100644 --- a/example/api_keys.xml +++ b/example/api_keys.xml @@ -1,6 +1,6 @@ gophillygo_user_uuid Please set location mode to \"High Accuracy\" to receive notifications when places you want to go are nearby. + Nearby GoPhillyGo destinations you want to go visit From 1df21dc0b8b1920bdd141ba2105f00bc2a375190 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Mon, 4 Jun 2018 00:02:14 -0400 Subject: [PATCH 10/16] Expand geofence notifications Open details on notification tap. Include place name in notification message. --- .../app/tasks/AddGeofenceWorker.java | 12 ++- .../tasks/AddGeofencesBroadcastReceiver.java | 11 ++- .../GeofenceTransitionBroadcastReceiver.java | 32 +++++-- .../app/tasks/GeofenceTransitionWorker.java | 90 ++++++++++++------- app/src/main/res/values/strings.xml | 1 + 5 files changed, 105 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java index 3923f464..c244801f 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java @@ -46,6 +46,7 @@ public class AddGeofenceWorker extends Worker { public static final String LATITUDES_KEY = "latitudes"; public static final String LONGITUDES_KEY = "longitudes"; public static final String GEOFENCE_LABELS_KEY = "geofence_labels"; + public static final String GEOFENCE_NAMES_KEY = "geofence_names"; @NonNull @Override @@ -60,23 +61,26 @@ public WorkerResult doWork() { double[] latitudes; double[] longitudes; String[] geofenceLabels; + String[] geofenceNames; // Get the data for the locations to add as primitive arrays with label and coordinates at // matching offsets. Data data = getInputData(); Map map = data.getKeyValueMap(); if (map.containsKey(LATITUDES_KEY) && map.containsKey(LONGITUDES_KEY) && - map.containsKey(GEOFENCE_LABELS_KEY)) { + map.containsKey(GEOFENCE_LABELS_KEY) && map.containsKey(GEOFENCE_NAMES_KEY)) { latitudes = data.getDoubleArray(LATITUDES_KEY); longitudes = data.getDoubleArray(LONGITUDES_KEY); geofenceLabels = data.getStringArray(GEOFENCE_LABELS_KEY); + geofenceNames = data.getStringArray(GEOFENCE_NAMES_KEY); } else { Log.e(LOG_LABEL, "Data missing for geofences to add"); return WorkerResult.FAILURE; } - if (latitudes.length != longitudes.length || latitudes.length != geofenceLabels.length) { + if (latitudes.length != longitudes.length || latitudes.length != geofenceLabels.length || + latitudes.length != geofenceNames.length) { Log.e(LOG_LABEL, "Location data for geofences to add should be arrays of the same length."); return WorkerResult.FAILURE; } @@ -95,11 +99,13 @@ public WorkerResult doWork() { // Location access permissions prompting is handled by `GpgLocationUtils`. Intent intent = new Intent(context, GeofenceTransitionBroadcastReceiver.class); + intent.putExtra(GEOFENCE_LABELS_KEY, geofenceLabels); + intent.putExtra(GEOFENCE_NAMES_KEY, geofenceNames); intent.setAction(ACTION_GEOFENCE_TRANSITION); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, TRANSITION_BROADCAST_REQUEST_CODE, intent, - PendingIntent.FLAG_CANCEL_CURRENT); + PendingIntent.FLAG_UPDATE_CURRENT); try { geofencingClient.addGeofences(builder.build(), pendingIntent); diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index 66079535..1078794b 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -53,14 +53,17 @@ public void onReceive(Context context, Intent intent) { double[] latitudes; double[] longitudes; String[] labels; + String[] names; // Use intent extras when adding geofence(s) in response to user action. if (intent.hasExtra(AddGeofenceWorker.LATITUDES_KEY) && intent.hasExtra(AddGeofenceWorker.LONGITUDES_KEY) - && intent.hasExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY)) { + && intent.hasExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY) && + intent.hasExtra(AddGeofenceWorker.GEOFENCE_NAMES_KEY)) { latitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LATITUDES_KEY); longitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LONGITUDES_KEY); labels = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY); + names = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_NAMES_KEY); // Sanity check the data before starting the worker if (latitudes.length == 0 || latitudes.length != longitudes.length || @@ -73,6 +76,7 @@ public void onReceive(Context context, Intent intent) { .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) + .putStringArray(AddGeofenceWorker.GEOFENCE_NAMES_KEY, names) .build(); startWorker(data); @@ -108,10 +112,12 @@ protected Void doInBackground(Void... voids) { double[] latitudes = new double[destinationsCount]; double[] longitudes = new double[destinationsCount]; String[] labels = new String[destinationsCount]; + String[] names = new String[destinationsCount]; for (int i = 0; i < destinationsCount; i++) { Destination destination = destinations.get(i); labels[i] = String.valueOf(destination.getId()); + names[i] = destination.getName(); DestinationLocation location = destination.getLocation(); latitudes[i] = location.getY(); longitudes[i] = location.getX(); @@ -121,6 +127,7 @@ protected Void doInBackground(Void... voids) { .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) + .putStringArray(AddGeofenceWorker.GEOFENCE_NAMES_KEY, names) .build(); startWorker(data); @@ -143,11 +150,13 @@ public static void addOneGeofence(@NonNull Destination destination) { double[] latitudes = {destination.getLocation().getY()}; double[] longitudes = {destination.getLocation().getX()}; String[] labels = {String.valueOf(destination.getId())}; + String[] names = {String.valueOf(destination.getName())}; Data data = new Data.Builder() .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) .putStringArray(AddGeofenceWorker.GEOFENCE_LABELS_KEY, labels) + .putStringArray(AddGeofenceWorker.GEOFENCE_NAMES_KEY, names) .build(); Log.d(LOG_LABEL, "addOneGeofence"); diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java index 49aaed14..9568b8db 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java @@ -8,7 +8,9 @@ import com.google.android.gms.location.Geofence; import com.google.android.gms.location.GeofencingEvent; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import androidx.work.Data; @@ -50,14 +52,32 @@ private Data getGeofenceData(Intent intent) { List triggeringGeofences = geofencingEvent.getTriggeringGeofences(); String[] geofences = new String[triggeringGeofences.size()]; - int i = 0; - for (Geofence fence : triggeringGeofences) { - geofences[i] = (fence.getRequestId()); - i++; - } + String[] geofenceNames = new String[triggeringGeofences.size()]; + + if (intent.hasExtra(AddGeofenceWorker.GEOFENCE_NAMES_KEY) && + intent.hasExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY)) { + + // get full set of IDs (labels) and place names that are flagged for geofencing + String[] labels = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY); + String[] names = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_NAMES_KEY); - builder.putStringArray(GeofenceTransitionWorker.TRIGGERING_GEOFENCES, geofences); + Map geofenceNameMap = new HashMap<>(labels.length); + for (int i = 0; i < labels.length; i++) { + geofenceNameMap.put(labels[i], names[i]); + } + int i = 0; + for (Geofence fence : triggeringGeofences) { + geofences[i] = (fence.getRequestId()); + geofenceNames[i] = geofenceNameMap.get(fence.getRequestId()); + i++; + } + + builder.putStringArray(GeofenceTransitionWorker.TRIGGERING_GEOFENCES, geofences); + builder.putStringArray(AddGeofenceWorker.GEOFENCE_NAMES_KEY, geofenceNames); + } else { + Log.e(LOG_LABEL, "Broadcast intent is missing the geofence labels and/or names"); + } } } else { builder.putInt(GeofenceTransitionWorker.ERROR_CODE_KEY, geofencingEvent.getErrorCode()); diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java index f5b6451a..b8c35002 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -1,18 +1,24 @@ package com.gophillygo.app.tasks; +import android.annotation.SuppressLint; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.app.TaskStackBuilder; import android.util.Log; import com.google.android.gms.location.Geofence; import com.google.android.gms.location.GeofenceStatusCodes; import com.gophillygo.app.R; +import com.gophillygo.app.activities.PlaceDetailActivity; import androidx.work.Data; import androidx.work.Worker; @@ -30,6 +36,7 @@ public class GeofenceTransitionWorker extends Worker { @NonNull @Override + @SuppressLint("StringFormatInvalid") public WorkerResult doWork() { // Geofence event data passed along as primitives @@ -84,31 +91,47 @@ public WorkerResult doWork() { return WorkerResult.FAILURE; } + Context context = getApplicationContext(); + // Get the transition type. int geofenceTransition = data.getInt(TRANSITION_KEY, Geofence.GEOFENCE_TRANSITION_EXIT); Boolean enteredGeofence = geofenceTransition == AddGeofenceWorker.GEOFENCE_ENTER_TRIGGER; String[] geofences = data.getStringArray(TRIGGERING_GEOFENCES); + String[] geofencePlaceNames = data.getStringArray(AddGeofenceWorker.GEOFENCE_NAMES_KEY); if (geofences.length > 0) { Handler handler = new Handler(Looper.getMainLooper()); - for (String geofenceID : geofences) { + for (int i = 0; i < geofences.length; i++) { + String geofenceID = geofences[i]; + String placeName = geofencePlaceNames[i]; // TODO: send notification if (enteredGeofence) { Log.d(LOG_LABEL, "Entered geofence ID " + geofenceID); - // FIXME: set up channel - // https://developer.android.com/training/notify-user/build-notification - handler.post(() -> { - createNotificationChannel(); + // Get intent for the detail view to open on notification click. + Intent intent = new Intent(context, PlaceDetailActivity.class); + intent.putExtra(PlaceDetailActivity.DESTINATION_ID_KEY, Long.valueOf(geofenceID)); + + // Add the intent to the stack builder, which inflates the back stack + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + stackBuilder.addNextIntentWithParentStack(intent); + // Get the PendingIntent containing the entire back stack + PendingIntent resultPendingIntent = + stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + + createNotificationChannel(context); // show on UI thread - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) - .setSmallIcon(R.drawable.watershed_alliance_full_icon_300x104px) - .setContentTitle("Near a place") - .setContentText("Something interesting is nearby") - .setPriority(NotificationCompat.PRIORITY_DEFAULT); + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, CHANNEL_ID) + // TODO: use app icon of some sort + .setSmallIcon(R.drawable.ic_flag_blue_24dp) + .setContentTitle(placeName) + .setContentText(context.getString(R.string.place_nearby_notification, placeName)) + .setContentIntent(resultPendingIntent) + .setAutoCancel(true) // close notifcation when tapped + .setPriority(NotificationCompat.PRIORITY_HIGH); - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); // notificationId is a unique int for each notification that you must define notificationManager.notify(Integer.valueOf(geofenceID), mBuilder.build()); @@ -117,21 +140,12 @@ public WorkerResult doWork() { } else { Log.d(LOG_LABEL, "Exited geofence ID " + geofenceID); // TODO: remove and re-register geofence, or else it will ignore future events - createNotificationChannel(); handler.post(() -> { - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) - .setSmallIcon(R.drawable.watershed_alliance_full_icon_300x104px) - .setContentTitle("Left a place") - .setContentText("Passed by something interesting nearby") - .setPriority(NotificationCompat.PRIORITY_DEFAULT); - - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(getApplicationContext()); - - // notificationId is a unique int for each notification that you must define - notificationManager.notify(Integer.valueOf(geofenceID), mBuilder.build()); + Log.d(LOG_LABEL, "Removing notification for geofence"); + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + notificationManager.cancel(Integer.valueOf(geofenceID)); }); } - } return WorkerResult.SUCCESS; @@ -141,19 +155,33 @@ public WorkerResult doWork() { } } - private void createNotificationChannel() { - // Create the NotificationChannel, but only on API 26+ because - // the NotificationChannel class is new and not in the support library + /** + * Create the NotificationChannel, but only on API 26+ because the NotificationChannel class + * is new and not in the support library. + * https://developer.android.com/training/notify-user/build-notification + * + * @param context Application context + */ + private static void createNotificationChannel(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - CharSequence name = CHANNEL_ID; - String description = getApplicationContext().getString(R.string.channel_description); + String description = context.getString(R.string.channel_description); int importance = NotificationManager.IMPORTANCE_DEFAULT; - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_ID, importance); channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this - NotificationManager notificationManager = getApplicationContext().getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + if (notificationManager != null) { + NotificationChannel existingChannel = notificationManager.getNotificationChannel(CHANNEL_ID); + if (existingChannel == null) { + Log.d(LOG_LABEL, "Creating new notification channel for app:"); + notificationManager.createNotificationChannel(channel); + } else { + Log.d(LOG_LABEL, "Have notification channel set up already"); + } + } else { + Log.e(LOG_LABEL, "Failed to get notification manager"); + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dba7dbb2..39cc2621 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,4 +86,5 @@ gophillygo_user_uuid Please set location mode to \"High Accuracy\" to receive notifications when places you want to go are nearby. Nearby GoPhillyGo destinations you want to go visit + Are you visiting %1$s now? Learn more! From 299eb845a93304e2ca9c9d16ff4ef1fc77bcd072 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Mon, 4 Jun 2018 10:57:20 -0400 Subject: [PATCH 11/16] Remove geofences Add worker to remove geofences after user unselects "want to go" for a place, and also to re-register geofences on exit. --- .../app/activities/MapsActivity.java | 5 +- .../app/activities/PlaceDetailActivity.java | 5 +- .../app/activities/PlacesListActivity.java | 5 +- .../app/data/DestinationRepository.java | 3 - .../app/tasks/AddGeofenceWorker.java | 2 + .../tasks/AddGeofencesBroadcastReceiver.java | 19 ++--- .../GeofenceTransitionBroadcastReceiver.java | 28 +++++--- .../app/tasks/GeofenceTransitionWorker.java | 13 +++- .../app/tasks/RemoveGeofenceWorker.java | 70 +++++++++++++++++++ 9 files changed, 122 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/gophillygo/app/tasks/RemoveGeofenceWorker.java diff --git a/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java b/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java index 1050f266..dcc53dd8 100644 --- a/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java @@ -34,6 +34,7 @@ import com.gophillygo.app.databinding.MapPopupCardBinding; import com.gophillygo.app.tasks.AddGeofenceWorker; import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; +import com.gophillygo.app.tasks.RemoveGeofenceWorker; import com.gophillygo.app.utils.FlagMenuUtils; import com.gophillygo.app.utils.GpgLocationUtils; @@ -171,8 +172,8 @@ public void optionsButtonClick(View view, T info) { Log.d(LOG_LABEL, "Add geofence from map"); AddGeofencesBroadcastReceiver.addOneGeofence((Destination)info.getAttraction()); } else if (haveExistingGeofence) { - // FIXME: implement removing geofence - Log.e(LOG_LABEL, "TODO: implement removing geofence"); + Log.e(LOG_LABEL, "Removing geofence"); + RemoveGeofenceWorker.removeOneGeofence(String.valueOf(info.getAttraction().getId())); } return true; }); diff --git a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java index cf43f9c0..1c4e428f 100644 --- a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java @@ -19,6 +19,7 @@ import com.gophillygo.app.databinding.ActivityPlaceDetailBinding; import com.gophillygo.app.di.GpgViewModelFactory; import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; +import com.gophillygo.app.tasks.RemoveGeofenceWorker; import com.gophillygo.app.utils.FlagMenuUtils; import com.gophillygo.app.utils.UserUuidUtils; import com.synnapps.carouselview.CarouselView; @@ -132,8 +133,8 @@ private void displayDestination() { Log.d(LOG_LABEL, "Add geofence from place detail"); AddGeofencesBroadcastReceiver.addOneGeofence(destinationInfo.getAttraction()); } else if (haveExistingGeofence) { - // FIXME: implement removing geofence - Log.e(LOG_LABEL, "TODO: implement removing geofence"); + Log.e(LOG_LABEL, "Removing geofence"); + RemoveGeofenceWorker.removeOneGeofence(String.valueOf(destinationInfo.getAttraction().getId())); } return true; }); diff --git a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java index 0ad7b869..e6265c98 100644 --- a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java @@ -21,6 +21,7 @@ import com.gophillygo.app.databinding.ActivityPlacesListBinding; import com.gophillygo.app.databinding.FilterButtonBarBinding; import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; +import com.gophillygo.app.tasks.RemoveGeofenceWorker; import java.util.ArrayList; import java.util.List; @@ -86,8 +87,8 @@ public boolean clickedFlagOption(MenuItem item, AttractionInfo destinationInfo, Log.d(LOG_LABEL, "Add geofence from places list"); AddGeofencesBroadcastReceiver.addOneGeofence((Destination)destinationInfo.getAttraction()); } else if (haveExistingGeofence) { - // FIXME: implement removing geofence - Log.e(LOG_LABEL, "TODO: implement removing geofence"); + Log.e(LOG_LABEL, "Removing geofence"); + RemoveGeofenceWorker.removeOneGeofence(String.valueOf(destinationInfo.getAttraction().getId())); } return true; } diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java b/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java index b27fea81..c950c5df 100644 --- a/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java +++ b/app/src/main/java/com/gophillygo/app/data/DestinationRepository.java @@ -85,9 +85,6 @@ protected Void doInBackground(Void... voids) { @SuppressLint("StaticFieldLeak") public void updateAttractionFlag(AttractionFlag flag, String userUuid, String apiKey) { - // TODO: remove geofence if should be unset - // TODO: add geofence if want to go - new AsyncTask() { @Override protected Void doInBackground(Void... voids) { diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java index c244801f..cb49ec5b 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java @@ -101,6 +101,8 @@ public WorkerResult doWork() { Intent intent = new Intent(context, GeofenceTransitionBroadcastReceiver.class); intent.putExtra(GEOFENCE_LABELS_KEY, geofenceLabels); intent.putExtra(GEOFENCE_NAMES_KEY, geofenceNames); + intent.putExtra(LATITUDES_KEY, latitudes); + intent.putExtra(LONGITUDES_KEY, longitudes); intent.setAction(ACTION_GEOFENCE_TRANSITION); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, TRANSITION_BROADCAST_REQUEST_CODE, diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index 1078794b..6f0bac54 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -94,7 +94,6 @@ protected Void doInBackground(Void... voids) { List destinations = destinationDao .getGeofenceDestinations(AttractionFlag.Option.WantToGo.code); if (destinations == null || destinations.isEmpty()) { - // FIXME: this returns empty after reboot, although there are geofences to add Log.d(LOG_LABEL, "Have no destinations with geofences to add."); return null; } @@ -106,9 +105,6 @@ protected Void doInBackground(Void... voids) { return null; } - // TODO: remove any existing geofences first before adding all from database? - // If got here from reboot event, they shouldn't exist anyways. - double[] latitudes = new double[destinationsCount]; double[] longitudes = new double[destinationsCount]; String[] labels = new String[destinationsCount]; @@ -147,10 +143,15 @@ protected Void doInBackground(Void... voids) { * @param destination Destination with a location to use for the geofence to add. */ public static void addOneGeofence(@NonNull Destination destination) { - double[] latitudes = {destination.getLocation().getY()}; - double[] longitudes = {destination.getLocation().getX()}; - String[] labels = {String.valueOf(destination.getId())}; - String[] names = {String.valueOf(destination.getName())}; + addOneGeofence(destination.getLocation().getX(), destination.getLocation().getY(), + String.valueOf(destination.getId()), String.valueOf(destination.getName())); + } + + public static void addOneGeofence(double x, double y, @NonNull String label, @NonNull String name) { + double[] latitudes = {y}; + double[] longitudes = {x}; + String[] labels = {label}; + String[] names = {name}; Data data = new Data.Builder() .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) @@ -163,7 +164,7 @@ public static void addOneGeofence(@NonNull Destination destination) { startWorker(data); } - private static void startWorker(Data data) { + public static void startWorker(Data data) { // Start a worker to add geofences from a background thread. OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(AddGeofenceWorker.class); workRequestBuilder.setInputData(data); diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java index 9568b8db..913b691d 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java @@ -51,30 +51,42 @@ private Data getGeofenceData(Intent intent) { geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { List triggeringGeofences = geofencingEvent.getTriggeringGeofences(); - String[] geofences = new String[triggeringGeofences.size()]; - String[] geofenceNames = new String[triggeringGeofences.size()]; + int numGeofences = triggeringGeofences.size(); + String[] geofences = new String[numGeofences]; + String[] geofenceNames = new String[numGeofences]; + double[] geofenceLatitudes = new double[numGeofences]; + double[] geofenceLongitudes = new double[numGeofences]; if (intent.hasExtra(AddGeofenceWorker.GEOFENCE_NAMES_KEY) && - intent.hasExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY)) { + intent.hasExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY) && + intent.hasExtra(AddGeofenceWorker.LATITUDES_KEY) && + intent.hasExtra(AddGeofenceWorker.LONGITUDES_KEY)) { - // get full set of IDs (labels) and place names that are flagged for geofencing + // get full set of info for places that are flagged for geofencing String[] labels = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY); String[] names = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_NAMES_KEY); + double[] latitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LATITUDES_KEY); + double[] longitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LONGITUDES_KEY); - Map geofenceNameMap = new HashMap<>(labels.length); + Map geofenceNameMap = new HashMap<>(labels.length); for (int i = 0; i < labels.length; i++) { - geofenceNameMap.put(labels[i], names[i]); + geofenceNameMap.put(labels[i], i); } int i = 0; for (Geofence fence : triggeringGeofences) { - geofences[i] = (fence.getRequestId()); - geofenceNames[i] = geofenceNameMap.get(fence.getRequestId()); + int index = geofenceNameMap.get(fence.getRequestId()); + geofences[i] = fence.getRequestId(); + geofenceNames[i] = names[index]; + geofenceLatitudes[i] = latitudes[index]; + geofenceLongitudes[i] = longitudes[index]; i++; } builder.putStringArray(GeofenceTransitionWorker.TRIGGERING_GEOFENCES, geofences); builder.putStringArray(AddGeofenceWorker.GEOFENCE_NAMES_KEY, geofenceNames); + builder.putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, geofenceLatitudes); + builder.putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, geofenceLongitudes); } else { Log.e(LOG_LABEL, "Broadcast intent is missing the geofence labels and/or names"); } diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java index b8c35002..ab11d9ee 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -98,13 +98,18 @@ public WorkerResult doWork() { Boolean enteredGeofence = geofenceTransition == AddGeofenceWorker.GEOFENCE_ENTER_TRIGGER; String[] geofences = data.getStringArray(TRIGGERING_GEOFENCES); String[] geofencePlaceNames = data.getStringArray(AddGeofenceWorker.GEOFENCE_NAMES_KEY); + double[] latitudes = data.getDoubleArray(AddGeofenceWorker.LATITUDES_KEY); + double[] longitudes = data.getDoubleArray(AddGeofenceWorker.LONGITUDES_KEY); if (geofences.length > 0) { + // Send notification the main thread Handler handler = new Handler(Looper.getMainLooper()); for (int i = 0; i < geofences.length; i++) { String geofenceID = geofences[i]; String placeName = geofencePlaceNames[i]; - // TODO: send notification + double latitude = latitudes[i]; + double longitude = longitudes[i]; + if (enteredGeofence) { Log.d(LOG_LABEL, "Entered geofence ID " + geofenceID); @@ -139,12 +144,16 @@ public WorkerResult doWork() { } else { Log.d(LOG_LABEL, "Exited geofence ID " + geofenceID); - // TODO: remove and re-register geofence, or else it will ignore future events handler.post(() -> { Log.d(LOG_LABEL, "Removing notification for geofence"); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.cancel(Integer.valueOf(geofenceID)); }); + + Log.d(LOG_LABEL, "Re-registering geofence after exit"); + // remove and re-register geofence, or else it will ignore future events + RemoveGeofenceWorker.removeOneGeofence(geofenceID); + AddGeofencesBroadcastReceiver.addOneGeofence(longitude, latitude, geofenceID, placeName); } } diff --git a/app/src/main/java/com/gophillygo/app/tasks/RemoveGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/RemoveGeofenceWorker.java new file mode 100644 index 00000000..a0f4bc1c --- /dev/null +++ b/app/src/main/java/com/gophillygo/app/tasks/RemoveGeofenceWorker.java @@ -0,0 +1,70 @@ +package com.gophillygo.app.tasks; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.google.android.gms.location.GeofencingClient; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.tasks.OnFailureListener; +import com.google.android.gms.tasks.OnSuccessListener; + +import java.util.ArrayList; +import java.util.Arrays; + +import androidx.work.Data; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; +import androidx.work.Worker; + +public class RemoveGeofenceWorker extends Worker { + + private static final String REMOVE_GEOFENCES_KEY = "remove_geofences"; + private static final String REMOVE_GEOFENCE_TAG = "gpg-remove-geofences"; + private static final String LOG_LABEL = "RemoveGeofenceWorker"; + + @NonNull + @Override + public WorkerResult doWork() { + GeofencingClient geofencingClient = LocationServices.getGeofencingClient(getApplicationContext()); + + Data data = getInputData(); + if (data.getKeyValueMap().containsKey(REMOVE_GEOFENCES_KEY)) { + String[] removeGeofences = data.getStringArray(REMOVE_GEOFENCES_KEY); + Log.d(LOG_LABEL, "Going to remove " + removeGeofences.length + " geofences"); + geofencingClient.removeGeofences(new ArrayList<>(Arrays.asList(removeGeofences))).addOnSuccessListener(aVoid -> { + Log.d(LOG_LABEL, removeGeofences.length + " geofence(s) removed successfully"); + }).addOnFailureListener(e -> { + Log.d(LOG_LABEL, "Failed to remove " + removeGeofences.length + " geofences."); + }); + return WorkerResult.SUCCESS; + } else { + Log.e(LOG_LABEL, "Did not receive data for geofences to remove"); + return WorkerResult.FAILURE; + } + } + + /** + * Start a worker to remove a single geofence with the given place ID. + * + * @param geofenceId ID of the geofenced destination to stop geofencing + */ + public static void removeOneGeofence(String geofenceId) { + String[] geofences = {geofenceId}; + + Data data = new Data.Builder() + .putStringArray(REMOVE_GEOFENCES_KEY, geofences) + .build(); + + Log.d(LOG_LABEL, "removeOneGeofence"); + + OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(RemoveGeofenceWorker.class); + workRequestBuilder.setInputData(data); + workRequestBuilder.addTag(REMOVE_GEOFENCE_TAG); + // TODO: set constraints and backoff on builder + WorkRequest workRequest = workRequestBuilder.build(); + WorkManager.getInstance().enqueue(workRequest); + Log.d(LOG_LABEL, "Enqueued new work request to remove one geofence"); + } +} From ea81ce599c794b55749c4974426b891d2d0f99f9 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Mon, 4 Jun 2018 11:06:54 -0400 Subject: [PATCH 12/16] Finish geofence broadcast receiver in async handlers Ensure background task gets released after worker started. --- .../app/tasks/AddGeofencesBroadcastReceiver.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index 6f0bac54..51def38f 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -89,6 +89,17 @@ public void onReceive(Context context, Intent intent) { final PendingResult pendingResult = goAsync(); // Read datatabase instead of relying on an intent with extras; on boot, have no extras set new AsyncTask() { + @Override + protected void onPostExecute(Void aVoid) { + // Need to release BroadcastReceiver since have gone async + pendingResult.finish(); + } + + @Override + protected void onCancelled() { + pendingResult.abortBroadcast(); + } + @Override protected Void doInBackground(Void... voids) { List destinations = destinationDao @@ -127,9 +138,6 @@ protected Void doInBackground(Void... voids) { .build(); startWorker(data); - - // Need to release BroadcastReceiver since have gone async - pendingResult.finish(); return null; } }.execute(); From 901b74af684af8c1322a82898803381496d1c746 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Mon, 4 Jun 2018 11:18:39 -0400 Subject: [PATCH 13/16] Revise geofencing error handling --- .../gophillygo/app/tasks/GeofenceTransitionWorker.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java index ab11d9ee..3f1bc6b6 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -45,9 +45,10 @@ public WorkerResult doWork() { if (data.getBoolean(HAS_ERROR_KEY, true)) { int error = data.getInt(ERROR_CODE_KEY, GeofenceStatusCodes.DEVELOPER_ERROR); + // https://developers.google.com/android/reference/com/google/android/gms/location/GeofenceStatusCodes switch (error) { case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE: - Log.e(LOG_LABEL, "Geofence not available; high accuracy location probably not enabled"); + Log.e(LOG_LABEL, "Geofencing service not available; high accuracy location probably not enabled"); // This typically happens after NLP (Android's Network Location Provider) is disabled. // https://developer.android.com/training/location/geofencing // TODO: geofences should be re-registered on PROVIDERS_CHANGED @@ -59,14 +60,12 @@ public WorkerResult doWork() { Log.e(LOG_LABEL, "Too many geofences!"); break; case GeofenceStatusCodes.TIMEOUT: - // FIXME: what could cause this? Log.w(LOG_LABEL, "Geofence timeout"); return WorkerResult.RETRY; case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS: - Log.e(LOG_LABEL, "Too many geofence pending intents"); - break; + Log.e(LOG_LABEL, "Too many pending intents to addGeofence. Max is 5."); + return WorkerResult.RETRY; case GeofenceStatusCodes.API_NOT_CONNECTED: - // FIXME: what could cause this? Log.e(LOG_LABEL, "Geofencing prevented because API not connected"); return WorkerResult.RETRY; case GeofenceStatusCodes.CANCELED: @@ -82,7 +81,6 @@ public WorkerResult doWork() { Log.e(LOG_LABEL, "Geofencing encountered an internal error"); break; case GeofenceStatusCodes.INTERRUPTED: - // FIXME: what could cause this? Log.w(LOG_LABEL, "Geofencing interrupted"); return WorkerResult.RETRY; default: From a5bbb35fbcb31e3a74761a2f8f15ec5b3858b478 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Mon, 4 Jun 2018 14:40:21 -0400 Subject: [PATCH 14/16] Add geofencing for events --- .../app/activities/EventDetailActivity.java | 18 ++++++ .../app/activities/EventsListActivity.java | 25 ++++++++ .../app/activities/MapsActivity.java | 19 +++--- .../app/activities/PlaceDetailActivity.java | 7 +-- .../app/activities/PlacesListActivity.java | 8 +-- .../com/gophillygo/app/data/EventDao.java | 19 ++++++ .../app/tasks/AddGeofenceWorker.java | 4 +- .../tasks/AddGeofencesBroadcastReceiver.java | 61 ++++++++++++++++--- .../GeofenceTransitionBroadcastReceiver.java | 24 ++++++-- .../app/tasks/GeofenceTransitionWorker.java | 36 +++++++---- 10 files changed, 175 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java index 30c96260..d4fa8dd6 100644 --- a/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java @@ -20,11 +20,14 @@ import com.gophillygo.app.R; import com.gophillygo.app.data.DestinationViewModel; import com.gophillygo.app.data.EventViewModel; +import com.gophillygo.app.data.models.AttractionFlag; import com.gophillygo.app.data.models.DestinationInfo; import com.gophillygo.app.data.models.Event; import com.gophillygo.app.data.models.EventInfo; import com.gophillygo.app.databinding.ActivityEventDetailBinding; import com.gophillygo.app.di.GpgViewModelFactory; +import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; +import com.gophillygo.app.tasks.RemoveGeofenceWorker; import com.gophillygo.app.utils.FlagMenuUtils; import com.gophillygo.app.utils.UserUuidUtils; import com.synnapps.carouselview.CarouselView; @@ -185,9 +188,24 @@ private void displayEvent() { Log.d(LOG_LABEL, "Clicked flags button"); PopupMenu menu = FlagMenuUtils.getFlagPopupMenu(this, flagOptionsCard, eventInfo.getFlag()); menu.setOnMenuItemClickListener(item -> { + Boolean haveExistingGeofence = eventInfo.getFlag().getOption() + .api_name.equals(AttractionFlag.Option.WantToGo.api_name); + eventInfo.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(eventInfo.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); + if (eventInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { + if (haveExistingGeofence) { + Log.d(LOG_LABEL, "No change to event geofence"); + return true; // no change + } + // add geofence + Log.d(LOG_LABEL, "Add event geofence from event list"); + AddGeofencesBroadcastReceiver.addOneGeofence(eventInfo); + } else if (haveExistingGeofence) { + Log.e(LOG_LABEL, "Removing geofence from event list"); + RemoveGeofenceWorker.removeOneGeofence(String.valueOf(eventInfo.getAttraction().getId())); + } return true; }); }); diff --git a/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java b/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java index b49eea7e..a27e9e28 100644 --- a/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java @@ -17,6 +17,7 @@ import com.gophillygo.app.R; import com.gophillygo.app.adapters.EventsListAdapter; import com.gophillygo.app.data.EventViewModel; +import com.gophillygo.app.data.models.AttractionFlag; import com.gophillygo.app.data.models.AttractionInfo; import com.gophillygo.app.data.models.EventInfo; import com.gophillygo.app.data.networkresource.Resource; @@ -24,6 +25,8 @@ import com.gophillygo.app.databinding.ActivityEventsListBinding; import com.gophillygo.app.databinding.FilterButtonBarBinding; import com.gophillygo.app.di.GpgViewModelFactory; +import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; +import com.gophillygo.app.tasks.RemoveGeofenceWorker; import java.util.ArrayList; import java.util.List; @@ -65,9 +68,31 @@ public void clickedAttraction(int position) { } public boolean clickedFlagOption(MenuItem item, AttractionInfo eventInfo, Integer position) { + Boolean haveExistingGeofence = eventInfo.getFlag() + .getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name); + eventInfo.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(eventInfo.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); adapter.notifyItemChanged(position); + + if (((EventInfo)eventInfo).hasDestinationName()) { + if (eventInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { + if (haveExistingGeofence) { + Log.d(LOG_LABEL, "No change to event geofence"); + return true; // no change + } + // add geofence + Log.d(LOG_LABEL, "Add event geofence from event list"); + AddGeofencesBroadcastReceiver.addOneGeofence((EventInfo)eventInfo); + } else if (haveExistingGeofence) { + Log.e(LOG_LABEL, "Removing geofence from event list"); + RemoveGeofenceWorker.removeOneGeofence(String.valueOf(eventInfo.getAttraction().getId())); + } + } else { + // TODO: notify user? + Log.w(LOG_LABEL, "Cannot add geofence for an event without an associated destination"); + } + return true; } diff --git a/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java b/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java index dcc53dd8..be1baf40 100644 --- a/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java @@ -31,6 +31,7 @@ import com.gophillygo.app.data.models.AttractionInfo; import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.data.models.DestinationLocation; +import com.gophillygo.app.data.models.EventInfo; import com.gophillygo.app.databinding.MapPopupCardBinding; import com.gophillygo.app.tasks.AddGeofenceWorker; import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; @@ -148,14 +149,8 @@ public Drawable getFlagImage(AttractionInfo info) { public void optionsButtonClick(View view, T info) { PopupMenu menu = FlagMenuUtils.getFlagPopupMenu(this, view, info.getFlag()); menu.setOnMenuItemClickListener(item -> { - // FIXME: support geofencing events too - - Boolean haveExistingGeofence = false; - if (!info.getAttraction().isEvent() && info.getFlag().getOption() - .api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { - - haveExistingGeofence = true; - } + Boolean haveExistingGeofence = info.getFlag().getOption() + .api_name.equals(AttractionFlag.Option.WantToGo.api_name); info.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(info.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); @@ -170,7 +165,13 @@ public void optionsButtonClick(View view, T info) { // add geofence Log.d(LOG_LABEL, "Add geofence from map"); - AddGeofencesBroadcastReceiver.addOneGeofence((Destination)info.getAttraction()); + if (info.getAttraction().isEvent()) { + // event + AddGeofencesBroadcastReceiver.addOneGeofence((EventInfo)info); + } else { + // destination + AddGeofencesBroadcastReceiver.addOneGeofence((Destination)info.getAttraction()); + } } else if (haveExistingGeofence) { Log.e(LOG_LABEL, "Removing geofence"); RemoveGeofenceWorker.removeOneGeofence(String.valueOf(info.getAttraction().getId())); diff --git a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java index 1c4e428f..2d2a2e6b 100644 --- a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java @@ -115,12 +115,9 @@ private void displayDestination() { Log.d(LOG_LABEL, "Clicked flags button"); PopupMenu menu = FlagMenuUtils.getFlagPopupMenu(this, flagOptionsCard, destinationInfo.getFlag()); menu.setOnMenuItemClickListener(item -> { - Boolean haveExistingGeofence = false; - if (!destinationInfo.getAttraction().isEvent() && destinationInfo.getFlag().getOption() - .api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { + Boolean haveExistingGeofence = destinationInfo.getFlag().getOption() + .api_name.equals(AttractionFlag.Option.WantToGo.api_name); - haveExistingGeofence = true; - } destinationInfo.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(destinationInfo.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); if (destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { diff --git a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java index e6265c98..ce131c3b 100644 --- a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java @@ -70,13 +70,13 @@ public void clickedAttraction(int position) { } public boolean clickedFlagOption(MenuItem item, AttractionInfo destinationInfo, Integer position) { - Boolean haveExistingGeofence = false; - if (destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { - haveExistingGeofence = true; - } + Boolean haveExistingGeofence = destinationInfo.getFlag().getOption() + .api_name.equals(AttractionFlag.Option.WantToGo.api_name); + destinationInfo.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(destinationInfo.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); placesListAdapter.notifyItemChanged(position); + if (destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { if (haveExistingGeofence) { Log.d(LOG_LABEL, "No change to geofence"); diff --git a/app/src/main/java/com/gophillygo/app/data/EventDao.java b/app/src/main/java/com/gophillygo/app/data/EventDao.java index 887caff8..35ebff2f 100644 --- a/app/src/main/java/com/gophillygo/app/data/EventDao.java +++ b/app/src/main/java/com/gophillygo/app/data/EventDao.java @@ -46,4 +46,23 @@ public void bulkUpdate(List events) { update(event); } } + + /** + * Find those events that both have a given flag set, and have an associated destination, + * which are those events that should be geofenced. + * + * Must be accessed on a background thread. + * + * @return EventInfo objects + */ + + @Query("SELECT event.*, destination.name AS destinationName, destination.distance AS distance, " + + "destination.categories AS destinationCategories, attractionflag.option, " + + "destination.x, destination.y, destination.distance " + + "FROM event " + + "INNER JOIN destination ON destination.id = event.destination " + + "LEFT JOIN attractionflag " + + "ON event.id = attractionflag.attractionID AND attractionflag.is_event = 1 " + + "WHERE attractionflag.option = :geofenceFlagCode") + public abstract List getGeofenceEvents(int geofenceFlagCode); } diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java index cb49ec5b..4baba04c 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java @@ -104,10 +104,12 @@ public WorkerResult doWork() { intent.putExtra(LATITUDES_KEY, latitudes); intent.putExtra(LONGITUDES_KEY, longitudes); intent.setAction(ACTION_GEOFENCE_TRANSITION); + // FIXME: max of 5 pending intents can resolve at once, so cannot send more than + // five notifications at a time. How to address? PendingIntent pendingIntent = PendingIntent.getBroadcast(context, TRANSITION_BROADCAST_REQUEST_CODE, intent, - PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent.FLAG_ONE_SHOT); try { geofencingClient.addGeofences(builder.build(), pendingIntent); diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index 51def38f..ae88887b 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -9,10 +9,14 @@ import android.util.Log; import com.gophillygo.app.data.DestinationDao; +import com.gophillygo.app.data.EventDao; import com.gophillygo.app.data.models.AttractionFlag; import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.data.models.DestinationLocation; +import com.gophillygo.app.data.models.Event; +import com.gophillygo.app.data.models.EventInfo; +import java.util.ArrayList; import java.util.List; import javax.inject.Inject; @@ -40,6 +44,9 @@ public class AddGeofencesBroadcastReceiver extends BroadcastReceiver { @Inject DestinationDao destinationDao; + @Inject + EventDao eventDao; + public static final String ADD_GEOFENCE_TAG = "gpg-add-geofences"; private static final String LOG_LABEL = "AddGeofenceBroadcast"; @@ -104,32 +111,55 @@ protected void onCancelled() { protected Void doInBackground(Void... voids) { List destinations = destinationDao .getGeofenceDestinations(AttractionFlag.Option.WantToGo.code); - if (destinations == null || destinations.isEmpty()) { - Log.d(LOG_LABEL, "Have no destinations with geofences to add."); + List events = eventDao.getGeofenceEvents(AttractionFlag.Option.WantToGo.code); + + // Check that there is at least one place to geofence and prevent NPEs by + // initializing null lists + if ((events == null || events.isEmpty()) && + (destinations == null || destinations.isEmpty())) { + Log.d(LOG_LABEL, "Have no destinations or events with geofences to add."); return null; + } else if (events == null) { + events = new ArrayList<>(0); + } else if (destinations == null) { + destinations = new ArrayList<>(0); } int destinationsCount = destinations.size(); - if (destinationsCount > MAX_GEOFENCES) { - // FIXME: handle + int geofencesCount = destinationsCount + events.size(); + if (geofencesCount > MAX_GEOFENCES) { + // FIXME: handle having too many geofences Log.e(LOG_LABEL, "Too many destinations with geofences to add."); return null; } - double[] latitudes = new double[destinationsCount]; - double[] longitudes = new double[destinationsCount]; - String[] labels = new String[destinationsCount]; - String[] names = new String[destinationsCount]; + // send arrays of combined values for destinations and events + double[] latitudes = new double[geofencesCount]; + double[] longitudes = new double[geofencesCount]; + String[] labels = new String[geofencesCount]; + String[] names = new String[geofencesCount]; + // add destinations to the beginning for (int i = 0; i < destinationsCount; i++) { Destination destination = destinations.get(i); - labels[i] = String.valueOf(destination.getId()); + labels[i] = "d" + String.valueOf(destination.getId()); names[i] = destination.getName(); DestinationLocation location = destination.getLocation(); latitudes[i] = location.getY(); longitudes[i] = location.getX(); } + // add events to the end + for (int i = 0; i < geofencesCount; i++) { + int combinedIndex = i + destinationsCount; + EventInfo eventInfo = events.get(i); + labels[combinedIndex] = "e" + String.valueOf(eventInfo.getAttraction().getId()); + names[combinedIndex] = eventInfo.getEvent().getName(); + DestinationLocation location = eventInfo.getLocation(); + latitudes[combinedIndex] = location.getY(); + latitudes[combinedIndex] = location.getX(); + } + Data data = new Data.Builder() .putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) @@ -152,7 +182,18 @@ protected Void doInBackground(Void... voids) { */ public static void addOneGeofence(@NonNull Destination destination) { addOneGeofence(destination.getLocation().getX(), destination.getLocation().getY(), - String.valueOf(destination.getId()), String.valueOf(destination.getName())); + "d" + String.valueOf(destination.getId()), String.valueOf(destination.getName())); + } + + public static void addOneGeofence(@NonNull EventInfo eventInfo) { + DestinationLocation location = eventInfo.getLocation(); + if (location != null) { + Event event = eventInfo.getEvent(); + addOneGeofence(location.getX(), location.getY(), + "e" + String.valueOf(event.getId()), event.getName()); + } else { + Log.e(LOG_LABEL, "Cannot add geofence for event without associated location."); + } } public static void addOneGeofence(double x, double y, @NonNull String label, @NonNull String name) { diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java index 913b691d..857efb50 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java @@ -68,6 +68,7 @@ private Data getGeofenceData(Intent intent) { double[] latitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LATITUDES_KEY); double[] longitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LONGITUDES_KEY); + // map the geofence string IDs to their index in the full value arrays Map geofenceNameMap = new HashMap<>(labels.length); for (int i = 0; i < labels.length; i++) { geofenceNameMap.put(labels[i], i); @@ -75,12 +76,23 @@ private Data getGeofenceData(Intent intent) { int i = 0; for (Geofence fence : triggeringGeofences) { - int index = geofenceNameMap.get(fence.getRequestId()); - geofences[i] = fence.getRequestId(); - geofenceNames[i] = names[index]; - geofenceLatitudes[i] = latitudes[index]; - geofenceLongitudes[i] = longitudes[index]; - i++; + String fenceId = fence.getRequestId(); + Log.d(LOG_LABEL, "Handling transition for geofence " + fenceId); + try { + int index = geofenceNameMap.get(fenceId); + geofences[i] = fenceId; + geofenceNames[i] = names[index]; + geofenceLatitudes[i] = latitudes[index]; + geofenceLongitudes[i] = longitudes[index]; + i++; + } catch (NullPointerException npe) { + // FIXME: why does this happen sometimes? + Log.e(LOG_LABEL, "Null pointer exception attempting to find geofence in map"); + Log.e(LOG_LABEL, "Fence ID: " + fenceId); + for (String label: labels) { + Log.d(LOG_LABEL, "Found label " + label); + } + } } builder.putStringArray(GeofenceTransitionWorker.TRIGGERING_GEOFENCES, geofences); diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java index 3f1bc6b6..83903332 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -18,6 +18,7 @@ import com.google.android.gms.location.Geofence; import com.google.android.gms.location.GeofenceStatusCodes; import com.gophillygo.app.R; +import com.gophillygo.app.activities.EventDetailActivity; import com.gophillygo.app.activities.PlaceDetailActivity; import androidx.work.Data; @@ -56,7 +57,6 @@ public WorkerResult doWork() { // what to fence. break; case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES: - // FIXME: ensure there are not more than 100 geofences. Clear them here? Log.e(LOG_LABEL, "Too many geofences!"); break; case GeofenceStatusCodes.TIMEOUT: @@ -103,18 +103,32 @@ public WorkerResult doWork() { // Send notification the main thread Handler handler = new Handler(Looper.getMainLooper()); for (int i = 0; i < geofences.length; i++) { - String geofenceID = geofences[i]; + // Need a unique int we can find later, for the notification + String geofenceLabel = geofences[i]; String placeName = geofencePlaceNames[i]; double latitude = latitudes[i]; double longitude = longitudes[i]; + // Geofence string ID is "d" for destination or "e" for event, followed by the + // destination or event integer ID. + int geofenceId = Integer.valueOf(geofenceLabel.substring(1)); + boolean isEvent = geofenceLabel.startsWith("e"); + String notificationTag = isEvent ? "e" : "d"; + if (enteredGeofence) { - Log.d(LOG_LABEL, "Entered geofence ID " + geofenceID); + Log.d(LOG_LABEL, "Entered geofence ID " + geofenceLabel); handler.post(() -> { // Get intent for the detail view to open on notification click. - Intent intent = new Intent(context, PlaceDetailActivity.class); - intent.putExtra(PlaceDetailActivity.DESTINATION_ID_KEY, Long.valueOf(geofenceID)); + Intent intent; + if (isEvent) { + intent = new Intent(context, EventDetailActivity.class); + intent.putExtra(EventDetailActivity.EVENT_ID_KEY, (long)geofenceId); + } else { + intent = new Intent(context, PlaceDetailActivity.class); + intent.putExtra(PlaceDetailActivity.DESTINATION_ID_KEY, (long)geofenceId); + } + // Add the intent to the stack builder, which inflates the back stack TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); @@ -136,22 +150,22 @@ public WorkerResult doWork() { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - // notificationId is a unique int for each notification that you must define - notificationManager.notify(Integer.valueOf(geofenceID), mBuilder.build()); + // The pair of notification string tag and int must be unique for the app + notificationManager.notify(notificationTag, geofenceId, mBuilder.build()); }); } else { - Log.d(LOG_LABEL, "Exited geofence ID " + geofenceID); + Log.d(LOG_LABEL, "Exited geofence ID " + geofenceLabel); handler.post(() -> { Log.d(LOG_LABEL, "Removing notification for geofence"); NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - notificationManager.cancel(Integer.valueOf(geofenceID)); + notificationManager.cancel(notificationTag, geofenceId); }); Log.d(LOG_LABEL, "Re-registering geofence after exit"); // remove and re-register geofence, or else it will ignore future events - RemoveGeofenceWorker.removeOneGeofence(geofenceID); - AddGeofencesBroadcastReceiver.addOneGeofence(longitude, latitude, geofenceID, placeName); + RemoveGeofenceWorker.removeOneGeofence(geofenceLabel); + AddGeofencesBroadcastReceiver.addOneGeofence(longitude, latitude, geofenceLabel, placeName); } } From 9c91da9662da39fb7ddc2551e765d1a2c1a13b95 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Tue, 5 Jun 2018 00:37:59 -0400 Subject: [PATCH 15/16] Query database in geofence transition broadcast receiver Intent callbacks for geofences may return places not added with the registered callback intent, so its data must come from the database on handling the event. --- .../gophillygo/app/data/DestinationDao.java | 11 ++ .../com/gophillygo/app/data/EventDao.java | 16 ++ .../app/tasks/AddGeofenceWorker.java | 10 +- .../tasks/AddGeofencesBroadcastReceiver.java | 2 +- .../GeofenceTransitionBroadcastReceiver.java | 146 +++++++++++------- .../app/tasks/GeofenceTransitionWorker.java | 109 +++++++------ 6 files changed, 182 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/com/gophillygo/app/data/DestinationDao.java b/app/src/main/java/com/gophillygo/app/data/DestinationDao.java index 7685f451..007a96ed 100644 --- a/app/src/main/java/com/gophillygo/app/data/DestinationDao.java +++ b/app/src/main/java/com/gophillygo/app/data/DestinationDao.java @@ -57,4 +57,15 @@ public void bulkUpdate(List destinations) { "ON destination.id = attractionflag.attractionID AND attractionflag.is_event = 0 " + "WHERE attractionflag.option = :geofenceFlagCode") public abstract List getGeofenceDestinations(int geofenceFlagCode); + + /** + * Get a single destination. + * + * Must be accessed from a background thread. + * + * @param destinationId ID of place to fetch + * @return Matching destination, with related event count and user flag. + */ + @Query("SELECT * FROM destination WHERE destination.id = :destinationId") + public abstract Destination getDestinationInBackground(long destinationId); } diff --git a/app/src/main/java/com/gophillygo/app/data/EventDao.java b/app/src/main/java/com/gophillygo/app/data/EventDao.java index 35ebff2f..2dc4b029 100644 --- a/app/src/main/java/com/gophillygo/app/data/EventDao.java +++ b/app/src/main/java/com/gophillygo/app/data/EventDao.java @@ -65,4 +65,20 @@ public void bulkUpdate(List events) { "ON event.id = attractionflag.attractionID AND attractionflag.is_event = 1 " + "WHERE attractionflag.option = :geofenceFlagCode") public abstract List getGeofenceEvents(int geofenceFlagCode); + + /** + * Get event from background thread. + * + * @param eventId ID of event to fetch (*not* placeID) + * @return Event with related destination information, if any + */ + @Query("SELECT event.*, destination.name AS destinationName, destination.distance AS distance, " + + "destination.categories AS destinationCategories, attractionflag.option, " + + "destination.x, destination.y, destination.distance " + + "FROM event " + + "LEFT JOIN destination ON destination.id = event.destination " + + "LEFT JOIN attractionflag " + + "ON event.id = attractionflag.attractionID AND attractionflag.is_event = 1 " + + "WHERE event.id = :eventId") + public abstract EventInfo getEventInBackground(long eventId); } diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java index 4baba04c..19ba580c 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofenceWorker.java @@ -51,6 +51,7 @@ public class AddGeofenceWorker extends Worker { @NonNull @Override public WorkerResult doWork() { + Log.d(LOG_LABEL, "Starting add geofence worker"); Context context = getApplicationContext(); @@ -99,17 +100,10 @@ public WorkerResult doWork() { // Location access permissions prompting is handled by `GpgLocationUtils`. Intent intent = new Intent(context, GeofenceTransitionBroadcastReceiver.class); - intent.putExtra(GEOFENCE_LABELS_KEY, geofenceLabels); - intent.putExtra(GEOFENCE_NAMES_KEY, geofenceNames); - intent.putExtra(LATITUDES_KEY, latitudes); - intent.putExtra(LONGITUDES_KEY, longitudes); - intent.setAction(ACTION_GEOFENCE_TRANSITION); - // FIXME: max of 5 pending intents can resolve at once, so cannot send more than - // five notifications at a time. How to address? PendingIntent pendingIntent = PendingIntent.getBroadcast(context, TRANSITION_BROADCAST_REQUEST_CODE, intent, - PendingIntent.FLAG_ONE_SHOT); + PendingIntent.FLAG_UPDATE_CURRENT); try { geofencingClient.addGeofences(builder.build(), pendingIntent); diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index ae88887b..1d8ad3b2 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -150,7 +150,7 @@ protected Void doInBackground(Void... voids) { } // add events to the end - for (int i = 0; i < geofencesCount; i++) { + for (int i = 0; i < events.size(); i++) { int combinedIndex = i + destinationsCount; EventInfo eventInfo = events.get(i); labels[combinedIndex] = "e" + String.valueOf(eventInfo.getAttraction().getId()); diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java index 857efb50..bdd1cd44 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionBroadcastReceiver.java @@ -1,18 +1,25 @@ package com.gophillygo.app.tasks; +import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.os.AsyncTask; import android.util.Log; import com.google.android.gms.location.Geofence; import com.google.android.gms.location.GeofencingEvent; +import com.gophillygo.app.data.DestinationDao; +import com.gophillygo.app.data.EventDao; +import com.gophillygo.app.data.models.Destination; +import com.gophillygo.app.data.models.DestinationLocation; +import com.gophillygo.app.data.models.EventInfo; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.TimeUnit; +import javax.inject.Inject; + import androidx.work.Data; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; @@ -21,22 +28,21 @@ public class GeofenceTransitionBroadcastReceiver extends BroadcastReceiver { + @Inject + DestinationDao destinationDao; + + @Inject + EventDao eventDao; + private static final String LOG_LABEL = "GeofenceTransitionBR"; + @SuppressLint("StaticFieldLeak") @Override public void onReceive(Context context, Intent intent) { AndroidInjection.inject(this, context); Log.d(LOG_LABEL, "Received geofence transition event"); // Start a worker to send notifications from a background thread. - OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(GeofenceTransitionWorker.class); - workRequestBuilder.setInputData(getGeofenceData(intent)); - workRequestBuilder.setInitialDelay(0, TimeUnit.SECONDS); - // TODO: set constraints and backoff on builder - WorkRequest workRequest = workRequestBuilder.build(); - WorkManager.getInstance().enqueue(workRequest); - } - private Data getGeofenceData(Intent intent) { GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); Data.Builder builder = new Data.Builder(); @@ -51,62 +57,90 @@ private Data getGeofenceData(Intent intent) { geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { List triggeringGeofences = geofencingEvent.getTriggeringGeofences(); - int numGeofences = triggeringGeofences.size(); - String[] geofences = new String[numGeofences]; - String[] geofenceNames = new String[numGeofences]; - double[] geofenceLatitudes = new double[numGeofences]; - double[] geofenceLongitudes = new double[numGeofences]; - - if (intent.hasExtra(AddGeofenceWorker.GEOFENCE_NAMES_KEY) && - intent.hasExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY) && - intent.hasExtra(AddGeofenceWorker.LATITUDES_KEY) && - intent.hasExtra(AddGeofenceWorker.LONGITUDES_KEY)) { - - // get full set of info for places that are flagged for geofencing - String[] labels = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_LABELS_KEY); - String[] names = intent.getStringArrayExtra(AddGeofenceWorker.GEOFENCE_NAMES_KEY); - double[] latitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LATITUDES_KEY); - double[] longitudes = intent.getDoubleArrayExtra(AddGeofenceWorker.LONGITUDES_KEY); - - // map the geofence string IDs to their index in the full value arrays - Map geofenceNameMap = new HashMap<>(labels.length); - for (int i = 0; i < labels.length; i++) { - geofenceNameMap.put(labels[i], i); + final int numGeofences = triggeringGeofences.size(); + + // Query database on background thread by taking broadcast receiver async briefly + // https://developer.android.com/guide/components/broadcasts + final PendingResult pendingResult = goAsync(); + new AsyncTask() { + @Override + protected void onPostExecute(Data data) { + startWorker(data); + // Need to release BroadcastReceiver since have gone async + pendingResult.finish(); } - int i = 0; - for (Geofence fence : triggeringGeofences) { - String fenceId = fence.getRequestId(); - Log.d(LOG_LABEL, "Handling transition for geofence " + fenceId); - try { - int index = geofenceNameMap.get(fenceId); - geofences[i] = fenceId; - geofenceNames[i] = names[index]; - geofenceLatitudes[i] = latitudes[index]; - geofenceLongitudes[i] = longitudes[index]; - i++; - } catch (NullPointerException npe) { - // FIXME: why does this happen sometimes? - Log.e(LOG_LABEL, "Null pointer exception attempting to find geofence in map"); - Log.e(LOG_LABEL, "Fence ID: " + fenceId); - for (String label: labels) { - Log.d(LOG_LABEL, "Found label " + label); + @Override + protected void onCancelled() { + pendingResult.abortBroadcast(); + } + + @Override + protected Data doInBackground(Void... voids) { + + // send arrays of combined values for destinations and events + double[] latitudes = new double[numGeofences]; + double[] longitudes = new double[numGeofences]; + String[] labels = new String[numGeofences]; + String[] names = new String[numGeofences]; + + for (int i = 0; i < numGeofences; i++) { + Geofence fence = triggeringGeofences.get(i); + String fenceId = fence.getRequestId(); + Log.d(LOG_LABEL, "Handling transition for geofence " + fenceId); + // Geofence string ID is "d" for destination or "e" for event, followed by the + // destination or event integer ID. + int geofenceId = Integer.valueOf(fenceId.substring(1)); + boolean isEvent = fenceId.startsWith("e"); + + // query for each event or destination synchronously from the database + if (isEvent) { + EventInfo eventInfo = eventDao.getEventInBackground(geofenceId); + if (eventInfo == null) { + Log.e(LOG_LABEL, "Could not find event for geofence " + geofenceId); + continue; + } + labels[i] = "e" + String.valueOf(eventInfo.getAttraction().getId()); + names[i] = eventInfo.getEvent().getName(); + DestinationLocation location = eventInfo.getLocation(); + latitudes[i] = location.getY(); + latitudes[i] = location.getX(); + } else { + Destination destination = destinationDao.getDestinationInBackground(geofenceId); + if (destination == null) { + Log.e(LOG_LABEL, "Could not find destination for geofence " + geofenceId); + } + labels[i] = "d" + String.valueOf(destination.getId()); + names[i] = destination.getName(); + DestinationLocation location = destination.getLocation(); + latitudes[i] = location.getY(); + longitudes[i] = location.getX(); } } + + return builder.putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, latitudes) + .putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, longitudes) + .putStringArray(GeofenceTransitionWorker.TRIGGERING_GEOFENCES_KEY, labels) + .putStringArray(AddGeofenceWorker.GEOFENCE_NAMES_KEY, names) + .build(); } + }.execute(); - builder.putStringArray(GeofenceTransitionWorker.TRIGGERING_GEOFENCES, geofences); - builder.putStringArray(AddGeofenceWorker.GEOFENCE_NAMES_KEY, geofenceNames); - builder.putDoubleArray(AddGeofenceWorker.LATITUDES_KEY, geofenceLatitudes); - builder.putDoubleArray(AddGeofenceWorker.LONGITUDES_KEY, geofenceLongitudes); - } else { - Log.e(LOG_LABEL, "Broadcast intent is missing the geofence labels and/or names"); - } } } else { + Log.e(LOG_LABEL, "Geofencing transition event had an error"); builder.putInt(GeofenceTransitionWorker.ERROR_CODE_KEY, geofencingEvent.getErrorCode()); + startWorker(builder.build()); } + } - return builder.build(); + private static void startWorker(Data data) { + Log.d(LOG_LABEL, "Going to start geofence transition worker"); + OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(GeofenceTransitionWorker.class); + workRequestBuilder.setInputData(data); + workRequestBuilder.setInitialDelay(0, TimeUnit.SECONDS); + // TODO: set constraints and backoff on builder + WorkRequest workRequest = workRequestBuilder.setInitialDelay(0, TimeUnit.SECONDS).build(); + WorkManager.getInstance().enqueue(workRequest); } } diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java index 83903332..615fda31 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -29,7 +29,7 @@ public class GeofenceTransitionWorker extends Worker { public static final String HAS_ERROR_KEY = "has_error"; public static final String ERROR_CODE_KEY = "error_code"; public static final String TRANSITION_KEY = "transition"; - public static final String TRIGGERING_GEOFENCES = "triggering_geofences"; + public static final String TRIGGERING_GEOFENCES_KEY = "triggering_geofences"; private static final String CHANNEL_ID = "gophillygo-nearby-places"; @@ -39,70 +39,41 @@ public class GeofenceTransitionWorker extends Worker { @Override @SuppressLint("StringFormatInvalid") public WorkerResult doWork() { + Log.d(LOG_LABEL, "Starting geofence transition worker"); // Geofence event data passed along as primitives Data data = getInputData(); if (data.getBoolean(HAS_ERROR_KEY, true)) { + Log.d(LOG_LABEL, "Found error for geofence transition"); int error = data.getInt(ERROR_CODE_KEY, GeofenceStatusCodes.DEVELOPER_ERROR); - - // https://developers.google.com/android/reference/com/google/android/gms/location/GeofenceStatusCodes - switch (error) { - case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE: - Log.e(LOG_LABEL, "Geofencing service not available; high accuracy location probably not enabled"); - // This typically happens after NLP (Android's Network Location Provider) is disabled. - // https://developer.android.com/training/location/geofencing - // TODO: geofences should be re-registered on PROVIDERS_CHANGED - // but implicit system broadcast cannot read DB in background to find - // what to fence. - break; - case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES: - Log.e(LOG_LABEL, "Too many geofences!"); - break; - case GeofenceStatusCodes.TIMEOUT: - Log.w(LOG_LABEL, "Geofence timeout"); - return WorkerResult.RETRY; - case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS: - Log.e(LOG_LABEL, "Too many pending intents to addGeofence. Max is 5."); - return WorkerResult.RETRY; - case GeofenceStatusCodes.API_NOT_CONNECTED: - Log.e(LOG_LABEL, "Geofencing prevented because API not connected"); - return WorkerResult.RETRY; - case GeofenceStatusCodes.CANCELED: - Log.w(LOG_LABEL, "Geofencing cancelled"); - break; - case GeofenceStatusCodes.ERROR: - Log.w(LOG_LABEL, "Geofencing error"); - break; - case GeofenceStatusCodes.DEVELOPER_ERROR: - Log.e(LOG_LABEL, "Geofencing encountered a developer error"); - break; - case GeofenceStatusCodes.INTERNAL_ERROR: - Log.e(LOG_LABEL, "Geofencing encountered an internal error"); - break; - case GeofenceStatusCodes.INTERRUPTED: - Log.w(LOG_LABEL, "Geofencing interrupted"); - return WorkerResult.RETRY; - default: - Log.w(LOG_LABEL, "Unrecognized GeofenceStatusCodes value: " + error); - } - return WorkerResult.FAILURE; + handleError(error); } + // if got this far, have no error Context context = getApplicationContext(); // Get the transition type. int geofenceTransition = data.getInt(TRANSITION_KEY, Geofence.GEOFENCE_TRANSITION_EXIT); Boolean enteredGeofence = geofenceTransition == AddGeofenceWorker.GEOFENCE_ENTER_TRIGGER; - String[] geofences = data.getStringArray(TRIGGERING_GEOFENCES); + String[] geofences = data.getStringArray(TRIGGERING_GEOFENCES_KEY); String[] geofencePlaceNames = data.getStringArray(AddGeofenceWorker.GEOFENCE_NAMES_KEY); double[] latitudes = data.getDoubleArray(AddGeofenceWorker.LATITUDES_KEY); double[] longitudes = data.getDoubleArray(AddGeofenceWorker.LONGITUDES_KEY); + Log.d(LOG_LABEL, "Got geofence transition worker data"); - if (geofences.length > 0) { + int geofencesCount = geofences.length; + Log.d(LOG_LABEL, "Have " + geofencesCount + " geofence transitions to process"); + if (geofencePlaceNames.length != geofences.length || geofences.length != latitudes.length || + geofences.length != longitudes.length) { + Log.e(LOG_LABEL, "Got geofence worker data arrays of differing lengths"); + return WorkerResult.FAILURE; + } + + if (geofencesCount > 0) { // Send notification the main thread Handler handler = new Handler(Looper.getMainLooper()); - for (int i = 0; i < geofences.length; i++) { + for (int i = 0; i < geofencesCount; i++) { // Need a unique int we can find later, for the notification String geofenceLabel = geofences[i]; String placeName = geofencePlaceNames[i]; @@ -116,7 +87,7 @@ public WorkerResult doWork() { String notificationTag = isEvent ? "e" : "d"; if (enteredGeofence) { - Log.d(LOG_LABEL, "Entered geofence ID " + geofenceLabel); + Log.d(LOG_LABEL, "Entered geofence ID " + geofenceLabel + " for " + placeName); handler.post(() -> { // Get intent for the detail view to open on notification click. @@ -206,4 +177,48 @@ private static void createNotificationChannel(Context context) { } } + private WorkerResult handleError(int error) { + // https://developers.google.com/android/reference/com/google/android/gms/location/GeofenceStatusCodes + switch (error) { + case GeofenceStatusCodes.GEOFENCE_NOT_AVAILABLE: + Log.e(LOG_LABEL, "Geofencing service not available; high accuracy location probably not enabled"); + // This typically happens after NLP (Android's Network Location Provider) is disabled. + // https://developer.android.com/training/location/geofencing + // TODO: geofences should be re-registered on PROVIDERS_CHANGED + // but implicit system broadcast cannot read DB in background to find + // what to fence. + break; + case GeofenceStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES: + Log.e(LOG_LABEL, "Too many geofences!"); + break; + case GeofenceStatusCodes.TIMEOUT: + Log.w(LOG_LABEL, "Geofence timeout"); + return WorkerResult.RETRY; + case GeofenceStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS: + Log.e(LOG_LABEL, "Too many pending intents to addGeofence. Max is 5."); + return WorkerResult.RETRY; + case GeofenceStatusCodes.API_NOT_CONNECTED: + Log.e(LOG_LABEL, "Geofencing prevented because API not connected"); + return WorkerResult.RETRY; + case GeofenceStatusCodes.CANCELED: + Log.w(LOG_LABEL, "Geofencing cancelled"); + break; + case GeofenceStatusCodes.ERROR: + Log.w(LOG_LABEL, "Geofencing error"); + break; + case GeofenceStatusCodes.DEVELOPER_ERROR: + Log.e(LOG_LABEL, "Geofencing encountered a developer error"); + break; + case GeofenceStatusCodes.INTERNAL_ERROR: + Log.e(LOG_LABEL, "Geofencing encountered an internal error"); + break; + case GeofenceStatusCodes.INTERRUPTED: + Log.w(LOG_LABEL, "Geofencing interrupted"); + return WorkerResult.RETRY; + default: + Log.w(LOG_LABEL, "Unrecognized GeofenceStatusCodes value: " + error); + } + return WorkerResult.FAILURE; + } + } From ca4a9ba8d4025186460f110030c61f7dc65039c1 Mon Sep 17 00:00:00 2001 From: Kathryn Killebrew Date: Wed, 6 Jun 2018 14:40:20 -0400 Subject: [PATCH 16/16] Refactor geofence removal --- .idea/misc.xml | 31 +++---------------- .../activities/AttractionDetailActivity.java | 25 +++++++++++++-- .../activities/BaseAttractionActivity.java | 26 ++++++++++++++-- .../app/activities/EventDetailActivity.java | 15 ++------- .../app/activities/EventsListActivity.java | 15 ++------- .../app/activities/MapsActivity.java | 22 ++----------- .../app/activities/PlaceDetailActivity.java | 19 ++---------- .../app/activities/PlacesListActivity.java | 16 ++-------- .../tasks/AddGeofencesBroadcastReceiver.java | 2 +- .../app/tasks/GeofenceTransitionWorker.java | 11 ++++--- .../app/tasks/RemoveGeofenceWorker.java | 9 ++++++ 11 files changed, 77 insertions(+), 114 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index e99bcb2b..cc51e58e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,23 +1,16 @@ - - - - - - - - @@ -38,20 +31,4 @@ - - - - - 1.8 - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/gophillygo/app/activities/AttractionDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/AttractionDetailActivity.java index 4bee113a..4e655723 100644 --- a/app/src/main/java/com/gophillygo/app/activities/AttractionDetailActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/AttractionDetailActivity.java @@ -1,6 +1,5 @@ package com.gophillygo.app.activities; -import android.arch.persistence.room.util.StringUtil; import android.content.Intent; import android.graphics.drawable.Drawable; import android.net.Uri; @@ -19,19 +18,41 @@ import com.gophillygo.app.data.models.AttractionInfo; import com.gophillygo.app.data.models.DestinationInfo; import com.gophillygo.app.data.models.DestinationLocation; +import com.gophillygo.app.data.models.EventInfo; +import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; +import com.gophillygo.app.tasks.RemoveGeofenceWorker; import com.synnapps.carouselview.CarouselView; abstract class AttractionDetailActivity extends AppCompatActivity { protected static final int COLLAPSED_LINE_COUNT = 4; protected static final int EXPANDED_MAX_LINES = 50; private static final String LOG_LABEL = "AttractionDetail"; - protected DestinationInfo destinationInfo; + protected DestinationInfo destinationInfo; protected View.OnClickListener toggleClickListener; protected abstract Class getMapActivity(); protected abstract int getAttractionId(); + protected void addOrRemoveGeofence(AttractionInfo info, Boolean haveExistingGeofence, Boolean settingGeofence) { + if (settingGeofence) { + if (haveExistingGeofence) { + Log.d(LOG_LABEL, "No change to geofence"); + return; + } + // add geofence + Log.d(LOG_LABEL, "Add attraction geofence"); + if (info instanceof EventInfo) { + AddGeofencesBroadcastReceiver.addOneGeofence((EventInfo)info); + } else if (info instanceof DestinationInfo) { + AddGeofencesBroadcastReceiver.addOneGeofence(((DestinationInfo) info).getDestination()); + } + } else if (haveExistingGeofence) { + Log.e(LOG_LABEL, "Removing attraction geofence"); + RemoveGeofenceWorker.removeOneGeofence(info); + } + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); diff --git a/app/src/main/java/com/gophillygo/app/activities/BaseAttractionActivity.java b/app/src/main/java/com/gophillygo/app/activities/BaseAttractionActivity.java index a3209d5e..7ffe8938 100644 --- a/app/src/main/java/com/gophillygo/app/activities/BaseAttractionActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/BaseAttractionActivity.java @@ -11,13 +11,16 @@ import android.util.Log; import com.gophillygo.app.data.DestinationViewModel; +import com.gophillygo.app.data.models.AttractionInfo; import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.data.models.DestinationInfo; import com.gophillygo.app.data.models.DestinationLocation; +import com.gophillygo.app.data.models.EventInfo; import com.gophillygo.app.data.networkresource.Status; import com.gophillygo.app.di.GpgViewModelFactory; import com.gophillygo.app.tasks.AddGeofenceWorker; import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; +import com.gophillygo.app.tasks.RemoveGeofenceWorker; import com.gophillygo.app.utils.GpgLocationUtils; import com.gophillygo.app.utils.UserUuidUtils; @@ -27,9 +30,6 @@ import javax.inject.Inject; -import androidx.work.Data; -import androidx.work.WorkManager; - /** * Base activity that requests last known location and destination data when opened; * if either change, updates the distances to the destinations and calls @@ -63,6 +63,26 @@ public abstract class BaseAttractionActivity extends AppCompatActivity @SuppressWarnings("WeakerAccess") DestinationViewModel viewModel; + protected void addOrRemoveGeofence(AttractionInfo info, Boolean haveExistingGeofence, Boolean settingGeofence) { + if (settingGeofence) { + if (haveExistingGeofence) { + Log.d(LOG_LABEL, "No change to geofence"); + return; + } + // add geofence + Log.d(LOG_LABEL, "Add attraction geofence"); + if (info instanceof EventInfo) { + AddGeofencesBroadcastReceiver.addOneGeofence((EventInfo)info); + } else if (info instanceof DestinationInfo) { + AddGeofencesBroadcastReceiver.addOneGeofence(((DestinationInfo) info).getDestination()); + } + + } else if (haveExistingGeofence) { + Log.e(LOG_LABEL, "Removing attraction geofence"); + RemoveGeofenceWorker.removeOneGeofence(info); + } + } + private void setDefaultLocation() { Log.w(LOG_LABEL, "Using City Hall as default location"); diff --git a/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java index d4fa8dd6..7cb10556 100644 --- a/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/EventDetailActivity.java @@ -193,19 +193,8 @@ private void displayEvent() { eventInfo.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(eventInfo.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); - - if (eventInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { - if (haveExistingGeofence) { - Log.d(LOG_LABEL, "No change to event geofence"); - return true; // no change - } - // add geofence - Log.d(LOG_LABEL, "Add event geofence from event list"); - AddGeofencesBroadcastReceiver.addOneGeofence(eventInfo); - } else if (haveExistingGeofence) { - Log.e(LOG_LABEL, "Removing geofence from event list"); - RemoveGeofenceWorker.removeOneGeofence(String.valueOf(eventInfo.getAttraction().getId())); - } + Boolean settingGeofence = destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name); + addOrRemoveGeofence(destinationInfo, haveExistingGeofence, settingGeofence); return true; }); }); diff --git a/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java b/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java index a27e9e28..ce25c8f2 100644 --- a/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/EventsListActivity.java @@ -75,19 +75,10 @@ public boolean clickedFlagOption(MenuItem item, AttractionInfo eventInfo, Intege viewModel.updateAttractionFlag(eventInfo.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); adapter.notifyItemChanged(position); + // do not attempt to add a geofence for an event with no location if (((EventInfo)eventInfo).hasDestinationName()) { - if (eventInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { - if (haveExistingGeofence) { - Log.d(LOG_LABEL, "No change to event geofence"); - return true; // no change - } - // add geofence - Log.d(LOG_LABEL, "Add event geofence from event list"); - AddGeofencesBroadcastReceiver.addOneGeofence((EventInfo)eventInfo); - } else if (haveExistingGeofence) { - Log.e(LOG_LABEL, "Removing geofence from event list"); - RemoveGeofenceWorker.removeOneGeofence(String.valueOf(eventInfo.getAttraction().getId())); - } + Boolean settingGeofence = eventInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name); + addOrRemoveGeofence(eventInfo, haveExistingGeofence, settingGeofence); } else { // TODO: notify user? Log.w(LOG_LABEL, "Cannot add geofence for an event without an associated destination"); diff --git a/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java b/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java index be1baf40..30ed73a7 100644 --- a/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/MapsActivity.java @@ -156,26 +156,8 @@ public void optionsButtonClick(View view, T info) { viewModel.updateAttractionFlag(info.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); popupBinding.setAttractionInfo(info); popupBinding.setAttraction(info.getAttraction()); - - if (info.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { - if (haveExistingGeofence) { - Log.d(LOG_LABEL, "No change to geofence"); - return true; // no change - } - - // add geofence - Log.d(LOG_LABEL, "Add geofence from map"); - if (info.getAttraction().isEvent()) { - // event - AddGeofencesBroadcastReceiver.addOneGeofence((EventInfo)info); - } else { - // destination - AddGeofencesBroadcastReceiver.addOneGeofence((Destination)info.getAttraction()); - } - } else if (haveExistingGeofence) { - Log.e(LOG_LABEL, "Removing geofence"); - RemoveGeofenceWorker.removeOneGeofence(String.valueOf(info.getAttraction().getId())); - } + Boolean settingGeofence = info.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name); + addOrRemoveGeofence(info, haveExistingGeofence, settingGeofence); return true; }); } diff --git a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java index 2d2a2e6b..d4f9e6c0 100644 --- a/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/PlaceDetailActivity.java @@ -11,15 +11,11 @@ import android.view.Gravity; import android.widget.TextView; -import com.gophillygo.app.CarouselViewListener; import com.gophillygo.app.R; import com.gophillygo.app.data.DestinationViewModel; import com.gophillygo.app.data.models.AttractionFlag; -import com.gophillygo.app.data.models.Destination; import com.gophillygo.app.databinding.ActivityPlaceDetailBinding; import com.gophillygo.app.di.GpgViewModelFactory; -import com.gophillygo.app.tasks.AddGeofencesBroadcastReceiver; -import com.gophillygo.app.tasks.RemoveGeofenceWorker; import com.gophillygo.app.utils.FlagMenuUtils; import com.gophillygo.app.utils.UserUuidUtils; import com.synnapps.carouselview.CarouselView; @@ -120,19 +116,8 @@ private void displayDestination() { destinationInfo.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(destinationInfo.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); - if (destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { - if (haveExistingGeofence) { - Log.d(LOG_LABEL, "No change to geofence"); - return true; // no change - } - - // add geofence - Log.d(LOG_LABEL, "Add geofence from place detail"); - AddGeofencesBroadcastReceiver.addOneGeofence(destinationInfo.getAttraction()); - } else if (haveExistingGeofence) { - Log.e(LOG_LABEL, "Removing geofence"); - RemoveGeofenceWorker.removeOneGeofence(String.valueOf(destinationInfo.getAttraction().getId())); - } + Boolean settingGeofence = destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name); + addOrRemoveGeofence(destinationInfo, haveExistingGeofence, settingGeofence); return true; }); }); diff --git a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java index ce131c3b..d7f85c0d 100644 --- a/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java +++ b/app/src/main/java/com/gophillygo/app/activities/PlacesListActivity.java @@ -76,20 +76,8 @@ public boolean clickedFlagOption(MenuItem item, AttractionInfo destinationInfo, destinationInfo.updateAttractionFlag(item.getItemId()); viewModel.updateAttractionFlag(destinationInfo.getFlag(), userUuid, getString(R.string.user_flag_post_api_key)); placesListAdapter.notifyItemChanged(position); - - if (destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name)) { - if (haveExistingGeofence) { - Log.d(LOG_LABEL, "No change to geofence"); - return true; // no change - } - - // add geofence - Log.d(LOG_LABEL, "Add geofence from places list"); - AddGeofencesBroadcastReceiver.addOneGeofence((Destination)destinationInfo.getAttraction()); - } else if (haveExistingGeofence) { - Log.e(LOG_LABEL, "Removing geofence"); - RemoveGeofenceWorker.removeOneGeofence(String.valueOf(destinationInfo.getAttraction().getId())); - } + Boolean settingGeofence = destinationInfo.getFlag().getOption().api_name.equals(AttractionFlag.Option.WantToGo.api_name); + addOrRemoveGeofence(destinationInfo, haveExistingGeofence, settingGeofence); return true; } diff --git a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java index 1d8ad3b2..5d19e3cd 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java +++ b/app/src/main/java/com/gophillygo/app/tasks/AddGeofencesBroadcastReceiver.java @@ -74,7 +74,7 @@ public void onReceive(Context context, Intent intent) { // Sanity check the data before starting the worker if (latitudes.length == 0 || latitudes.length != longitudes.length || - latitudes.length != labels.length) { + latitudes.length != labels.length || latitudes.length != names.length) { Log.e(LOG_LABEL, "Extras data of zero or mismatched length found"); return; } diff --git a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java index 615fda31..fd86118c 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/GeofenceTransitionWorker.java @@ -113,6 +113,7 @@ public WorkerResult doWork() { NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context, CHANNEL_ID) // TODO: use app icon of some sort .setSmallIcon(R.drawable.ic_flag_blue_24dp) + .setOnlyAlertOnce(false) .setContentTitle(placeName) .setContentText(context.getString(R.string.place_nearby_notification, placeName)) .setContentIntent(resultPendingIntent) @@ -132,12 +133,12 @@ public WorkerResult doWork() { NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); notificationManager.cancel(notificationTag, geofenceId); }); - - Log.d(LOG_LABEL, "Re-registering geofence after exit"); - // remove and re-register geofence, or else it will ignore future events - RemoveGeofenceWorker.removeOneGeofence(geofenceLabel); - AddGeofencesBroadcastReceiver.addOneGeofence(longitude, latitude, geofenceLabel, placeName); } + + Log.d(LOG_LABEL, "Re-registering geofence after transition"); + // remove and re-register geofence, or else it will ignore future events + RemoveGeofenceWorker.removeOneGeofence(geofenceLabel); + AddGeofencesBroadcastReceiver.addOneGeofence(longitude, latitude, geofenceLabel, placeName); } return WorkerResult.SUCCESS; diff --git a/app/src/main/java/com/gophillygo/app/tasks/RemoveGeofenceWorker.java b/app/src/main/java/com/gophillygo/app/tasks/RemoveGeofenceWorker.java index a0f4bc1c..55f51095 100644 --- a/app/src/main/java/com/gophillygo/app/tasks/RemoveGeofenceWorker.java +++ b/app/src/main/java/com/gophillygo/app/tasks/RemoveGeofenceWorker.java @@ -8,6 +8,9 @@ import com.google.android.gms.location.LocationServices; import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; +import com.gophillygo.app.data.models.AttractionInfo; +import com.gophillygo.app.data.models.DestinationInfo; +import com.gophillygo.app.data.models.EventInfo; import java.util.ArrayList; import java.util.Arrays; @@ -67,4 +70,10 @@ public static void removeOneGeofence(String geofenceId) { WorkManager.getInstance().enqueue(workRequest); Log.d(LOG_LABEL, "Enqueued new work request to remove one geofence"); } + + public static void removeOneGeofence(AttractionInfo info) { + String prefix = info instanceof EventInfo ? "e" : "d"; + String id = String.valueOf(info.getAttraction().getId()); + removeOneGeofence(prefix + id); + } }