One geofence per place

pull/996/head
Alex Baker 4 years ago
parent 2d79e2e571
commit 0bdf9c8d6f

@ -2,21 +2,30 @@ package org.tasks.data
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with
import org.junit.Assert.assertEquals
import com.todoroo.astrid.dao.TaskDao
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.makers.GeofenceMaker.ARRIVAL
import org.tasks.makers.GeofenceMaker.DEPARTURE
import org.tasks.makers.GeofenceMaker.PLACE
import org.tasks.makers.GeofenceMaker.TASK
import org.tasks.makers.GeofenceMaker.newGeofence
import org.tasks.makers.PlaceMaker.LATITUDE
import org.tasks.makers.PlaceMaker.LONGITUDE
import org.tasks.makers.PlaceMaker.newPlace
import org.tasks.makers.TaskMaker.*
import javax.inject.Inject
@RunWith(AndroidJUnit4::class)
class LocationDaoTest : InjectingTestCase() {
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var taskDao: TaskDao
@Test
fun getExistingPlace() {
@ -43,6 +52,62 @@ class LocationDaoTest : InjectingTestCase() {
assertEquals(-116.816944, place?.longitude)
}
@Test
fun noActiveGeofences() {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1)))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid)))
assertNull(locationDao.getGeofencesByPlace(place.uid))
}
@Test
fun activeArrivalGeofence() {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1)))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)))
val geofence = locationDao.getGeofencesByPlace(place.uid)
assertTrue(geofence.arrival)
assertFalse(geofence.departure)
}
@Test
fun activeDepartureGeofence() {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1)))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true)))
val geofence = locationDao.getGeofencesByPlace(place.uid)
assertFalse(geofence.arrival)
assertTrue(geofence.departure)
}
@Test
fun geofenceInactiveForCompletedTask() {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(COMPLETION_TIME, newDateTime())))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)))
assertNull(locationDao.getGeofencesByPlace(place.uid))
}
@Test
fun geofenceInactiveForDeletedTask() {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(DELETION_TIME, newDateTime())))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)))
assertNull(locationDao.getGeofencesByPlace(place.uid))
}
override fun inject(component: TestComponent) = component.inject(this)
}

@ -0,0 +1,31 @@
package org.tasks.makers
import com.natpryce.makeiteasy.Instantiator
import com.natpryce.makeiteasy.Property
import com.natpryce.makeiteasy.PropertyLookup
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.helper.UUIDHelper
import org.tasks.data.Geofence
import org.tasks.data.Place
object GeofenceMaker {
val PLACE: Property<Geofence, String> = Property.newProperty()
val TASK: Property<Geofence, Long> = Property.newProperty()
val ARRIVAL: Property<Geofence, Boolean> = Property.newProperty()
val DEPARTURE: Property<Geofence, Boolean> = Property.newProperty()
val RADIUS: Property<Geofence, Int> = Property.newProperty()
private val instantiator = Instantiator { lookup: PropertyLookup<Geofence> ->
val geofence = Geofence()
geofence.place = lookup.valueOf(PLACE, "")
geofence.task = lookup.valueOf(TASK, 1)
geofence.isArrival = lookup.valueOf(ARRIVAL, false)
geofence.isDeparture = lookup.valueOf(DEPARTURE, false)
geofence.radius = lookup.valueOf(RADIUS, 250)
geofence
}
fun newGeofence(vararg properties: PropertyValue<in Geofence?, *>): Geofence {
return Maker.make(instantiator, *properties)
}
}

@ -4,14 +4,17 @@ import com.natpryce.makeiteasy.Instantiator
import com.natpryce.makeiteasy.Property
import com.natpryce.makeiteasy.PropertyLookup
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.helper.UUIDHelper
import org.tasks.data.Place
object PlaceMaker {
val LATITUDE: Property<Place, Double> = Property.newProperty()
val LONGITUDE: Property<Place, Double> = Property.newProperty()
val UUID: Property<Place, String> = Property.newProperty()
private val instantiator = Instantiator { lookup: PropertyLookup<Place> ->
val place = Place()
place.uid = lookup.valueOf(UUID, UUIDHelper.newUUID())
place.latitude = lookup.valueOf(LATITUDE, 0.0)
place.longitude = lookup.valueOf(LONGITUDE, 0.0)
place

@ -1,25 +1,24 @@
package org.tasks.location;
import static com.google.android.gms.location.Geofence.NEVER_EXPIRE;
import static com.google.common.base.Predicates.notNull;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.transform;
import static java.util.Collections.singletonList;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import com.google.android.gms.location.Geofence;
import androidx.annotation.Nullable;
import com.google.android.gms.location.GeofencingClient;
import com.google.android.gms.location.GeofencingRequest;
import com.google.android.gms.location.GeofencingRequest.Builder;
import com.google.android.gms.location.LocationServices;
import com.google.common.collect.ImmutableList;
import java.util.List;
import javax.inject.Inject;
import org.tasks.data.Location;
import org.tasks.data.LocationDao;
import org.tasks.data.MergedGeofence;
import org.tasks.data.Place;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.PermissionChecker;
import timber.log.Timber;
public class GeofenceApi {
@ -38,77 +37,56 @@ public class GeofenceApi {
}
public void registerAll() {
register(locationDao.getActiveGeofences());
update(locationDao.getPlacesWithGeofences());
}
public void register(long taskId) {
register(locationDao.getActiveGeofences(taskId));
public void update(long taskId) {
update(locationDao.getPlaceForTask(taskId));
}
public void register(Location location) {
register(singletonList(location));
public void update(String place) {
update(locationDao.getPlace(place));
}
private void register(final List<Location> locations) {
if (!permissionChecker.canAccessLocation()) {
return;
}
List<Geofence> requests = getRequests(locations);
if (!requests.isEmpty()) {
LocationServices.getGeofencingClient(context)
.addGeofences(
new Builder().addGeofences(requests).build(),
PendingIntent.getBroadcast(
context,
0,
new Intent(context, GeofenceTransitionsIntentService.Broadcast.class),
PendingIntent.FLAG_UPDATE_CURRENT));
private void update(List<Place> places) {
for (Place place : places) {
update(place);
}
}
public void cancel(long taskId) {
cancel(locationDao.getGeofences(taskId));
}
public void cancel(final Location location) {
if (location != null) {
cancel(singletonList(location));
}
}
public void cancel(final List<Location> locations) {
if (!permissionChecker.canAccessLocation()) {
public void update(@Nullable Place place) {
if (place == null || !permissionChecker.canAccessLocation()) {
return;
}
List<String> requestIds = getRequestIds(locations);
if (!requestIds.isEmpty()) {
LocationServices.getGeofencingClient(context).removeGeofences(requestIds);
}
}
@SuppressWarnings("ConstantConditions")
private List<String> getRequestIds(List<Location> locations) {
return transform(newArrayList(filter(locations, notNull())), l -> Long.toString(l.getId()));
}
private List<com.google.android.gms.location.Geofence> getRequests(List<Location> locations) {
return transform(
newArrayList(filter(locations, l -> l != null && (l.isArrival() || l.isDeparture()))),
this::toGoogleGeofence);
GeofencingClient client = LocationServices.getGeofencingClient(context);
MergedGeofence geofence = locationDao.getGeofencesByPlace(place.getUid());
if (geofence != null) {
Timber.d("Adding geofence for %s", geofence);
client.addGeofences(
new Builder().addGeofence(toGoogleGeofence(geofence)).build(),
PendingIntent.getBroadcast(
context,
0,
new Intent(context, GeofenceTransitionsIntentService.Broadcast.class),
PendingIntent.FLAG_UPDATE_CURRENT));
} else {
Timber.d("Removing geofence for %s", place);
client.removeGeofences(ImmutableList.of(Long.toString(place.getId())));
}
}
private com.google.android.gms.location.Geofence toGoogleGeofence(Location location) {
private com.google.android.gms.location.Geofence toGoogleGeofence(MergedGeofence geofence) {
int transitionTypes = 0;
if (location.isArrival()) {
if (geofence.isArrival()) {
transitionTypes |= GeofencingRequest.INITIAL_TRIGGER_ENTER;
}
if (location.isDeparture()) {
if (geofence.isDeparture()) {
transitionTypes |= GeofencingRequest.INITIAL_TRIGGER_EXIT;
}
return new com.google.android.gms.location.Geofence.Builder()
.setCircularRegion(location.getLatitude(), location.getLongitude(), location.getRadius())
.setRequestId(Long.toString(location.getId()))
.setCircularRegion(geofence.getLatitude(), geofence.getLongitude(), geofence.getRadius())
.setRequestId(geofence.getUid())
.setTransitionTypes(transitionTypes)
.setExpirationDuration(NEVER_EXPIRE)
.build();

@ -4,7 +4,6 @@ import static com.google.android.gms.location.Geofence.GEOFENCE_TRANSITION_ENTER
import static com.google.android.gms.location.Geofence.GEOFENCE_TRANSITION_EXIT;
import static com.todoroo.astrid.reminders.ReminderService.TYPE_GEOFENCE_ENTER;
import static com.todoroo.astrid.reminders.ReminderService.TYPE_GEOFENCE_EXIT;
import static java.util.Collections.singletonList;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import android.content.BroadcastReceiver;
@ -12,11 +11,13 @@ import android.content.Context;
import android.content.Intent;
import androidx.core.app.JobIntentService;
import com.google.android.gms.location.GeofencingEvent;
import com.google.common.collect.Lists;
import java.util.List;
import javax.inject.Inject;
import org.tasks.Notifier;
import org.tasks.data.Location;
import org.tasks.data.Geofence;
import org.tasks.data.LocationDao;
import org.tasks.data.Place;
import org.tasks.injection.InjectingJobIntentService;
import org.tasks.injection.ServiceComponent;
import org.tasks.notifications.Notification;
@ -58,20 +59,27 @@ public class GeofenceTransitionsIntentService extends InjectingJobIntentService
com.google.android.gms.location.Geofence triggeringGeofence, boolean arrival) {
String requestId = triggeringGeofence.getRequestId();
try {
Place place = locationDao.getPlace(requestId);
if (place == null) {
Timber.e("Can't find place for requestId %s", requestId);
return;
}
List<Geofence> geofences = arrival
? locationDao.getArrivalGeofences(place.getUid())
: locationDao.getDepartureGeofences(place.getUid());
notifier.triggerNotifications(
singletonList(
toNotification(locationDao.getGeofence(Long.parseLong(requestId)), arrival)));
Lists.transform(geofences, g -> toNotification(place, g, arrival)));
} catch (Exception e) {
Timber.e(e, "Error triggering geofence %s: %s", requestId, e.getMessage());
}
}
private Notification toNotification(Location location, boolean arrival) {
private Notification toNotification(Place place, Geofence geofence, boolean arrival) {
Notification notification = new Notification();
notification.taskId = location.getTask();
notification.taskId = geofence.getTask();
notification.type = arrival ? TYPE_GEOFENCE_ENTER : TYPE_GEOFENCE_EXIT;
notification.timestamp = currentTimeMillis();
notification.location = location.getId();
notification.location = place.getId();
return notification;
}

@ -95,7 +95,7 @@ class PlaceSettingsActivity : BaseListSettingsActivity(), MapFragment.MapFragmen
}
override fun delete() {
locationDao.getGeofencesByPlace(place.uid).forEach(locationDao::delete)
locationDao.deleteGeofencesByPlace(place.uid)
locationDao.delete(place)
setResult(Activity.RESULT_OK, Intent(TaskListFragment.ACTION_DELETED))
finish()

@ -10,7 +10,6 @@ import com.google.common.collect.Sets.difference
import com.google.common.collect.Sets.newHashSet
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.SyncFlags
import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.service.TaskCreator
import net.fortuna.ical4j.model.Parameter
@ -29,6 +28,7 @@ import java.io.ByteArrayOutputStream
import java.io.StringReader
import javax.inject.Inject
@Suppress("ClassName")
class iCalendar @Inject constructor(
private val tagDataDao: TagDataDao,
private val preferences: Preferences,
@ -85,14 +85,13 @@ class iCalendar @Inject constructor(
val geofence = Geofence(place!!.uid, preferences)
geofence.task = taskId
geofence.id = locationDao.insert(geofence)
geofenceApi.register(Location(geofence, place))
} else if (place != existing.place) {
geofenceApi.cancel(existing)
val geofence = existing.geofence
geofence.place = place!!.uid
locationDao.update(geofence)
geofenceApi.register(existing)
geofenceApi.update(existing.place)
}
geofenceApi.update(place)
}
fun getTags(categories: List<String>): List<TagData> {
@ -154,8 +153,8 @@ class iCalendar @Inject constructor(
val geo = remote.geoPosition
if (geo == null) {
locationDao.getActiveGeofences(task.getId()).forEach {
geofenceApi.cancel(it)
locationDao.delete(it.geofence)
geofenceApi.update(it.place)
}
} else {
setPlace(task.getId(), geo)

@ -15,17 +15,59 @@ import org.tasks.filters.LocationFilters;
public interface LocationDao {
@Query(
"SELECT * FROM geofences INNER JOIN places ON geofences.place = places.uid WHERE geofence_id = :id LIMIT 1")
Location getGeofence(Long id);
"SELECT places.*"
+ " FROM places"
+ " INNER JOIN geofences ON geofences.place = places.uid"
+ " INNER JOIN tasks ON geofences.task = tasks._id"
+ " WHERE tasks.completed = 0 AND tasks.deleted = 0"
+ " AND (geofences.arrival > 0 OR geofences.departure > 0)"
+ " GROUP BY places.uid")
List<Place> getPlacesWithGeofences();
@Query(
"SELECT * FROM geofences INNER JOIN places ON geofences.place = places.uid WHERE task = :taskId ORDER BY name ASC LIMIT 1")
"SELECT places.*,"
+ " max(geofences.arrival) as arrival,"
+ " max(geofences.departure) as departure,"
+ " min(geofences.radius) as radius"
+ " FROM places"
+ " INNER JOIN geofences ON geofences.place = places.uid"
+ " INNER JOIN tasks ON tasks._id = geofences.task"
+ " WHERE place = :uid AND tasks.completed = 0 AND tasks.deleted = 0"
+ " AND (geofences.arrival > 0 OR geofences.departure > 0)"
+ " GROUP BY places.uid")
MergedGeofence getGeofencesByPlace(String uid);
@Query("DELETE FROM geofences WHERE place = :place")
void deleteGeofencesByPlace(String place);
@Query(
"SELECT geofences.* FROM geofences"
+ " INNER JOIN tasks ON tasks._id = geofences.task"
+ " WHERE place = :place AND arrival = 1 AND tasks.completed = 0 AND tasks.deleted = 0")
List<Geofence> getArrivalGeofences(String place);
@Query(
"SELECT geofences.* FROM geofences"
+ " INNER JOIN tasks ON tasks._id = geofences.task"
+ " WHERE place = :place AND departure = 1 AND tasks.completed = 0 AND tasks.deleted = 0")
List<Geofence> getDepartureGeofences(String place);
@Query(
"SELECT * FROM geofences"
+ " INNER JOIN places ON geofences.place = places.uid"
+ " WHERE task = :taskId ORDER BY name ASC LIMIT 1")
Location getGeofences(long taskId);
@Query(
"SELECT geofences.*, places.* FROM geofences INNER JOIN places ON geofences.place = places.uid INNER JOIN tasks ON tasks._id = geofences.task WHERE tasks._id = :taskId AND tasks.deleted = 0 AND tasks.completed = 0")
List<Location> getActiveGeofences(long taskId);
@Query("SELECT places.*"
+ " FROM places"
+ " INNER JOIN geofences ON geofences.place = places.uid"
+ " WHERE geofences.task = :taskId")
Place getPlaceForTask(long taskId);
@Query(
"SELECT geofences.*, places.* FROM geofences INNER JOIN places ON geofences.place = places.uid INNER JOIN tasks ON tasks._id = geofences.task WHERE tasks.deleted = 0 AND tasks.completed = 0")
List<Location> getActiveGeofences();
@ -57,9 +99,6 @@ public interface LocationDao {
@Query("SELECT * FROM geofences WHERE task = :taskId")
List<Geofence> getGeofencesForTask(long taskId);
@Query("SELECT * FROM geofences WHERE place = :uid")
List<Geofence> getGeofencesByPlace(String uid);
@Query("SELECT * FROM places")
List<Place> getPlaces();

@ -0,0 +1,52 @@
package org.tasks.data;
import androidx.room.Embedded;
public class MergedGeofence {
@Embedded Place place;
boolean arrival;
boolean departure;
int radius;
public Place getPlace() {
return place;
}
public String getUid() {
return place.getUid();
}
public double getLatitude() {
return place.getLatitude();
}
public double getLongitude() {
return place.getLongitude();
}
public boolean isArrival() {
return arrival;
}
public boolean isDeparture() {
return departure;
}
public int getRadius() {
return radius;
}
@Override
public String toString() {
return "MergedGeofence{"
+ "place="
+ place.getDisplayName()
+ ", arrival="
+ arrival
+ ", departure="
+ departure
+ ", radius="
+ radius
+ '}';
}
}

@ -98,9 +98,9 @@ public class AfterSaveWork extends InjectingWorker {
if (justCompleted || justDeleted) {
notificationManager.cancel(taskId);
geofenceApi.cancel(taskId);
} else if (completionDateModified || deletionDateModified) {
geofenceApi.register(taskId);
}
if (completionDateModified || deletionDateModified) {
geofenceApi.update(taskId);
}
if (justCompleted) {

@ -7,6 +7,8 @@ import com.todoroo.astrid.alarms.AlarmService;
import com.todoroo.astrid.reminders.ReminderService;
import com.todoroo.astrid.timers.TimerPlugin;
import javax.inject.Inject;
import org.tasks.data.Geofence;
import org.tasks.data.LocationDao;
import org.tasks.data.TaskAttachment;
import org.tasks.data.TaskAttachmentDao;
import org.tasks.data.UserActivity;
@ -29,6 +31,7 @@ public class CleanupWork extends InjectingWorker {
@Inject AlarmService alarmService;
@Inject TaskAttachmentDao taskAttachmentDao;
@Inject UserActivityDao userActivityDao;
@Inject LocationDao locationDao;
public CleanupWork(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
@ -47,7 +50,10 @@ public class CleanupWork extends InjectingWorker {
alarmService.cancelAlarms(task);
reminderService.cancelReminder(task);
notificationManager.cancel(task);
geofenceApi.cancel(task);
for (Geofence geofence : locationDao.getGeofencesForTask(task)) {
locationDao.delete(geofence);
geofenceApi.update(geofence.getPlace());
}
for (TaskAttachment attachment : taskAttachmentDao.getAttachments(task)) {
FileHelper.delete(context, attachment.parseUri());
taskAttachmentDao.delete(attachment);

@ -41,8 +41,8 @@ import java.util.List;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager;
import org.tasks.R;
import org.tasks.data.Location;
import org.tasks.data.LocationDao;
import org.tasks.data.Place;
import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication;
import org.tasks.intents.TaskIntents;
@ -383,14 +383,14 @@ public class NotificationManager {
PendingIntent.getActivity(context, (int) id, intent, PendingIntent.FLAG_UPDATE_CURRENT));
if (type == TYPE_GEOFENCE_ENTER || type == TYPE_GEOFENCE_EXIT) {
Location location = locationDao.getGeofence(notification.location);
if (location != null) {
Place place = locationDao.getPlace(notification.location);
if (place != null) {
builder.setContentText(
context.getString(
type == TYPE_GEOFENCE_ENTER
? R.string.location_arrived
: R.string.location_departed,
location.getDisplayName()));
place.getDisplayName()));
}
} else if (!Strings.isNullOrEmpty(taskDescription)) {
builder

@ -263,8 +263,8 @@ public class LocationControlSet extends TaskEditControlFragment {
}
if (original != null) {
geofenceApi.cancel(original);
locationDao.delete(original.geofence);
geofenceApi.update(original.place);
}
if (location != null) {
Place place = location.place;
@ -272,7 +272,7 @@ public class LocationControlSet extends TaskEditControlFragment {
geofence.setTask(task.getId());
geofence.setPlace(place.getUid());
geofence.setId(locationDao.insert(geofence));
geofenceApi.register(location);
geofenceApi.update(place);
}
task.setModificationDate(DateUtilities.now());
}

Loading…
Cancel
Save