From c9869bab0e5365d94b7224e9f930fbf084324a7f Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Sun, 28 Jun 2020 18:09:08 +0200 Subject: [PATCH] Generate UUID for each track (necessary for synchronization). Fixes #294. --- .../CustomContentProviderUtilsTest.java | 30 ++++++---- .../provider/CustomSQLiteOpenHelperTest.java | 49 ++++++++-------- .../opentracks/util/UUIDUtilsTest.java | 23 ++++++++ .../opentracks/content/data/Track.java | 12 ++++ .../content/data/TracksColumns.java | 7 ++- .../provider/ContentProviderUtils.java | 6 ++ .../provider/CustomSQLiteOpenHelper.java | 56 +++++++++++++++++-- .../dennisguse/opentracks/util/UUIDUtils.java | 23 ++++++++ 8 files changed, 166 insertions(+), 40 deletions(-) create mode 100644 src/androidTest/java/de/dennisguse/opentracks/util/UUIDUtilsTest.java create mode 100644 src/main/java/de/dennisguse/opentracks/util/UUIDUtils.java diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java index f2c36ebbe..93ba7826e 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomContentProviderUtilsTest.java @@ -37,6 +37,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.UUID; import de.dennisguse.opentracks.content.data.TestDataUtil; import de.dennisguse.opentracks.content.data.Track; @@ -47,6 +48,7 @@ import de.dennisguse.opentracks.content.data.Waypoint; import de.dennisguse.opentracks.content.data.WaypointsColumns; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.util.FileUtils; +import de.dennisguse.opentracks.util.UUIDUtils; import static org.mockito.Mockito.when; @@ -171,22 +173,26 @@ public class CustomContentProviderUtilsTest { */ @Test public void testCreateTrack() { - int startColumnIndex = 1; - int columnIndex = startColumnIndex; - when(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)).thenReturn(columnIndex++); - when(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).thenReturn(columnIndex++); - columnIndex = startColumnIndex; - // Id - when(cursorMock.isNull(columnIndex++)).thenReturn(false); - // Name - when(cursorMock.isNull(columnIndex++)).thenReturn(false); long trackId = System.currentTimeMillis(); - columnIndex = startColumnIndex; + + int columnIndex = 1; // Id - when(cursorMock.getLong(columnIndex++)).thenReturn(trackId); + when(cursorMock.getColumnIndexOrThrow(TracksColumns._ID)).thenReturn(columnIndex); + when(cursorMock.isNull(columnIndex)).thenReturn(false); + when(cursorMock.getLong(columnIndex)).thenReturn(trackId); + + //Uuid + columnIndex++; + when(cursorMock.getColumnIndexOrThrow(TracksColumns.UUID)).thenReturn(columnIndex); + when(cursorMock.isNull(columnIndex)).thenReturn(false); + when(cursorMock.getBlob(columnIndex)).thenReturn(UUIDUtils.toBytes(UUID.randomUUID())); + // Name + columnIndex++; String name = NAME_PREFIX + trackId; - when(cursorMock.getString(columnIndex++)).thenReturn(name); + when(cursorMock.getColumnIndexOrThrow(TracksColumns.NAME)).thenReturn(columnIndex); + when(cursorMock.isNull(columnIndex)).thenReturn(false); + when(cursorMock.getString(columnIndex)).thenReturn(name); Track track = contentProviderUtils.createTrack(cursorMock); Assert.assertEquals(trackId, track.getId()); diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelperTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelperTest.java index 15eaa1f07..6bab2c3b1 100644 --- a/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelperTest.java +++ b/src/androidTest/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelperTest.java @@ -5,6 +5,7 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import androidx.annotation.VisibleForTesting; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -36,7 +37,8 @@ public class CustomSQLiteOpenHelperTest { * * @return Map(TableName, SQL) */ - private static Map getTableSQL(SQLiteDatabase db) { + @VisibleForTesting + public static Map getTableSQL(SQLiteDatabase db) { HashMap tableSQL = new HashMap<>(); try (Cursor cursor = db.query("sqlite_master", new String[]{"name", "SQL"}, "name IN ('" + TracksColumns.TABLE_NAME + "', '" + TrackPointsColumns.TABLE_NAME + "', '" + WaypointsColumns.TABLE_NAME + "')", null, null, null, "name")) { if (cursor != null) { @@ -53,6 +55,7 @@ public class CustomSQLiteOpenHelperTest { * * @return Map(tableName, SQL) */ + @VisibleForTesting public static Map getIndexSQL(SQLiteDatabase db) { HashMap indexSQL = new HashMap<>(); try (Cursor cursor = db.rawQuery("SELECT tbl_name, SQL FROM sqlite_master WHERE type = 'index'", null)) { @@ -104,24 +107,7 @@ public class CustomSQLiteOpenHelperTest { @Test public void onUpgrade_FromVersion23() { - // Manually create database schema with version 23 (base version) - { - SQLiteDatabase dbBase = new SQLiteOpenHelper(context, DATABASE_NAME, null, 23) { - @Override - public void onCreate(SQLiteDatabase db) { - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - } - }.getWritableDatabase(); - - dbBase.execSQL(TRACKS_CREATE_TABLE_V23); - dbBase.execSQL(TRACKPOINTS_CREATE_TABLE_V23); - dbBase.execSQL(WAYPOINTS_CREATE_TABLE_V23); - - dbBase.close(); - } + createVersion23(); // Open database with SQL upgrade Map tableByUpgrade; @@ -130,10 +116,9 @@ public class CustomSQLiteOpenHelperTest { tableByUpgrade = getTableSQL(dbUpgraded); indicesByUpgrade = getIndexSQL(dbUpgraded); } - - // Open database via context.deleteDatabase(DATABASE_NAME); + // Open database via creation script Map tablesByCreate; Map indicesByCreate; try (SQLiteDatabase dbCreated = new CustomSQLiteOpenHelper(context, DATABASE_NAME).getReadableDatabase()) { @@ -150,7 +135,8 @@ public class CustomSQLiteOpenHelperTest { Assert.assertEquals(tablesByCreate.get(WaypointsColumns.TABLE_NAME), tableByUpgrade.get(WaypointsColumns.TABLE_NAME)); // then - verify custom indices - Assert.assertEquals(2, indicesByCreate.size()); + Assert.assertEquals(3, indicesByCreate.size()); + Assert.assertEquals(indicesByUpgrade.get(TracksColumns.TABLE_NAME), indicesByCreate.get(TracksColumns.TABLE_NAME)); Assert.assertEquals(indicesByUpgrade.get(TrackPointsColumns.TABLE_NAME), indicesByCreate.get(TrackPointsColumns.TABLE_NAME)); Assert.assertEquals(indicesByUpgrade.get(WaypointsColumns.TABLE_NAME), indicesByCreate.get(WaypointsColumns.TABLE_NAME)); } @@ -176,4 +162,23 @@ public class CustomSQLiteOpenHelperTest { // then - verify custom indices Assert.assertEquals(0, indicesByDowngrade.size()); } + + private void createVersion23() { + // Manually create database schema with version 23 (base version) + SQLiteDatabase dbBase = new SQLiteOpenHelper(context, DATABASE_NAME, null, 23) { + @Override + public void onCreate(SQLiteDatabase db) { + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + } + }.getWritableDatabase(); + + dbBase.execSQL(TRACKS_CREATE_TABLE_V23); + dbBase.execSQL(TRACKPOINTS_CREATE_TABLE_V23); + dbBase.execSQL(WAYPOINTS_CREATE_TABLE_V23); + + dbBase.close(); + } } \ No newline at end of file diff --git a/src/androidTest/java/de/dennisguse/opentracks/util/UUIDUtilsTest.java b/src/androidTest/java/de/dennisguse/opentracks/util/UUIDUtilsTest.java new file mode 100644 index 000000000..daf1a5bdf --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/util/UUIDUtilsTest.java @@ -0,0 +1,23 @@ +package de.dennisguse.opentracks.util; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.UUID; + +@RunWith(AndroidJUnit4.class) +public class UUIDUtilsTest { + + @Test + public void test() { + UUID uuid = UUID.randomUUID(); + + byte[] bytes = UUIDUtils.toBytes(uuid); + UUID output = UUIDUtils.fromBytes(bytes); + + Assert.assertEquals(uuid, output); + } +} \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/content/data/Track.java b/src/main/java/de/dennisguse/opentracks/content/data/Track.java index 7bbcf4d50..ea6aa0821 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/Track.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/Track.java @@ -16,6 +16,8 @@ package de.dennisguse.opentracks.content.data; +import java.util.UUID; + import de.dennisguse.opentracks.stats.TrackStatistics; /** @@ -27,6 +29,8 @@ import de.dennisguse.opentracks.stats.TrackStatistics; public class Track { private long id = -1L; + private UUID uuid = UUID.randomUUID(); + private String name = ""; private String description = ""; private String category = ""; @@ -46,6 +50,14 @@ public class Track { this.id = id; } + public UUID getUuid() { + return uuid; + } + + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + public String getName() { return name; } diff --git a/src/main/java/de/dennisguse/opentracks/content/data/TracksColumns.java b/src/main/java/de/dennisguse/opentracks/content/data/TracksColumns.java index ba36ca0a2..aacb20641 100644 --- a/src/main/java/de/dennisguse/opentracks/content/data/TracksColumns.java +++ b/src/main/java/de/dennisguse/opentracks/content/data/TracksColumns.java @@ -35,6 +35,7 @@ public interface TracksColumns extends BaseColumns { String DEFAULT_SORT_ORDER = "_id"; // Columns + String UUID = "uuid"; String NAME = "name"; // track name String DESCRIPTION = "description"; // track description String CATEGORY = "category"; // track activity type @@ -71,5 +72,9 @@ public interface TracksColumns extends BaseColumns { + MINELEVATION + " FLOAT, " // min elevation + MAXELEVATION + " FLOAT, " // max elevation + ELEVATIONGAIN + " FLOAT, " // elevation gain - + ICON + " TEXT)"; // icon + + ICON + " TEXT, " // icon + + UUID + " BLOB)"; // UUID + + String CREATE_TABLE_INDEX = "CREATE UNIQUE INDEX " + TABLE_NAME + "_" + UUID + "_index ON " + TABLE_NAME + "(" + UUID + ")"; + } diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java index be49a725f..7dd72e6e5 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java @@ -42,6 +42,7 @@ import de.dennisguse.opentracks.content.data.Waypoint; import de.dennisguse.opentracks.content.data.WaypointsColumns; import de.dennisguse.opentracks.stats.TrackStatistics; import de.dennisguse.opentracks.util.FileUtils; +import de.dennisguse.opentracks.util.UUIDUtils; /** * {@link ContentProviderUtils} implementation. @@ -99,6 +100,7 @@ public class ContentProviderUtils { */ public Track createTrack(Cursor cursor) { int idIndex = cursor.getColumnIndexOrThrow(TracksColumns._ID); + int uuidIndex = cursor.getColumnIndexOrThrow(TracksColumns.UUID); int nameIndex = cursor.getColumnIndexOrThrow(TracksColumns.NAME); int descriptionIndex = cursor.getColumnIndexOrThrow(TracksColumns.DESCRIPTION); int categoryIndex = cursor.getColumnIndexOrThrow(TracksColumns.CATEGORY); @@ -118,6 +120,9 @@ public class ContentProviderUtils { if (!cursor.isNull(idIndex)) { track.setId(cursor.getLong(idIndex)); } + if (!cursor.isNull(uuidIndex)) { + track.setUuid(UUIDUtils.fromBytes(cursor.getBlob(uuidIndex))); + } if (!cursor.isNull(nameIndex)) { track.setName(cursor.getString(nameIndex)); } @@ -287,6 +292,7 @@ public class ContentProviderUtils { if (track.getId() >= 0) { values.put(TracksColumns._ID, track.getId()); } + values.put(TracksColumns.UUID, UUIDUtils.toBytes(track.getUuid())); values.put(TracksColumns.NAME, track.getName()); values.put(TracksColumns.DESCRIPTION, track.getDescription()); values.put(TracksColumns.CATEGORY, track.getCategory()); diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelper.java b/src/main/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelper.java index f545233b9..7ef23b53d 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelper.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/CustomSQLiteOpenHelper.java @@ -1,15 +1,20 @@ package de.dennisguse.opentracks.content.provider; +import android.content.ContentValues; import android.content.Context; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; import androidx.annotation.VisibleForTesting; +import java.util.UUID; + import de.dennisguse.opentracks.content.data.TrackPointsColumns; import de.dennisguse.opentracks.content.data.TracksColumns; import de.dennisguse.opentracks.content.data.WaypointsColumns; +import de.dennisguse.opentracks.util.UUIDUtils; /** * Database helper for creating and upgrading the database. @@ -17,13 +22,11 @@ import de.dennisguse.opentracks.content.data.WaypointsColumns; @VisibleForTesting class CustomSQLiteOpenHelper extends SQLiteOpenHelper { - @VisibleForTesting - private static final int DATABASE_VERSION = 25; private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName(); - @VisibleForTesting - private static final String DATABASE_NAME = "database.db"; + private static final int DATABASE_VERSION = 26; + private static final String DATABASE_NAME = "database.db"; public CustomSQLiteOpenHelper(Context context) { this(context, DATABASE_NAME); @@ -45,6 +48,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper { db.execSQL(TrackPointsColumns.CREATE_TABLE_INDEX); db.execSQL(TracksColumns.CREATE_TABLE); + db.execSQL(TracksColumns.CREATE_TABLE_INDEX); db.execSQL(WaypointsColumns.CREATE_TABLE); db.execSQL(WaypointsColumns.CREATE_TABLE_INDEX); @@ -61,6 +65,9 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper { case 25: upgradeFrom24to25(db); break; + case 26: + upgradeFrom25to26(db); + break; default: throw new RuntimeException("Not implemented: upgrade to " + toVersion); @@ -79,6 +86,9 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper { case 24: downgradeFrom25to24(db); break; + case 25: + downgradeFrom26to25(db); + break; default: throw new RuntimeException("Not implemented: downgrade to " + toVersion); @@ -107,7 +117,7 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper { } private void downgradeFrom24to23(SQLiteDatabase db) { - //Not needed as the delete columns did not contain any data + //Not needed as the deleted columns did not contain any data db.beginTransaction(); db.execSQL("ALTER TABLE tracks RENAME TO tracks_old"); db.execSQL("CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, icon TEXT)"); @@ -142,4 +152,40 @@ class CustomSQLiteOpenHelper extends SQLiteOpenHelper { db.setTransactionSuccessful(); db.endTransaction(); } + + private void upgradeFrom25to26(SQLiteDatabase db) { + db.beginTransaction(); + + db.execSQL("ALTER TABLE tracks ADD COLUMN uuid BLOB"); + try (Cursor cursor = db.query("tracks", new String[]{"_id"}, null, null, null, null, null)) { + if (cursor.moveToFirst()) { + int trackIdIndex = cursor.getColumnIndexOrThrow("_id"); + do { + long trackId = cursor.getLong(trackIdIndex); + ContentValues cv = new ContentValues(); + cv.put("uuid", UUIDUtils.toBytes(UUID.randomUUID())); + db.update("tracks", cv, "_id = ?", new String[]{String.valueOf(trackId)}); + } while (cursor.moveToNext()); + } + } + + db.execSQL("CREATE UNIQUE INDEX tracks_uuid_index ON tracks(uuid)"); + + db.setTransactionSuccessful(); + db.endTransaction(); + } + + private void downgradeFrom26to25(SQLiteDatabase db) { + db.beginTransaction(); + + db.execSQL("DROP INDEX tracks_uuid_index"); + + db.execSQL("ALTER TABLE tracks RENAME TO tracks_old"); + db.execSQL("CREATE TABLE tracks (_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, description TEXT, category TEXT, starttime INTEGER, stoptime INTEGER, numpoints INTEGER, totaldistance FLOAT, totaltime INTEGER, movingtime INTEGER, avgspeed FLOAT, avgmovingspeed FLOAT, maxspeed FLOAT, minelevation FLOAT, maxelevation FLOAT, elevationgain FLOAT, mingrade FLOAT, maxgrade FLOAT, icon TEXT)"); + db.execSQL("INSERT INTO tracks SELECT _id, name, description, category, starttime, stoptime, numpoints, totaldistance, totaltime, movingtime, avgspeed, avgmovingspeed, maxspeed, minelevation, maxelevation, elevationgain, 0, 0, icon FROM tracks_old"); + db.execSQL("DROP TABLE tracks_old"); + + db.setTransactionSuccessful(); + db.endTransaction(); + } } diff --git a/src/main/java/de/dennisguse/opentracks/util/UUIDUtils.java b/src/main/java/de/dennisguse/opentracks/util/UUIDUtils.java new file mode 100644 index 000000000..0a5ce7621 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/util/UUIDUtils.java @@ -0,0 +1,23 @@ +package de.dennisguse.opentracks.util; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; +import java.util.UUID; + +public class UUIDUtils { + + public static UUID fromBytes(byte[] bytes) { + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + long mostSignificant = byteBuffer.getLong(); + long lestSignificant = byteBuffer.getLong(); + return new UUID(mostSignificant, lestSignificant); + } + + public static byte[] toBytes(@NonNull UUID uuid) { + ByteBuffer byteBuffer = ByteBuffer.allocate(16); + byteBuffer.putLong(uuid.getMostSignificantBits()); + byteBuffer.putLong(uuid.getLeastSignificantBits()); + return byteBuffer.array(); + } +}