Synchronization locations with CalDAV and EteSync

pull/935/head
Alex Baker 4 years ago
parent 57c642ab9c
commit 91c5beb199

@ -13,12 +13,14 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimaps;
import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.dao.TaskDao;
import java.io.File;
import java.util.List;
import javax.inject.Inject;
import net.fortuna.ical4j.model.property.Geo;
import org.tasks.R;
import org.tasks.analytics.Tracker;
import org.tasks.caldav.iCalendar;
@ -31,6 +33,8 @@ import org.tasks.data.FilterDao;
import org.tasks.data.GoogleTaskAccount;
import org.tasks.data.GoogleTaskList;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.data.Location;
import org.tasks.data.LocationDao;
import org.tasks.data.Tag;
import org.tasks.data.TagDao;
import org.tasks.data.TagData;
@ -56,6 +60,7 @@ public class Upgrader {
private static final int V6_9 = 608;
private static final int V7_0 = 617;
public static final int V8_2 = 675;
private static final int V8_5 = 700;
private final Context context;
private final Preferences preferences;
private final Tracker tracker;
@ -68,6 +73,7 @@ public class Upgrader {
private final TaskAttachmentDao taskAttachmentDao;
private final CaldavDao caldavDao;
private final TaskDao taskDao;
private final LocationDao locationDao;
private final iCalendar iCal;
@Inject
@ -84,6 +90,7 @@ public class Upgrader {
TaskAttachmentDao taskAttachmentDao,
CaldavDao caldavDao,
TaskDao taskDao,
LocationDao locationDao,
iCalendar iCal) {
this.context = context;
this.preferences = preferences;
@ -97,6 +104,7 @@ public class Upgrader {
this.taskAttachmentDao = taskAttachmentDao;
this.caldavDao = caldavDao;
this.taskDao = taskDao;
this.locationDao = locationDao;
this.iCal = iCal;
}
@ -113,6 +121,7 @@ public class Upgrader {
run(from, V6_9, this::applyCaldavCategories);
run(from, V7_0, this::applyCaldavSubtasks);
run(from, V8_2, this::migrateColors);
run(from, V8_5, this::applyCaldavGeo);
}
preferences.setCurrentVersion(to);
}
@ -181,6 +190,35 @@ public class Upgrader {
}
}
private void applyCaldavGeo() {
List<CaldavTask> updated = newArrayList();
List<Long> tasksWithLocations =
Lists.transform(locationDao.getActiveGeofences(), Location::getTask);
for (CaldavTask task : transform(caldavDao.getTasks(), CaldavTaskContainer::getCaldavTask)) {
long taskId = task.getTask();
if (tasksWithLocations.contains(taskId)) {
continue;
}
at.bitfire.ical4android.Task remoteTask = iCalendar.Companion.fromVtodo(task.getVtodo());
if (remoteTask == null) {
continue;
}
Geo geo = remoteTask.getGeoPosition();
if (geo == null) {
continue;
}
iCal.setPlace(taskId, geo);
}
batch(tasksWithLocations, taskDao::touch);
caldavDao.update(updated);
}
private void applyCaldavSubtasks() {
List<CaldavTask> updated = newArrayList();

@ -15,8 +15,15 @@ import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.service.TaskCreator
import net.fortuna.ical4j.model.Parameter
import net.fortuna.ical4j.model.parameter.RelType
import net.fortuna.ical4j.model.property.Geo
import net.fortuna.ical4j.model.property.RelatedTo
import org.tasks.caldav.GeoUtils.equalish
import org.tasks.caldav.GeoUtils.toGeo
import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.data.*
import org.tasks.jobs.WorkManager
import org.tasks.location.GeofenceApi
import org.tasks.preferences.Preferences
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.StringReader
@ -24,6 +31,10 @@ import javax.inject.Inject
class iCalendar @Inject constructor(
private val tagDataDao: TagDataDao,
private val preferences: Preferences,
private val locationDao: LocationDao,
private val workManager: WorkManager,
private val geofenceApi: GeofenceApi,
private val taskCreator: TaskCreator,
private val tagDao: TagDao,
private val taskDao: TaskDao,
@ -60,6 +71,30 @@ class iCalendar @Inject constructor(
}
}
fun setPlace(taskId: Long, geo: Geo) {
var place: Place? = locationDao.findPlace(
geo.latitude.toLikeString(),
geo.longitude.toLikeString())
if (place == null) {
place = Place.newPlace(geo)
place.id = locationDao.insert(place)
workManager.reverseGeocode(place)
}
val existing: Location? = locationDao.getGeofences(taskId)
if (existing == null) {
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)
}
}
fun getTags(categories: List<String>): List<TagData> {
if (categories.isEmpty()) {
return emptyList()
@ -87,6 +122,11 @@ class iCalendar @Inject constructor(
} else {
remoteModel.uid = caldavTask.remoteId
}
val location = locationDao.getGeofences(task.getId())
val localGeo = toGeo(location)
if (localGeo == null || !localGeo.equalish(remoteModel.geoPosition)) {
remoteModel.geoPosition = localGeo
}
val os = ByteArrayOutputStream()
remoteModel.write(os)
@ -111,6 +151,15 @@ class iCalendar @Inject constructor(
caldavTask = existing
}
CaldavConverter.apply(task, remote)
val geo = remote.geoPosition
if (geo == null) {
locationDao.getActiveGeofences(task.getId()).forEach {
geofenceApi.cancel(it)
locationDao.delete(it.geofence)
}
} else {
setPlace(task.getId(), geo)
}
tagDao.applyTags(task, tagDataDao, getTags(remote.categories))
task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true)
task.putTransitory(TaskDao.TRANS_SUPPRESS_REFRESH, true)

@ -63,7 +63,7 @@ public class Location implements Serializable, Parcelable {
geofence.setTask(task);
}
public String getName() {
@Nullable public String getName() {
return place.getName();
}

@ -47,6 +47,9 @@ public interface LocationDao {
@Update
void update(Place place);
@Update
void update(Geofence geofence);
@Query("SELECT * FROM places WHERE uid = :uid LIMIT 1")
Place getByUid(String uid);
@ -56,6 +59,9 @@ public interface LocationDao {
@Query("SELECT * FROM places")
List<Place> getPlaces();
@Query("SELECT * FROM places WHERE place_id = :id")
Place getPlace(long id);
@Query(
"SELECT places.*, IFNULL(COUNT(geofence_id),0) AS count FROM places LEFT OUTER JOIN geofences ON geofences.place = places.uid GROUP BY uid ORDER BY COUNT(geofence_id) DESC")
LiveData<List<PlaceUsage>> getPlaceUsage();

@ -7,6 +7,7 @@ import android.location.Location;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
@ -21,6 +22,7 @@ import java.io.Serializable;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.fortuna.ical4j.model.property.Geo;
import org.tasks.location.MapPosition;
@Entity(tableName = TABLE_NAME, indices = @Index(name = "place_uid", value = "uid", unique = true))
@ -44,6 +46,8 @@ public class Place implements Serializable, Parcelable {
}
};
private static final Pattern pattern = Pattern.compile("(\\d+):(\\d+):(\\d+\\.\\d+)");
private static final Pattern COORDS =
Pattern.compile("^\\d+°\\d+'\\d+\\.\\d+\"[NS] \\d+°\\d+'\\d+\\.\\d+\"[EW]$");
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "place_id")
@ -96,12 +100,6 @@ public class Place implements Serializable, Parcelable {
longitude = parcel.readDouble();
}
private static String formatCoordinates(org.tasks.data.Place place) {
return String.format(
"%s %s",
formatCoordinate(place.getLatitude(), true), formatCoordinate(place.getLongitude(), false));
}
private static String formatCoordinate(double coordinates, boolean latitude) {
String output =
android.location.Location.convert(Math.abs(coordinates), Location.FORMAT_SECONDS);
@ -118,11 +116,21 @@ public class Place implements Serializable, Parcelable {
}
}
public static Place newPlace(MapPosition mapPosition) {
public static Place newPlace(Geo geo) {
Place place = newPlace();
place.setLatitude(geo.getLatitude().doubleValue());
place.setLongitude(geo.getLongitude().doubleValue());
return place;
}
@Nullable public static Place newPlace(@Nullable MapPosition mapPosition) {
if (mapPosition == null) {
return null;
}
Place place = newPlace();
place.setLatitude(mapPosition.getLatitude());
place.setLongitude(mapPosition.getLongitude());
place.setName(formatCoordinates(place));
return place;
}
@ -162,7 +170,7 @@ public class Place implements Serializable, Parcelable {
this.uid = uid;
}
public String getName() {
@Nullable public String getName() {
return name;
}
@ -211,13 +219,14 @@ public class Place implements Serializable, Parcelable {
}
public String getDisplayName() {
if (!Strings.isNullOrEmpty(name) && !COORDS.matcher(name).matches()) {
return name;
}
if (!Strings.isNullOrEmpty(address)) {
return address;
}
if (!Strings.isNullOrEmpty(name)) {
return name;
}
return formatCoordinates(this);
return String.format(
"%s %s", formatCoordinate(getLatitude(), true), formatCoordinate(getLongitude(), false));
}
public String getDisplayAddress() {

@ -6,8 +6,6 @@ import dagger.Module;
import dagger.Provides;
import org.tasks.R;
import org.tasks.billing.Inventory;
import org.tasks.location.Geocoder;
import org.tasks.location.MapboxGeocoder;
import org.tasks.preferences.Preferences;
import org.tasks.themes.ColorProvider;
import org.tasks.themes.ThemeAccent;
@ -51,10 +49,4 @@ public class ActivityModule {
public ThemeAccent getThemeAccent(ColorProvider colorProvider, Preferences preferences) {
return colorProvider.getThemeAccent(preferences.getInt(R.string.p_theme_accent, 1));
}
@Provides
@ActivityScope
public Geocoder getGeocoder(@ForApplication Context context) {
return new MapboxGeocoder(context);
}
}

@ -25,6 +25,8 @@ import org.tasks.data.TaskListMetadataDao;
import org.tasks.data.UserActivityDao;
import org.tasks.jobs.WorkManager;
import org.tasks.locale.Locale;
import org.tasks.location.Geocoder;
import org.tasks.location.MapboxGeocoder;
import org.tasks.notifications.NotificationDao;
import org.tasks.security.Encryption;
import org.tasks.security.KeyStoreEncryption;
@ -146,4 +148,9 @@ public class ApplicationModule {
public BillingClient getBillingClient(Inventory inventory, Tracker tracker) {
return new BillingClientImpl(context, inventory, tracker);
}
@Provides
public Geocoder getGeocoder(@ForApplication Context context) {
return new MapboxGeocoder(context);
}
}

@ -7,6 +7,7 @@ import org.tasks.jobs.CleanupWork;
import org.tasks.jobs.DriveUploader;
import org.tasks.jobs.MidnightRefreshWork;
import org.tasks.jobs.RefreshWork;
import org.tasks.jobs.ReverseGeocodeWork;
import org.tasks.jobs.SyncWork;
@Subcomponent(modules = WorkModule.class)
@ -25,4 +26,6 @@ public interface JobComponent {
void inject(AfterSaveWork afterSaveWork);
void inject(DriveUploader driveUploader);
void inject(ReverseGeocodeWork reverseGeocodeWork);
}

@ -0,0 +1,52 @@
package org.tasks.jobs
import android.content.Context
import androidx.work.WorkerParameters
import org.tasks.LocalBroadcastManager
import org.tasks.analytics.Tracker
import org.tasks.data.LocationDao
import org.tasks.injection.InjectingWorker
import org.tasks.injection.JobComponent
import org.tasks.location.Geocoder
import timber.log.Timber
import java.io.IOException
import javax.inject.Inject
class ReverseGeocodeWork(context: Context, workerParams: WorkerParameters) : InjectingWorker(context, workerParams) {
companion object {
const val PLACE_ID = "place_id"
}
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var geocoder: Geocoder
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var tracker: Tracker
public override fun run(): Result {
val id = inputData.getLong(PLACE_ID, 0)
if (id == 0L) {
Timber.e("Missing id")
return Result.failure()
}
val place = locationDao.getPlace(id)
if (place == null) {
Timber.e("Can't find place $id")
return Result.failure()
}
return try {
val result = geocoder.reverseGeocode(place.mapPosition)
result.id = place.id
result.uid = place.uid
locationDao.update(result)
localBroadcastManager.broadcastRefresh()
Timber.d("found $result")
Result.success()
} catch (e: IOException) {
tracker.reportException(e);
Result.failure()
}
}
override fun inject(component: JobComponent) = component.inject(this)
}

@ -9,6 +9,7 @@ import static io.reactivex.Single.zip;
import static org.tasks.date.DateTimeUtils.midnight;
import static org.tasks.date.DateTimeUtils.newDateTime;
import static org.tasks.db.DbUtils.batch;
import static org.tasks.jobs.ReverseGeocodeWork.PLACE_ID;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import static org.tasks.time.DateTimeUtils.printDuration;
import static org.tasks.time.DateTimeUtils.printTimestamp;
@ -37,9 +38,11 @@ import io.reactivex.schedulers.Schedulers;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.tasks.BuildConfig;
import org.tasks.R;
import org.tasks.data.CaldavDao;
import org.tasks.data.GoogleTaskListDao;
import org.tasks.data.Place;
import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
@ -54,6 +57,7 @@ public class WorkManager {
private static final String TAG_MIDNIGHT_REFRESH = "tag_midnight_refresh";
private static final String TAG_SYNC = "tag_sync";
private static final String TAG_BACKGROUND_SYNC = "tag_background_sync";
private static final String TAG_REVERSE_GEOCODE = "tag_reverse_geocode";
private final Context context;
private final Preferences preferences;
@ -120,6 +124,19 @@ public class WorkManager {
workManager.beginUniqueWork(TAG_SYNC, ExistingWorkPolicy.REPLACE, request).enqueue();
}
public void reverseGeocode(Place place) {
if (BuildConfig.DEBUG && place.getId() == 0) {
throw new RuntimeException("Missing id");
}
workManager.enqueue(
new Builder(ReverseGeocodeWork.class)
.setInputData(new Data.Builder().putLong(PLACE_ID, place.getId()).build())
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.setConstraints(
new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
.build());
}
public void updateBackgroundSync() {
updateBackgroundSync(null, null, null);
}

@ -6,7 +6,6 @@ import static com.todoroo.andlib.utility.AndroidUtilities.hideKeyboard;
import static org.tasks.PermissionUtil.verifyPermissions;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.location.Location;
@ -49,14 +48,12 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import org.tasks.Event;
import org.tasks.R;
import org.tasks.billing.Inventory;
import org.tasks.caldav.GeoUtils;
import org.tasks.data.LocationDao;
import org.tasks.data.Place;
import org.tasks.data.PlaceUsage;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ActivityComponent;
import org.tasks.injection.ForApplication;
import org.tasks.injection.InjectingAppCompatActivity;
import org.tasks.location.LocationPickerAdapter.OnLocationPicked;
import org.tasks.location.LocationSearchAdapter.OnPredictionPicked;
@ -106,10 +103,8 @@ public class LocationPickerActivity extends InjectingAppCompatActivity
@BindView(R.id.recent_locations)
RecyclerView recyclerView;
@Inject @ForApplication Context context;
@Inject Theme theme;
@Inject Toaster toaster;
@Inject Inventory inventory;
@Inject LocationDao locationDao;
@Inject PlaceSearchProvider searchProvider;
@Inject PermissionChecker permissionChecker;
@ -325,7 +320,7 @@ public class LocationPickerActivity extends InjectingAppCompatActivity
});
}
private void returnPlace(org.tasks.data.Place place) {
private void returnPlace(@Nullable org.tasks.data.Place place) {
if (place == null) {
Timber.e("Place is null");
return;

@ -4,6 +4,8 @@ import static com.todoroo.andlib.utility.AndroidUtilities.assertNotMainThread;
import static org.tasks.data.Place.newPlace;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParser;
import com.mapbox.api.geocoding.v5.MapboxGeocoding;
@ -25,6 +27,15 @@ public class MapboxGeocoder implements Geocoder {
public MapboxGeocoder(Context context) {
token = context.getString(R.string.mapbox_key);
Looper mainLooper = Looper.getMainLooper();
if (mainLooper.getThread() == Thread.currentThread()) {
init(context);
} else {
new Handler(mainLooper).post(() -> init(context));
}
}
private void init(Context context) {
Mapbox.getInstance(context, token);
}

Loading…
Cancel
Save