From e302f3402e45071b97da1be33bb388d11ef1d417 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Thu, 21 Mar 2019 14:49:08 -0500 Subject: [PATCH] Split locations table into geofences and places --- .../com.todoroo.astrid.dao.Database/61.json | 984 ++++++++++++++++++ .../java/org/tasks/location/PlacePicker.java | 27 +- .../astrid/backup/TasksXmlImporter.java | 23 +- .../java/com/todoroo/astrid/dao/Database.java | 8 +- .../org/tasks/backup/BackupContainer.java | 23 +- .../org/tasks/backup/TasksJsonExporter.java | 3 +- .../org/tasks/backup/TasksJsonImporter.java | 45 +- .../main/java/org/tasks/data/DeletionDao.java | 2 +- .../main/java/org/tasks/data/Geofence.java | 189 ++++ .../main/java/org/tasks/data/Location.java | 257 +---- .../main/java/org/tasks/data/LocationDao.java | 28 +- app/src/main/java/org/tasks/data/Place.java | 262 +++++ .../main/java/org/tasks/db/Migrations.java | 15 +- .../org/tasks/dialogs/LocationDialog.java | 2 +- .../org/tasks/location/GeofenceService.java | 10 +- .../java/org/tasks/ui/LocationControlSet.java | 4 - 16 files changed, 1630 insertions(+), 252 deletions(-) create mode 100644 app/schemas/com.todoroo.astrid.dao.Database/61.json create mode 100644 app/src/main/java/org/tasks/data/Geofence.java create mode 100644 app/src/main/java/org/tasks/data/Place.java diff --git a/app/schemas/com.todoroo.astrid.dao.Database/61.json b/app/schemas/com.todoroo.astrid.dao.Database/61.json new file mode 100644 index 000000000..f2fabd17c --- /dev/null +++ b/app/schemas/com.todoroo.astrid.dao.Database/61.json @@ -0,0 +1,984 @@ +{ + "formatVersion": 1, + "database": { + "version": 61, + "identityHash": "c6fc1759f838676d2d7f02f04154a679", + "entities": [ + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `type` INTEGER NOT NULL, `location` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "taskId", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_notification_task", + "unique": true, + "columnNames": [ + "task" + ], + "createSql": "CREATE UNIQUE INDEX `index_notification_task` ON `${TABLE_NAME}` (`task`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "tagdata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `remoteId` TEXT, `name` TEXT, `color` INTEGER, `tagOrdering` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tagOrdering", + "columnName": "tagOrdering", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "userActivity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `remoteId` TEXT, `message` TEXT, `picture` TEXT, `target_id` TEXT, `created_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "picture", + "columnName": "picture", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "targetId", + "columnName": "target_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created_at", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "task_attachments", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `remoteId` TEXT, `task_id` TEXT, `name` TEXT, `path` TEXT, `content_type` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "taskId", + "columnName": "task_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uri", + "columnName": "path", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "task_list_metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `remoteId` TEXT, `tag_uuid` TEXT, `filter` TEXT, `task_ids` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tagUuid", + "columnName": "tag_uuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filter", + "columnName": "filter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "taskIds", + "columnName": "task_ids", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `title` TEXT, `importance` INTEGER, `dueDate` INTEGER, `hideUntil` INTEGER, `created` INTEGER, `modified` INTEGER, `completed` INTEGER, `deleted` INTEGER, `notes` TEXT, `estimatedSeconds` INTEGER, `elapsedSeconds` INTEGER, `timerStart` INTEGER, `notificationFlags` INTEGER, `notifications` INTEGER, `lastNotified` INTEGER, `snoozeTime` INTEGER, `recurrence` TEXT, `repeatUntil` INTEGER, `calendarUri` TEXT, `remoteId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "importance", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dueDate", + "columnName": "dueDate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hideUntil", + "columnName": "hideUntil", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "estimatedSeconds", + "columnName": "estimatedSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "elapsedSeconds", + "columnName": "elapsedSeconds", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "timerStart", + "columnName": "timerStart", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notificationFlags", + "columnName": "notificationFlags", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "notifications", + "columnName": "notifications", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastNotified", + "columnName": "lastNotified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "snoozeTime", + "columnName": "snoozeTime", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "recurrence", + "columnName": "recurrence", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "repeatUntil", + "columnName": "repeatUntil", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "calendarUri", + "columnName": "calendarUri", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remoteId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "t_rid", + "unique": true, + "columnNames": [ + "remoteId" + ], + "createSql": "CREATE UNIQUE INDEX `t_rid` ON `${TABLE_NAME}` (`remoteId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "alarms", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `time` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "places", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`place_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `name` TEXT, `address` TEXT, `phone` TEXT, `url` TEXT, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "place_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "place_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "geofences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`geofence_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `place` TEXT, `radius` INTEGER NOT NULL, `arrival` INTEGER NOT NULL, `departure` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "geofence_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "place", + "columnName": "place", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radius", + "columnName": "radius", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "arrival", + "columnName": "arrival", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "departure", + "columnName": "departure", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "geofence_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `name` TEXT, `tag_uid` TEXT, `task_uid` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tagUid", + "columnName": "tag_uid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "taskUid", + "columnName": "task_uid", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "google_tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `remote_id` TEXT, `list_id` TEXT, `parent` INTEGER NOT NULL, `indent` INTEGER NOT NULL, `order` INTEGER NOT NULL, `remote_order` INTEGER NOT NULL, `last_sync` INTEGER NOT NULL, `deleted` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "listId", + "columnName": "list_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "indent", + "columnName": "indent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "remoteOrder", + "columnName": "remote_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "last_sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "filters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `sql` TEXT, `values` TEXT, `criterion` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sql", + "columnName": "sql", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "values", + "columnName": "values", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "criterion", + "columnName": "criterion", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "google_task_lists", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `remote_id` TEXT, `title` TEXT, `remote_order` INTEGER NOT NULL, `last_sync` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `color` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteOrder", + "columnName": "remote_order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "last_sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caldav_calendar", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `uuid` TEXT, `name` TEXT, `color` INTEGER NOT NULL, `ctag` TEXT, `url` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ctag", + "columnName": "ctag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caldav_tasks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `calendar` TEXT, `object` TEXT, `remote_id` TEXT, `etag` TEXT, `last_sync` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, `vtodo` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "task", + "columnName": "task", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "calendar", + "columnName": "calendar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "object", + "columnName": "object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastSync", + "columnName": "last_sync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "vtodo", + "columnName": "vtodo", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "caldav_account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT, `name` TEXT, `url` TEXT, `username` TEXT, `password` TEXT, `error` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "google_task_accounts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account` TEXT, `error` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"c6fc1759f838676d2d7f02f04154a679\")" + ] + } +} \ No newline at end of file diff --git a/app/src/googleplay/java/org/tasks/location/PlacePicker.java b/app/src/googleplay/java/org/tasks/location/PlacePicker.java index 6e3af78a8..db4c3dc46 100644 --- a/app/src/googleplay/java/org/tasks/location/PlacePicker.java +++ b/app/src/googleplay/java/org/tasks/location/PlacePicker.java @@ -9,7 +9,9 @@ import com.google.android.gms.common.GooglePlayServicesNotAvailableException; import com.google.android.gms.common.GooglePlayServicesRepairableException; import com.google.android.gms.location.places.Place; import com.google.android.gms.maps.model.LatLng; +import com.todoroo.astrid.helper.UUIDHelper; import org.tasks.R; +import org.tasks.data.Geofence; import org.tasks.data.Location; import org.tasks.preferences.Preferences; import timber.log.Timber; @@ -36,23 +38,32 @@ public class PlacePicker { public static Location getPlace(Context context, Intent data, Preferences preferences) { Place place = com.google.android.gms.location.places.ui.PlacePicker.getPlace(context, data); LatLng latLng = place.getLatLng(); - Location location = new Location(); - location.setName(place.getName().toString()); + Geofence g = new Geofence(); + g.setRadius(preferences.getInt(R.string.p_default_location_radius, 250)); + int defaultReminders = + preferences.getIntegerFromString(R.string.p_default_location_reminder_key, 1); + g.setArrival(defaultReminders == 1 || defaultReminders == 3); + g.setDeparture(defaultReminders == 2 || defaultReminders == 3); + + org.tasks.data.Place p = new org.tasks.data.Place(); + p.setUid(UUIDHelper.newUUID()); + p.setName(place.getName().toString()); CharSequence address = place.getAddress(); if (address != null) { - location.setAddress(place.getAddress().toString()); + p.setAddress(place.getAddress().toString()); } CharSequence phoneNumber = place.getPhoneNumber(); if (phoneNumber != null) { - location.setPhone(phoneNumber.toString()); + p.setPhone(phoneNumber.toString()); } Uri uri = place.getWebsiteUri(); if (uri != null) { - location.setUrl(uri.toString()); + p.setUrl(uri.toString()); } - location.setLatitude(latLng.latitude); - location.setLongitude(latLng.longitude); - location.setRadius(preferences.getInt(R.string.p_default_location_radius, 250)); + p.setLatitude(latLng.latitude); + p.setLongitude(latLng.longitude); + + Location location = new Location(g, p); Timber.i("Picked %s", location); return location; } diff --git a/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java b/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java index 095af6a74..e04d706e1 100755 --- a/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java +++ b/app/src/main/java/com/todoroo/astrid/backup/TasksXmlImporter.java @@ -15,6 +15,7 @@ import android.text.TextUtils; import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.helper.UUIDHelper; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -24,10 +25,11 @@ import org.tasks.R; import org.tasks.backup.XmlReader; import org.tasks.data.Alarm; import org.tasks.data.AlarmDao; +import org.tasks.data.Geofence; import org.tasks.data.GoogleTask; import org.tasks.data.GoogleTaskDao; -import org.tasks.data.Location; import org.tasks.data.LocationDao; +import org.tasks.data.Place; import org.tasks.data.Tag; import org.tasks.data.TagDao; import org.tasks.data.TagData; @@ -245,13 +247,18 @@ public class TasksXmlImporter { alarm.setTime(xml.readLong("value")); alarmDao.insert(alarm); } else if ("geofence".equals(key)) { - Location location = new Location(); - location.setTask(currentTask.getId()); - location.setName(xml.readString("value")); - location.setLatitude(xml.readDouble("value2")); - location.setLongitude(xml.readDouble("value3")); - location.setRadius(xml.readInteger("value4")); - locationDao.insert(location); + Place place = new Place(); + place.setUid(UUIDHelper.newUUID()); + place.setName(xml.readString("value")); + place.setLatitude(xml.readDouble("value2")); + place.setLongitude(xml.readDouble("value3")); + locationDao.insert(place); + Geofence geofence = new Geofence(); + geofence.setTask(currentTask.getId()); + geofence.setPlace(place.getUid()); + geofence.setRadius(xml.readInteger("value4")); + geofence.setArrival(true); + locationDao.insert(geofence); } else if ("tags-tag".equals(key)) { String name = xml.readString("value"); String tagUid = xml.readString("value2"); diff --git a/app/src/main/java/com/todoroo/astrid/dao/Database.java b/app/src/main/java/com/todoroo/astrid/dao/Database.java index f803b8b59..8795ea59e 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/Database.java +++ b/app/src/main/java/com/todoroo/astrid/dao/Database.java @@ -17,13 +17,14 @@ import org.tasks.data.CaldavTask; import org.tasks.data.DeletionDao; import org.tasks.data.Filter; import org.tasks.data.FilterDao; +import org.tasks.data.Geofence; import org.tasks.data.GoogleTask; import org.tasks.data.GoogleTaskAccount; import org.tasks.data.GoogleTaskDao; import org.tasks.data.GoogleTaskList; import org.tasks.data.GoogleTaskListDao; -import org.tasks.data.Location; import org.tasks.data.LocationDao; +import org.tasks.data.Place; import org.tasks.data.Tag; import org.tasks.data.TagDao; import org.tasks.data.TagData; @@ -46,7 +47,8 @@ import org.tasks.notifications.NotificationDao; TaskListMetadata.class, Task.class, Alarm.class, - Location.class, + Place.class, + Geofence.class, Tag.class, GoogleTask.class, Filter.class, @@ -56,7 +58,7 @@ import org.tasks.notifications.NotificationDao; CaldavAccount.class, GoogleTaskAccount.class }, - version = 60) + version = 61) public abstract class Database extends RoomDatabase { public static final String NAME = "database"; diff --git a/app/src/main/java/org/tasks/backup/BackupContainer.java b/app/src/main/java/org/tasks/backup/BackupContainer.java index c077293fa..dccd260a9 100644 --- a/app/src/main/java/org/tasks/backup/BackupContainer.java +++ b/app/src/main/java/org/tasks/backup/BackupContainer.java @@ -4,15 +4,17 @@ import static java.util.Collections.emptyList; import com.todoroo.astrid.data.Task; import java.util.List; +import org.tasks.backup.TasksJsonImporter.LegacyLocation; import org.tasks.data.Alarm; import org.tasks.data.CaldavAccount; import org.tasks.data.CaldavCalendar; import org.tasks.data.CaldavTask; import org.tasks.data.Filter; +import org.tasks.data.Geofence; import org.tasks.data.GoogleTask; import org.tasks.data.GoogleTaskAccount; import org.tasks.data.GoogleTaskList; -import org.tasks.data.Location; +import org.tasks.data.Place; import org.tasks.data.Tag; import org.tasks.data.TagData; import org.tasks.data.TaskAttachment; @@ -21,6 +23,7 @@ import org.tasks.data.UserActivity; class BackupContainer { private final List tasks; + private final List places; private final List tags; private final List filters; private final List googleTaskLists; @@ -30,6 +33,7 @@ class BackupContainer { BackupContainer( List tasks, + List places, List tags, List filters, List googleTaskAccounts, @@ -37,6 +41,7 @@ class BackupContainer { List caldavAccounts, List caldavCalendars) { this.tasks = tasks; + this.places = places; this.tags = tags; this.filters = filters; this.googleTaskAccounts = googleTaskAccounts; @@ -73,21 +78,26 @@ class BackupContainer { return googleTaskAccounts == null ? emptyList() : googleTaskAccounts; } + public List getPlaces() { + return places == null ? emptyList() : places; + } + static class TaskBackup { final Task task; final List alarms; - final List locations; + final List locations; final List tags; final List google; final List comments; + private final List geofences; private final List attachments; private final List caldavTasks; TaskBackup( Task task, List alarms, - List locations, + List geofences, List tags, List google, List comments, @@ -95,12 +105,13 @@ class BackupContainer { List caldavTasks) { this.task = task; this.alarms = alarms; - this.locations = locations; + this.geofences = geofences; this.tags = tags; this.google = google; this.comments = comments; this.attachments = attachments; this.caldavTasks = caldavTasks; + locations = emptyList(); } List getAttachments() { @@ -110,5 +121,9 @@ class BackupContainer { List getCaldavTasks() { return caldavTasks == null ? emptyList() : caldavTasks; } + + List getGeofences() { + return geofences == null ? emptyList() : geofences; + } } } diff --git a/app/src/main/java/org/tasks/backup/TasksJsonExporter.java b/app/src/main/java/org/tasks/backup/TasksJsonExporter.java index 3247f76ed..14da31ed9 100755 --- a/app/src/main/java/org/tasks/backup/TasksJsonExporter.java +++ b/app/src/main/java/org/tasks/backup/TasksJsonExporter.java @@ -181,7 +181,7 @@ public class TasksJsonExporter { new BackupContainer.TaskBackup( task, alarmDao.getAlarms(taskId), - locationDao.getGeofences(taskId), + locationDao.getGeofencesForTask(taskId), tagDao.getTagsForTask(taskId), googleTaskDao.getAllByTaskId(taskId), userActivityDao.getCommentsForTask(task.getUuid()), @@ -196,6 +196,7 @@ public class TasksJsonExporter { "data", new BackupContainer( taskBackups, + locationDao.getPlaces(), tagDataDao.getAll(), filterDao.getAll(), googleTaskListDao.getAccounts(), diff --git a/app/src/main/java/org/tasks/backup/TasksJsonImporter.java b/app/src/main/java/org/tasks/backup/TasksJsonImporter.java index 60431fee4..4954c4312 100644 --- a/app/src/main/java/org/tasks/backup/TasksJsonImporter.java +++ b/app/src/main/java/org/tasks/backup/TasksJsonImporter.java @@ -13,6 +13,7 @@ import com.google.gson.JsonObject; import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.data.Task; +import com.todoroo.astrid.helper.UUIDHelper; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -28,13 +29,14 @@ import org.tasks.data.CaldavDao; import org.tasks.data.CaldavTask; import org.tasks.data.Filter; import org.tasks.data.FilterDao; +import org.tasks.data.Geofence; import org.tasks.data.GoogleTask; import org.tasks.data.GoogleTaskAccount; import org.tasks.data.GoogleTaskDao; import org.tasks.data.GoogleTaskList; import org.tasks.data.GoogleTaskListDao; -import org.tasks.data.Location; import org.tasks.data.LocationDao; +import org.tasks.data.Place; import org.tasks.data.Tag; import org.tasks.data.TagDao; import org.tasks.data.TagData; @@ -139,6 +141,11 @@ public class TasksJsonImporter { googleTaskListDao.insert(googleTaskAccount); } } + for (Place place : backupContainer.getPlaces()) { + if (locationDao.getByUid(place.getUid()) == null) { + locationDao.insert(place); + } + } for (GoogleTaskList googleTaskList : backupContainer.getGoogleTaskLists()) { if (googleTaskListDao.getByRemoteId(googleTaskList.getRemoteId()) == null) { googleTaskListDao.insert(googleTaskList); @@ -185,14 +192,32 @@ public class TasksJsonImporter { googleTask.setTask(taskId); googleTaskDao.insert(googleTask); } + for (LegacyLocation location : backup.locations) { + Place place = new Place(); + place.setUid(UUIDHelper.newUUID()); + place.setLongitude(location.longitude); + place.setLatitude(location.latitude); + place.setName(location.name); + place.setAddress(location.address); + place.setUrl(location.url); + place.setPhone(location.phone); + locationDao.insert(place); + Geofence geofence = new Geofence(); + geofence.setTask(taskId); + geofence.setPlace(place.getUid()); + geofence.setRadius(location.radius); + geofence.setArrival(location.arrival); + geofence.setDeparture(location.departure); + locationDao.insert(geofence); + } for (Tag tag : backup.tags) { tag.setTask(taskId); tag.setTaskUid(taskUuid); tagDao.insert(tag); } - for (Location location : backup.locations) { - location.setTask(taskId); - locationDao.insert(location); + for (Geofence geofence : backup.getGeofences()) { + geofence.setTask(taskId); + locationDao.insert(geofence); } for (TaskAttachment attachment : backup.getAttachments()) { attachment.setTaskId(taskUuid); @@ -239,4 +264,16 @@ public class TasksJsonImporter { .setPositiveButton(android.R.string.ok, (dialog, id) -> dialog.dismiss()) .show(); } + + static class LegacyLocation { + String name; + String address; + String phone; + String url; + double latitude; + double longitude; + int radius; + boolean arrival; + boolean departure; + } } diff --git a/app/src/main/java/org/tasks/data/DeletionDao.java b/app/src/main/java/org/tasks/data/DeletionDao.java index cf583b243..328da7a5c 100644 --- a/app/src/main/java/org/tasks/data/DeletionDao.java +++ b/app/src/main/java/org/tasks/data/DeletionDao.java @@ -25,7 +25,7 @@ public abstract class DeletionDao { @Query("DELETE FROM tags WHERE task IN(:ids)") abstract void deleteTags(List ids); - @Query("DELETE FROM locations WHERE task IN(:ids)") + @Query("DELETE FROM geofences WHERE task IN(:ids)") abstract void deleteGeofences(List ids); @Query("DELETE FROM alarms WHERE task IN(:ids)") diff --git a/app/src/main/java/org/tasks/data/Geofence.java b/app/src/main/java/org/tasks/data/Geofence.java new file mode 100644 index 000000000..c3d8ad6cd --- /dev/null +++ b/app/src/main/java/org/tasks/data/Geofence.java @@ -0,0 +1,189 @@ +package org.tasks.data; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.PrimaryKey; +import java.io.Serializable; + +@Entity(tableName = "geofences") +public class Geofence implements Serializable, Parcelable { + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public Geofence createFromParcel(Parcel source) { + return new Geofence(source); + } + + @Override + public Geofence[] newArray(int size) { + return new Geofence[size]; + } + }; + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "geofence_id") + private transient long id; + + @ColumnInfo(name = "task") + private transient long task; + + @ColumnInfo(name = "place") + private String place; + + @ColumnInfo(name = "radius") + private int radius; + + @ColumnInfo(name = "arrival") + private boolean arrival; + + @ColumnInfo(name = "departure") + private boolean departure; + + public Geofence() {} + + @Ignore + public Geofence(Geofence o) { + id = o.id; + task = o.task; + place = o.place; + radius = o.radius; + arrival = o.arrival; + departure = o.departure; + } + + @Ignore + public Geofence(Parcel parcel) { + id = parcel.readLong(); + task = parcel.readLong(); + place = parcel.readString(); + radius = parcel.readInt(); + arrival = parcel.readInt() == 1; + departure = parcel.readInt() == 1; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getTask() { + return task; + } + + public void setTask(long task) { + this.task = task; + } + + public String getPlace() { + return place; + } + + public void setPlace(String place) { + this.place = place; + } + + public int getRadius() { + return radius; + } + + public void setRadius(int radius) { + this.radius = radius; + } + + public boolean isArrival() { + return arrival; + } + + public void setArrival(boolean arrival) { + this.arrival = arrival; + } + + public boolean isDeparture() { + return departure; + } + + public void setDeparture(boolean departure) { + this.departure = departure; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Geofence location = (Geofence) o; + + if (id != location.id) { + return false; + } + if (task != location.task) { + return false; + } + if (radius != location.radius) { + return false; + } + if (arrival != location.arrival) { + return false; + } + if (departure != location.departure) { + return false; + } + return place != null ? place.equals(location.place) : location.place == null; + } + + @Override + public int hashCode() { + int result = (int) (id ^ (id >>> 32)); + result = 31 * result + (int) (task ^ (task >>> 32)); + result = 31 * result + (place != null ? place.hashCode() : 0); + result = 31 * result + radius; + result = 31 * result + (arrival ? 1 : 0); + result = 31 * result + (departure ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "Location{" + + "id=" + + id + + ", task=" + + task + + ", place='" + + place + + '\'' + + ", radius=" + + radius + + ", arrival=" + + arrival + + ", departure=" + + departure + + '}'; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeLong(id); + out.writeLong(task); + out.writeString(place); + out.writeInt(radius); + out.writeInt(arrival ? 1 : 0); + out.writeInt(departure ? 1 : 0); + } +} diff --git a/app/src/main/java/org/tasks/data/Location.java b/app/src/main/java/org/tasks/data/Location.java index 7e50e3609..83faa1af9 100644 --- a/app/src/main/java/org/tasks/data/Location.java +++ b/app/src/main/java/org/tasks/data/Location.java @@ -1,25 +1,17 @@ package org.tasks.data; -import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.PrimaryKey; -import com.google.common.base.Strings; +import androidx.room.Embedded; import java.io.Serializable; -import java.util.regex.Pattern; -import org.tasks.backup.XmlReader; -@Entity(tableName = "locations") public class Location implements Serializable, Parcelable { - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { + public static final Creator CREATOR = + new Creator() { @Override - public Location createFromParcel(Parcel source) { - return new Location(source); + public Location createFromParcel(Parcel in) { + return new Location(in); } @Override @@ -27,187 +19,117 @@ public class Location implements Serializable, Parcelable { return new Location[size]; } }; - private static final Pattern COORDS = - Pattern.compile("^\\d+°\\d+'\\d+\\.\\d+\"[NS] \\d+°\\d+'\\d+\\.\\d+\"[EW]$"); - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = "_id") - private transient long id; - @ColumnInfo(name = "task") - private transient long task; - - @ColumnInfo(name = "name") - private String name; - - @ColumnInfo(name = "address") - private String address; - - @ColumnInfo(name = "phone") - private String phone; - - @ColumnInfo(name = "url") - private String url; - - @ColumnInfo(name = "latitude") - private double latitude; - - @ColumnInfo(name = "longitude") - private double longitude; - - @ColumnInfo(name = "radius") - private int radius; - - @ColumnInfo(name = "arrival") - private boolean arrival; - - @ColumnInfo(name = "departure") - private boolean departure; + @Embedded public Geofence geofence; + @Embedded public Place place; public Location() {} - @Ignore - public Location(Location o) { - id = o.id; - task = o.task; - name = o.name; - address = o.address; - phone = o.phone; - url = o.url; - latitude = o.latitude; - longitude = o.longitude; - radius = o.radius; - arrival = o.arrival; - departure = o.departure; - } - - @Ignore - public Location(Parcel parcel) { - id = parcel.readLong(); - task = parcel.readLong(); - name = parcel.readString(); - address = parcel.readString(); - phone = parcel.readString(); - url = parcel.readString(); - latitude = parcel.readDouble(); - longitude = parcel.readDouble(); - radius = parcel.readInt(); - arrival = parcel.readInt() == 1; - departure = parcel.readInt() == 1; + public Location(Geofence geofence, Place place) { + this.geofence = geofence; + this.place = place; } - @Ignore - public Location(XmlReader xml) { - xml.readString("name", this::setName); - xml.readDouble("latitude", this::setLatitude); - xml.readDouble("longitude", this::setLongitude); - xml.readInteger("radius", this::setRadius); + protected Location(Parcel in) { + geofence = in.readParcelable(Geofence.class.getClassLoader()); + place = in.readParcelable(Place.class.getClassLoader()); } public long getId() { - return id; + return geofence.getId(); } public void setId(long id) { - this.id = id; + geofence.setId(id); } public long getTask() { - return task; + return geofence.getTask(); } public void setTask(long task) { - this.task = task; + geofence.setTask(task); } public String getName() { - return name; + return place.getName(); } public void setName(String name) { - this.name = name; + place.setName(name); } public double getLatitude() { - return latitude; + return place.getLatitude(); } public void setLatitude(double latitude) { - this.latitude = latitude; + place.setLatitude(latitude); } public double getLongitude() { - return longitude; + return place.getLongitude(); } public void setLongitude(double longitude) { - this.longitude = longitude; + place.setLongitude(longitude); } public int getRadius() { - return radius; + return geofence.getRadius(); } public void setRadius(int radius) { - this.radius = radius; + geofence.setRadius(radius); } public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; + return place.getAddress(); } public String getPhone() { - return phone; + return place.getPhone(); } public void setPhone(String phone) { - this.phone = phone; + place.setPhone(phone); } public String getUrl() { - return url; + return place.getUrl(); } public void setUrl(String url) { - this.url = url; + place.setUrl(url); } public boolean isArrival() { - return arrival; + return geofence.isArrival(); } public void setArrival(boolean arrival) { - this.arrival = arrival; + geofence.setArrival(arrival); } public boolean isDeparture() { - return departure; + return geofence.isDeparture(); } public void setDeparture(boolean departure) { - this.departure = departure; + geofence.setDeparture(departure); } public String getDisplayName() { - if (Strings.isNullOrEmpty(address)) { - return name; - } - if (COORDS.matcher(name).matches()) { - return address; - } - if (address.startsWith(name)) { - return address; - } - return name; + return place.getDisplayName(); } public String getGeoUri() { - return String.format( - "geo:%s,%s?q=%s", - latitude, longitude, Uri.encode(Strings.isNullOrEmpty(address) ? name : address)); + return place.getGeoUri(); + } + + @Override + public String toString() { + return "Location{" + "geofence=" + geofence + ", place=" + place + '}'; } @Override @@ -215,114 +137,33 @@ public class Location implements Serializable, Parcelable { if (this == o) { return true; } - if (!(o instanceof Location)) { + if (o == null || getClass() != o.getClass()) { return false; } Location location = (Location) o; - if (id != location.id) { - return false; - } - if (task != location.task) { - return false; - } - if (Double.compare(location.latitude, latitude) != 0) { - return false; - } - if (Double.compare(location.longitude, longitude) != 0) { - return false; - } - if (radius != location.radius) { - return false; - } - if (arrival != location.arrival) { - return false; - } - if (departure != location.departure) { + if (geofence != null ? !geofence.equals(location.geofence) : location.geofence != null) { return false; } - if (name != null ? !name.equals(location.name) : location.name != null) { - return false; - } - if (address != null ? !address.equals(location.address) : location.address != null) { - return false; - } - if (phone != null ? !phone.equals(location.phone) : location.phone != null) { - return false; - } - return url != null ? url.equals(location.url) : location.url == null; + return place != null ? place.equals(location.place) : location.place == null; } @Override public int hashCode() { - int result; - long temp; - result = (int) (id ^ (id >>> 32)); - result = 31 * result + (int) (task ^ (task >>> 32)); - result = 31 * result + (name != null ? name.hashCode() : 0); - result = 31 * result + (address != null ? address.hashCode() : 0); - result = 31 * result + (phone != null ? phone.hashCode() : 0); - result = 31 * result + (url != null ? url.hashCode() : 0); - temp = Double.doubleToLongBits(latitude); - result = 31 * result + (int) (temp ^ (temp >>> 32)); - temp = Double.doubleToLongBits(longitude); - result = 31 * result + (int) (temp ^ (temp >>> 32)); - result = 31 * result + radius; - result = 31 * result + (arrival ? 1 : 0); - result = 31 * result + (departure ? 1 : 0); + int result = geofence != null ? geofence.hashCode() : 0; + result = 31 * result + (place != null ? place.hashCode() : 0); return result; } - @Override - public String toString() { - return "Location{" - + "id=" - + id - + ", task=" - + task - + ", name='" - + name - + '\'' - + ", address='" - + address - + '\'' - + ", phone='" - + phone - + '\'' - + ", url='" - + url - + '\'' - + ", latitude=" - + latitude - + ", longitude=" - + longitude - + ", radius=" - + radius - + ", arrival=" - + arrival - + ", departure=" - + departure - + '}'; - } - @Override public int describeContents() { return 0; } @Override - public void writeToParcel(Parcel out, int flags) { - out.writeLong(id); - out.writeLong(task); - out.writeString(name); - out.writeString(address); - out.writeString(phone); - out.writeString(url); - out.writeDouble(latitude); - out.writeDouble(longitude); - out.writeInt(radius); - out.writeInt(arrival ? 1 : 0); - out.writeInt(departure ? 1 : 0); + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(geofence, flags); + dest.writeParcelable(place, flags); } } diff --git a/app/src/main/java/org/tasks/data/LocationDao.java b/app/src/main/java/org/tasks/data/LocationDao.java index 104a11c3c..e40d9552b 100644 --- a/app/src/main/java/org/tasks/data/LocationDao.java +++ b/app/src/main/java/org/tasks/data/LocationDao.java @@ -3,6 +3,7 @@ package org.tasks.data; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; +import androidx.room.OnConflictStrategy; import androidx.room.Query; import io.reactivex.Single; import java.util.List; @@ -10,26 +11,39 @@ import java.util.List; @Dao public interface LocationDao { - @Query("SELECT * FROM locations WHERE _id = :id LIMIT 1") + @Query( + "SELECT * FROM geofences INNER JOIN places ON geofences.place = places.uid WHERE geofence_id = :id LIMIT 1") Location getGeofence(Long id); - @Query("SELECT * FROM locations WHERE task = :taskId ORDER BY name ASC") + @Query("SELECT * FROM geofences INNER JOIN places ON geofences.place = places.uid WHERE task = :taskId ORDER BY name ASC") List getGeofences(long taskId); @Query( - "SELECT locations.* FROM locations INNER JOIN tasks ON tasks._id = locations.task WHERE tasks._id = :taskId AND tasks.deleted = 0 AND tasks.completed = 0") + "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 getActiveGeofences(long taskId); @Query( - "SELECT locations.* FROM locations INNER JOIN tasks ON tasks._id = locations.task WHERE tasks.deleted = 0 AND tasks.completed = 0") + "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 getActiveGeofences(); - @Query("SELECT COUNT(*) FROM locations") + @Query("SELECT COUNT(*) FROM geofences") Single geofenceCount(); @Delete - void delete(Location location); + void delete(Geofence location); @Insert - void insert(Location location); + void insert(Geofence location); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + long insert(Place place); + + @Query("SELECT * FROM places WHERE uid = :uid LIMIT 1") + Place getByUid(String uid); + + @Query("SELECT * FROM geofences WHERE task = :taskId") + List getGeofencesForTask(long taskId); + + @Query("SELECT * FROM places") + List getPlaces(); } diff --git a/app/src/main/java/org/tasks/data/Place.java b/app/src/main/java/org/tasks/data/Place.java new file mode 100644 index 000000000..caceef674 --- /dev/null +++ b/app/src/main/java/org/tasks/data/Place.java @@ -0,0 +1,262 @@ +package org.tasks.data; + +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.PrimaryKey; +import com.google.common.base.Strings; +import java.io.Serializable; +import java.util.regex.Pattern; + +@Entity(tableName = "places") +public class Place implements Serializable, Parcelable { + + public static final Creator CREATOR = + new Creator() { + @Override + public Place createFromParcel(Parcel source) { + return new Place(source); + } + + @Override + public Place[] newArray(int size) { + return new Place[size]; + } + }; + + private static final Pattern COORDS = + Pattern.compile("^\\d+°\\d+'\\d+\\.\\d+\"[NS] \\d+°\\d+'\\d+\\.\\d+\"[EW]$"); + + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "place_id") + private transient long id; + + @ColumnInfo(name = "uid") + private String uid; + + @ColumnInfo(name = "name") + private String name; + + @ColumnInfo(name = "address") + private String address; + + @ColumnInfo(name = "phone") + private String phone; + + @ColumnInfo(name = "url") + private String url; + + @ColumnInfo(name = "latitude") + private double latitude; + + @ColumnInfo(name = "longitude") + private double longitude; + + public Place() {} + + @Ignore + public Place(Place o) { + id = o.id; + uid = o.uid; + name = o.name; + address = o.address; + phone = o.phone; + url = o.url; + latitude = o.latitude; + longitude = o.longitude; + } + + @Ignore + public Place(Parcel parcel) { + id = parcel.readLong(); + uid = parcel.readString(); + name = parcel.readString(); + address = parcel.readString(); + phone = parcel.readString(); + url = parcel.readString(); + latitude = parcel.readDouble(); + longitude = parcel.readDouble(); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getDisplayName() { + if (Strings.isNullOrEmpty(address)) { + return name; + } + if (COORDS.matcher(name).matches()) { + return address; + } + if (address.startsWith(name)) { + return address; + } + return name; + } + + public String getGeoUri() { + return String.format( + "geo:%s,%s?q=%s", + latitude, longitude, Uri.encode(Strings.isNullOrEmpty(address) ? name : address)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Place place = (Place) o; + + if (id != place.id) { + return false; + } + if (Double.compare(place.latitude, latitude) != 0) { + return false; + } + if (Double.compare(place.longitude, longitude) != 0) { + return false; + } + if (uid != null ? !uid.equals(place.uid) : place.uid != null) { + return false; + } + if (name != null ? !name.equals(place.name) : place.name != null) { + return false; + } + if (address != null ? !address.equals(place.address) : place.address != null) { + return false; + } + if (phone != null ? !phone.equals(place.phone) : place.phone != null) { + return false; + } + return url != null ? url.equals(place.url) : place.url == null; + } + + @Override + public int hashCode() { + int result; + long temp; + result = (int) (id ^ (id >>> 32)); + result = 31 * result + (uid != null ? uid.hashCode() : 0); + result = 31 * result + (name != null ? name.hashCode() : 0); + result = 31 * result + (address != null ? address.hashCode() : 0); + result = 31 * result + (phone != null ? phone.hashCode() : 0); + result = 31 * result + (url != null ? url.hashCode() : 0); + temp = Double.doubleToLongBits(latitude); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(longitude); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "Place{" + + "id=" + + id + + ", uid='" + + uid + + '\'' + + ", name='" + + name + + '\'' + + ", address='" + + address + + '\'' + + ", phone='" + + phone + + '\'' + + ", url='" + + url + + '\'' + + ", latitude=" + + latitude + + ", longitude=" + + longitude + + '}'; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeLong(id); + out.writeString(uid); + out.writeString(name); + out.writeString(address); + out.writeString(phone); + out.writeString(url); + out.writeDouble(latitude); + out.writeDouble(longitude); + } +} diff --git a/app/src/main/java/org/tasks/db/Migrations.java b/app/src/main/java/org/tasks/db/Migrations.java index 5c202704c..0b14ec5b3 100644 --- a/app/src/main/java/org/tasks/db/Migrations.java +++ b/app/src/main/java/org/tasks/db/Migrations.java @@ -218,6 +218,18 @@ public class Migrations { } }; + private static final Migration MIGRATION_60_61 = + new Migration(60, 61) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `places` (`place_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `name` TEXT, `address` TEXT, `phone` TEXT, `url` TEXT, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `geofences` (`geofence_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `place` TEXT, `radius` INTEGER NOT NULL, `arrival` INTEGER NOT NULL, `departure` INTEGER NOT NULL)"); + database.execSQL("INSERT INTO `places` (`place_id`, `uid`, `name`, `address`, `phone`, `url`, `latitude`, `longitude`) SELECT `_id`, hex(randomblob(16)), `name`, `address`, `phone`, `url`, `latitude`, `longitude` FROM `locations`"); + database.execSQL("INSERT INTO `geofences` (`geofence_id`, `task`, `place`, `radius`, `arrival`, `departure`) SELECT `_id`, `task`, `uid`, `radius`, `arrival`, `departure` FROM `locations` INNER JOIN `places` ON `_id` = `place_id`"); + database.execSQL("DROP TABLE `locations`"); + } + }; + public static final Migration[] MIGRATIONS = new Migration[] { MIGRATION_35_36, @@ -235,7 +247,8 @@ public class Migrations { MIGRATION_53_54, MIGRATION_54_58, MIGRATION_58_59, - MIGRATION_59_60 + MIGRATION_59_60, + MIGRATION_60_61 }; private static Migration NOOP(int from, int to) { diff --git a/app/src/main/java/org/tasks/dialogs/LocationDialog.java b/app/src/main/java/org/tasks/dialogs/LocationDialog.java index 39cd8f8b7..a0bcc9cfa 100644 --- a/app/src/main/java/org/tasks/dialogs/LocationDialog.java +++ b/app/src/main/java/org/tasks/dialogs/LocationDialog.java @@ -112,7 +112,7 @@ public class LocationDialog extends InjectingDialogFragment { } private Location getOriginal() { - return new Location(getArguments().getParcelable(EXTRA_ORIGINAL)); + return getArguments().getParcelable(EXTRA_ORIGINAL); } private void sendResult(DialogInterface d, int... i) { diff --git a/app/src/main/java/org/tasks/location/GeofenceService.java b/app/src/main/java/org/tasks/location/GeofenceService.java index 02aab39c4..530c79432 100644 --- a/app/src/main/java/org/tasks/location/GeofenceService.java +++ b/app/src/main/java/org/tasks/location/GeofenceService.java @@ -7,8 +7,10 @@ import static java.util.Collections.emptyList; import java.util.List; import java.util.Set; import javax.inject.Inject; +import org.tasks.data.Geofence; import org.tasks.data.Location; import org.tasks.data.LocationDao; +import org.tasks.data.Place; public class GeofenceService { @@ -71,14 +73,18 @@ public class GeofenceService { if (callback != null) { callback.beforeDelete(item); } - locationDao.delete(item); + locationDao.delete(item.geofence); dirty = true; } } // everything that remains shall be written for (Location location : locations) { - locationDao.insert(location); + Place place = location.place; + locationDao.insert(place); + Geofence geofence = location.geofence; + geofence.setPlace(place.getUid()); + locationDao.insert(geofence); dirty = true; } diff --git a/app/src/main/java/org/tasks/ui/LocationControlSet.java b/app/src/main/java/org/tasks/ui/LocationControlSet.java index 1d62924a7..225bb3968 100644 --- a/app/src/main/java/org/tasks/ui/LocationControlSet.java +++ b/app/src/main/java/org/tasks/ui/LocationControlSet.java @@ -150,10 +150,6 @@ public class LocationControlSet extends TaskEditControlFragment { if (requestCode == REQUEST_LOCATION_REMINDER) { if (resultCode == Activity.RESULT_OK) { Location place = PlacePicker.getPlace(context, data, preferences); - int defaultReminders = - preferences.getIntegerFromString(R.string.p_default_location_reminder_key, 1); - place.setArrival(defaultReminders == 1 || defaultReminders == 3); - place.setDeparture(defaultReminders == 2 || defaultReminders == 3); locations.add(place); setup(locations); }