diff --git a/transport/transport-runtime/src/androidTest/java/com/google/android/datatransport/runtime/scheduling/persistence/SpyEventStoreModule.java b/transport/transport-runtime/src/androidTest/java/com/google/android/datatransport/runtime/scheduling/persistence/SpyEventStoreModule.java index c6a2828bc3e..9487774001c 100644 --- a/transport/transport-runtime/src/androidTest/java/com/google/android/datatransport/runtime/scheduling/persistence/SpyEventStoreModule.java +++ b/transport/transport-runtime/src/androidTest/java/com/google/android/datatransport/runtime/scheduling/persistence/SpyEventStoreModule.java @@ -14,11 +14,6 @@ package com.google.android.datatransport.runtime.scheduling.persistence; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_CONTEXTS_SQL_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_EVENTS_SQL_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_EVENT_BACKEND_INDEX_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_EVENT_METADATA_SQL_V1; import static org.mockito.Mockito.spy; import com.google.android.datatransport.runtime.synchronization.SynchronizationGuard; @@ -45,32 +40,8 @@ static EventStore eventStore(SQLiteEventStore store) { abstract SynchronizationGuard synchronizationGuard(SQLiteEventStore store); @Provides - @Named("CREATE_EVENTS_SQL") - static String createEventsSql() { - return CREATE_EVENTS_SQL_V1; - } - - @Provides - @Named("CREATE_EVENT_METADATA_SQL") - static String createEventMetadataSql() { - return CREATE_EVENT_METADATA_SQL_V1; - } - - @Provides - @Named("CREATE_CONTEXTS_SQL") - static String createContextsSql() { - return CREATE_CONTEXTS_SQL_V1; - } - - @Provides - @Named("CREATE_EVENT_BACKEND_INDEX") - static String getCreateEventBackendIndex() { - return CREATE_EVENT_BACKEND_INDEX_V1; - } - - @Provides - @Named("CREATE_CONTEXT_BACKEND_PRIORITY_INDEX") - static String createEventBackendPriorityIndex() { - return CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1; + @Named("SCHEMA_VERSION") + static int schemaVersion() { + return SchemaManager.SCHEMA_VERSION; } } diff --git a/transport/transport-runtime/src/androidTest/java/com/google/android/datatransport/runtime/scheduling/persistence/TestEventStoreModule.java b/transport/transport-runtime/src/androidTest/java/com/google/android/datatransport/runtime/scheduling/persistence/TestEventStoreModule.java index 77c1a819a25..758be3234e0 100644 --- a/transport/transport-runtime/src/androidTest/java/com/google/android/datatransport/runtime/scheduling/persistence/TestEventStoreModule.java +++ b/transport/transport-runtime/src/androidTest/java/com/google/android/datatransport/runtime/scheduling/persistence/TestEventStoreModule.java @@ -14,11 +14,7 @@ package com.google.android.datatransport.runtime.scheduling.persistence; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_CONTEXTS_SQL_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_EVENTS_SQL_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_EVENT_BACKEND_INDEX_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_EVENT_METADATA_SQL_V1; +import static com.google.android.datatransport.runtime.scheduling.persistence.SchemaManager.SCHEMA_VERSION; import com.google.android.datatransport.runtime.synchronization.SynchronizationGuard; import dagger.Binds; @@ -49,32 +45,8 @@ static EventStoreConfig storeConfig() { abstract SynchronizationGuard synchronizationGuard(SQLiteEventStore store); @Provides - @Named("CREATE_EVENTS_SQL") - static String createEventsSql() { - return CREATE_EVENTS_SQL_V1; - } - - @Provides - @Named("CREATE_EVENT_METADATA_SQL") - static String createEventMetadataSql() { - return CREATE_EVENT_METADATA_SQL_V1; - } - - @Provides - @Named("CREATE_CONTEXTS_SQL") - static String createContextsSql() { - return CREATE_CONTEXTS_SQL_V1; - } - - @Provides - @Named("CREATE_EVENT_BACKEND_INDEX") - static String getCreateEventBackendIndex() { - return CREATE_EVENT_BACKEND_INDEX_V1; - } - - @Provides - @Named("CREATE_CONTEXT_BACKEND_PRIORITY_INDEX") - static String createEventBackendPriorityIndex() { - return CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1; + @Named("SCHEMA_VERSION") + static int schemaVersion() { + return SCHEMA_VERSION; } } diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/Destination.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/Destination.java new file mode 100644 index 00000000000..c88dac05180 --- /dev/null +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/Destination.java @@ -0,0 +1,29 @@ +// Copyright 2018 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.android.datatransport.runtime; + +import androidx.annotation.Nullable; + +public interface Destination { + /** Name that can be used to discover the backend */ + String getName(); + + /** + * Any extras that must be passed to the backend while uploading. Uploads to the backend are + * grouped by (backend_name, priority, extras) + */ + @Nullable + byte[] getExtras(); +} diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/TransportContext.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/TransportContext.java index b82d9899c67..46353f6edc4 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/TransportContext.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/TransportContext.java @@ -14,6 +14,7 @@ package com.google.android.datatransport.runtime; +import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import com.google.android.datatransport.Priority; import com.google.auto.value.AutoValue; @@ -26,6 +27,9 @@ public abstract class TransportContext { /** Backend events are sent to. */ public abstract String getBackendName(); + @Nullable + public abstract byte[] getExtras(); + /** * Priority of the event. * @@ -58,6 +62,8 @@ public abstract static class Builder { public abstract Builder setBackendName(String name); + public abstract Builder setExtras(@Nullable byte[] extras); + /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY) public abstract Builder setPriority(Priority priority); diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/TransportRuntime.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/TransportRuntime.java index 081a16cd9d1..da2fe60e363 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/TransportRuntime.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/TransportRuntime.java @@ -104,11 +104,22 @@ static void withInstance(TransportRuntimeComponent component, Callable cal } /** Returns a {@link TransportFactory} for a given {@code backendName}. */ + @Deprecated public TransportFactory newFactory(String backendName) { return new TransportFactoryImpl( TransportContext.builder().setBackendName(backendName).build(), this); } + /** Returns a {@link TransportFactory} for a given {@code backendName}. */ + public TransportFactory newFactory(Destination destination) { + return new TransportFactoryImpl( + TransportContext.builder() + .setBackendName(destination.getName()) + .setExtras(destination.getExtras()) + .build(), + this); + } + @RestrictTo(RestrictTo.Scope.LIBRARY) public Uploader getUploader() { return uploader; diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/backends/BackendRequest.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/backends/BackendRequest.java index b51a70d2a8b..ca5b9953aa1 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/backends/BackendRequest.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/backends/BackendRequest.java @@ -14,6 +14,7 @@ package com.google.android.datatransport.runtime.backends; +import androidx.annotation.Nullable; import com.google.android.datatransport.runtime.EventInternal; import com.google.auto.value.AutoValue; @@ -23,8 +24,24 @@ public abstract class BackendRequest { /** Events to be sent to the backend. */ public abstract Iterable getEvents(); + @Nullable + public abstract byte[] getExtras(); + /** Creates a new instance of the request. */ public static BackendRequest create(Iterable events) { - return new AutoValue_BackendRequest(events); + return BackendRequest.builder().setEvents(events).build(); + } + + public static Builder builder() { + return new AutoValue_BackendRequest.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setEvents(Iterable events); + + public abstract Builder setExtras(@Nullable byte[] extras); + + public abstract BackendRequest build(); } } diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerScheduler.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerScheduler.java index 3fbed4aadb7..9862ecb73bd 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerScheduler.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerScheduler.java @@ -19,6 +19,7 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.util.Base64; import androidx.annotation.VisibleForTesting; import com.google.android.datatransport.runtime.TransportContext; import com.google.android.datatransport.runtime.scheduling.persistence.EventStore; @@ -31,6 +32,7 @@ public class AlarmManagerScheduler implements WorkScheduler { static final String ATTEMPT_NUMBER = "attemptNumber"; static final String BACKEND_NAME = "backendName"; static final String EVENT_PRIORITY = "priority"; + static final String EXTRAS = "extras"; private final Context context; @@ -78,6 +80,10 @@ public void schedule(TransportContext transportContext, int attemptNumber) { intentDataBuilder.appendQueryParameter(BACKEND_NAME, transportContext.getBackendName()); intentDataBuilder.appendQueryParameter( EVENT_PRIORITY, String.valueOf(transportContext.getPriority().ordinal())); + if (transportContext.getExtras() != null) { + intentDataBuilder.appendQueryParameter( + EXTRAS, Base64.encodeToString(transportContext.getExtras(), Base64.DEFAULT)); + } Intent intent = new Intent(context, AlarmManagerSchedulerBroadcastReceiver.class); intent.setData(intentDataBuilder.build()); intent.putExtra(ATTEMPT_NUMBER, attemptNumber); diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerSchedulerBroadcastReceiver.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerSchedulerBroadcastReceiver.java index 80844c6a9ae..98db1058b8e 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerSchedulerBroadcastReceiver.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerSchedulerBroadcastReceiver.java @@ -14,6 +14,8 @@ package com.google.android.datatransport.runtime.scheduling.jobscheduling; +import static android.util.Base64.*; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -26,15 +28,21 @@ public class AlarmManagerSchedulerBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String backendName = intent.getData().getQueryParameter(AlarmManagerScheduler.BACKEND_NAME); + String extras = intent.getData().getQueryParameter(AlarmManagerScheduler.EXTRAS); int priority = Integer.valueOf(intent.getData().getQueryParameter(AlarmManagerScheduler.EVENT_PRIORITY)); int attemptNumber = intent.getExtras().getInt(AlarmManagerScheduler.ATTEMPT_NUMBER); TransportRuntime.initialize(context); + + TransportContext.Builder transportContext = + TransportContext.builder().setBackendName(backendName).setPriority(priority); + + if (extras != null) { + transportContext.setExtras(decode(extras, DEFAULT)); + } + TransportRuntime.getInstance() .getUploader() - .upload( - TransportContext.builder().setBackendName(backendName).setPriority(priority).build(), - attemptNumber, - () -> {}); + .upload(transportContext.build(), attemptNumber, () -> {}); } } diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoScheduler.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoScheduler.java index 9d45c97caea..0a70e52cacc 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoScheduler.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoScheduler.java @@ -14,6 +14,8 @@ package com.google.android.datatransport.runtime.scheduling.jobscheduling; +import static android.util.Base64.*; + import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.content.ComponentName; @@ -36,6 +38,7 @@ public class JobInfoScheduler implements WorkScheduler { static final String ATTEMPT_NUMBER = "attemptNumber"; static final String BACKEND_NAME = "backendName"; static final String EVENT_PRIORITY = "priority"; + static final String EXTRAS = "extras"; private final Context context; @@ -97,6 +100,9 @@ public void schedule(TransportContext transportContext, int attemptNumber) { bundle.putInt(ATTEMPT_NUMBER, attemptNumber); bundle.putString(BACKEND_NAME, transportContext.getBackendName()); bundle.putInt(EVENT_PRIORITY, transportContext.getPriority().ordinal()); + if (transportContext.getExtras() != null) { + bundle.putString(EXTRAS, encodeToString(transportContext.getExtras(), DEFAULT)); + } builder.setExtras(bundle); jobScheduler.schedule(builder.build()); diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoSchedulerService.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoSchedulerService.java index 75f5f0ada28..c493c95c9b4 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoSchedulerService.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoSchedulerService.java @@ -17,6 +17,7 @@ import android.app.job.JobParameters; import android.app.job.JobService; import android.os.Build; +import android.util.Base64; import androidx.annotation.RequiresApi; import com.google.android.datatransport.runtime.TransportContext; import com.google.android.datatransport.runtime.TransportRuntime; @@ -28,15 +29,21 @@ public class JobInfoSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { String backendName = params.getExtras().getString(JobInfoScheduler.BACKEND_NAME); + String extras = params.getExtras().getString(JobInfoScheduler.EXTRAS); + int priority = params.getExtras().getInt(JobInfoScheduler.EVENT_PRIORITY); int attemptNumber = params.getExtras().getInt(JobInfoScheduler.ATTEMPT_NUMBER); TransportRuntime.initialize(getApplicationContext()); + TransportContext.Builder transportContext = + TransportContext.builder().setBackendName(backendName).setPriority(priority); + + if (extras != null) { + transportContext.setExtras(Base64.decode(extras, Base64.DEFAULT)); + } + TransportRuntime.getInstance() .getUploader() - .upload( - TransportContext.builder().setBackendName(backendName).setPriority(priority).build(), - attemptNumber, - () -> this.jobFinished(params, false)); + .upload(transportContext.build(), attemptNumber, () -> this.jobFinished(params, false)); return true; } diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/Uploader.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/Uploader.java index 00c73827095..b2b5806c78e 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/Uploader.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/Uploader.java @@ -98,7 +98,7 @@ void logAndUpdateState(TransportContext transportContext, int attemptNumber) { Iterable persistedEvents = guard.runCriticalSection(() -> eventStore.loadBatch(transportContext)); - // Donot make a call to the backend if the list is empty. + // Do not make a call to the backend if the list is empty. if (!persistedEvents.iterator().hasNext()) { return; } @@ -106,7 +106,14 @@ void logAndUpdateState(TransportContext transportContext, int attemptNumber) { for (PersistedEvent persistedEvent : persistedEvents) { eventInternals.add(persistedEvent.getEvent()); } - BackendResponse response = backend.send(BackendRequest.create(eventInternals)); + + BackendResponse response = + backend.send( + BackendRequest.builder() + .setEvents(eventInternals) + .setExtras(transportContext.getExtras()) + .build()); + guard.runCriticalSection( () -> { if (response.getStatus() == BackendResponse.Status.TRANSIENT_ERROR) { diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/DatabaseBootstrapClient.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/DatabaseBootstrapClient.java deleted file mode 100644 index bbbf661e919..00000000000 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/DatabaseBootstrapClient.java +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.android.datatransport.runtime.scheduling.persistence; - -import android.database.sqlite.SQLiteDatabase; -import javax.inject.Inject; -import javax.inject.Named; - -class DatabaseBootstrapClient { - private final String createContextsSql; - private final String createEventsSql; - private final String createEventMetadataSql; - private final String createEventBackendIndex; - private final String createContextBackedPriorityIndex; - - @Inject - DatabaseBootstrapClient( - @Named("CREATE_EVENTS_SQL") String createEventsSql, - @Named("CREATE_EVENT_METADATA_SQL") String createEventMetadataSql, - @Named("CREATE_CONTEXTS_SQL") String createContextsSql, - @Named("CREATE_EVENT_BACKEND_INDEX") String createEventBackendIndex, - @Named("CREATE_CONTEXT_BACKEND_PRIORITY_INDEX") String createContextBackendPriorityIndex) { - this.createEventsSql = createEventsSql; - this.createEventMetadataSql = createEventMetadataSql; - this.createContextsSql = createContextsSql; - this.createEventBackendIndex = createEventBackendIndex; - this.createContextBackedPriorityIndex = createContextBackendPriorityIndex; - } - - void bootstrap(SQLiteDatabase db) { - db.execSQL(createEventsSql); - db.execSQL(createEventMetadataSql); - db.execSQL(createContextsSql); - db.execSQL(createEventBackendIndex); - db.execSQL(createContextBackedPriorityIndex); - } -} diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule.java index 4dfef347862..480247eba92 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/EventStoreModule.java @@ -14,6 +14,8 @@ package com.google.android.datatransport.runtime.scheduling.persistence; +import static com.google.android.datatransport.runtime.scheduling.persistence.SchemaManager.SCHEMA_VERSION; + import com.google.android.datatransport.runtime.synchronization.SynchronizationGuard; import dagger.Binds; import dagger.Module; @@ -22,38 +24,6 @@ @Module public abstract class EventStoreModule { - static final String CREATE_EVENTS_SQL_V1 = - "CREATE TABLE events " - + "(_id INTEGER PRIMARY KEY," - + " context_id INTEGER NOT NULL," - + " transport_name TEXT NOT NULL," - + " timestamp_ms INTEGER NOT NULL," - + " uptime_ms INTEGER NOT NULL," - + " payload BLOB NOT NULL," - + " code INTEGER," - + " num_attempts INTEGER NOT NULL," - + "FOREIGN KEY (context_id) REFERENCES transport_contexts(_id) ON DELETE CASCADE)"; - - static final String CREATE_EVENT_METADATA_SQL_V1 = - "CREATE TABLE event_metadata " - + "(_id INTEGER PRIMARY KEY," - + " event_id INTEGER NOT NULL," - + " name TEXT NOT NULL," - + " value TEXT NOT NULL," - + "FOREIGN KEY (event_id) REFERENCES events(_id) ON DELETE CASCADE)"; - - static final String CREATE_CONTEXTS_SQL_V1 = - "CREATE TABLE transport_contexts " - + "(_id INTEGER PRIMARY KEY," - + " backend_name TEXT NOT NULL," - + " priority INTEGER NOT NULL," - + " next_request_ms INTEGER NOT NULL)"; - - static final String CREATE_EVENT_BACKEND_INDEX_V1 = - "CREATE INDEX events_backend_id on events(context_id)"; - - static final String CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1 = - "CREATE UNIQUE INDEX contexts_backend_priority on transport_contexts(backend_name, priority)"; @Provides static EventStoreConfig storeConfig() { @@ -67,32 +37,8 @@ static EventStoreConfig storeConfig() { abstract SynchronizationGuard synchronizationGuard(SQLiteEventStore store); @Provides - @Named("CREATE_EVENTS_SQL") - static String createEventsSql() { - return CREATE_EVENTS_SQL_V1; - } - - @Provides - @Named("CREATE_EVENT_METADATA_SQL") - static String createEventMetadataSql() { - return CREATE_EVENT_METADATA_SQL_V1; - } - - @Provides - @Named("CREATE_CONTEXTS_SQL") - static String createContextsSql() { - return CREATE_CONTEXTS_SQL_V1; - } - - @Provides - @Named("CREATE_EVENT_BACKEND_INDEX") - static String getCreateEventBackendIndex() { - return CREATE_EVENT_BACKEND_INDEX_V1; - } - - @Provides - @Named("CREATE_CONTEXT_BACKEND_PRIORITY_INDEX") - static String createEventBackendPriorityIndex() { - return CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1; + @Named("SCHEMA_VERSION") + static int schemaVersion() { + return SCHEMA_VERSION; } } diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore.java index 834ce55bf9e..a3f8b9d1642 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStore.java @@ -19,6 +19,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabaseLockedException; import android.os.SystemClock; +import android.util.Base64; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; @@ -30,6 +31,7 @@ import com.google.android.datatransport.runtime.time.Monotonic; import com.google.android.datatransport.runtime.time.WallTime; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -78,7 +80,6 @@ private SQLiteDatabase getDb() { @Override @Nullable public PersistedEvent persist(TransportContext transportContext, EventInternal event) { - long newRowId = inTransaction( db -> { @@ -127,20 +128,33 @@ private long ensureTransportContext(SQLiteDatabase db, TransportContext transpor record.put("backend_name", transportContext.getBackendName()); record.put("priority", transportContext.getPriority().ordinal()); record.put("next_request_ms", 0); + if (transportContext.getExtras() != null) { + record.put("extras", Base64.encodeToString(transportContext.getExtras(), Base64.DEFAULT)); + } + return db.insert("transport_contexts", null, record); } @Nullable private Long getTransportContextId(SQLiteDatabase db, TransportContext transportContext) { + final StringBuilder selection = new StringBuilder("backend_name = ? and priority = ?"); + ArrayList selectionArgs = + new ArrayList<>( + Arrays.asList( + transportContext.getBackendName(), + String.valueOf(transportContext.getPriority().ordinal()))); + + if (transportContext.getExtras() != null) { + selection.append(" and extras = ?"); + selectionArgs.add(Base64.encodeToString(transportContext.getExtras(), Base64.DEFAULT)); + } + return tryWithCursor( db.query( "transport_contexts", new String[] {"_id"}, - "backend_name = ? and priority = ?", - new String[] { - transportContext.getBackendName(), - String.valueOf(transportContext.getPriority().ordinal()) - }, + selection.toString(), + selectionArgs.toArray(new String[0]), null, null, null), diff --git a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/SchemaManager.java b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/SchemaManager.java index c6d4993a63a..2395b5703c2 100644 --- a/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/SchemaManager.java +++ b/transport/transport-runtime/src/main/java/com/google/android/datatransport/runtime/scheduling/persistence/SchemaManager.java @@ -17,20 +17,88 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Build; +import java.util.Arrays; +import java.util.List; import javax.inject.Inject; +import javax.inject.Named; final class SchemaManager extends SQLiteOpenHelper { // TODO: when we do schema upgrades in the future we need to make sure both downgrades and // upgrades work as expected, e.g. `up+down+up` is equivalent to `up`. - private static int SCHEMA_VERSION = 1; private static final String DB_NAME = "com.google.android.datatransport.events"; - private final DatabaseBootstrapClient databaseBootstrapClient; + private final int schemaVersion; private boolean configured = false; + // Schema migration guidelines + // 1. Model migration at Vn as an operation performed on the database at Vn-1. + // 2. Append the migration to the ordered list of Migrations in the static initializer + // 3. Write tests that cover the following scenarios migrating to Vn from V0..Vn-1 + // Note: Migrations handle only upgrades. Downgrades will drop and recreate all tables/indices. + private static final String CREATE_EVENTS_SQL_V1 = + "CREATE TABLE events " + + "(_id INTEGER PRIMARY KEY," + + " context_id INTEGER NOT NULL," + + " transport_name TEXT NOT NULL," + + " timestamp_ms INTEGER NOT NULL," + + " uptime_ms INTEGER NOT NULL," + + " payload BLOB NOT NULL," + + " code INTEGER," + + " num_attempts INTEGER NOT NULL," + + "FOREIGN KEY (context_id) REFERENCES transport_contexts(_id) ON DELETE CASCADE)"; + + private static final String CREATE_EVENT_METADATA_SQL_V1 = + "CREATE TABLE event_metadata " + + "(_id INTEGER PRIMARY KEY," + + " event_id INTEGER NOT NULL," + + " name TEXT NOT NULL," + + " value TEXT NOT NULL," + + "FOREIGN KEY (event_id) REFERENCES events(_id) ON DELETE CASCADE)"; + + private static final String CREATE_CONTEXTS_SQL_V1 = + "CREATE TABLE transport_contexts " + + "(_id INTEGER PRIMARY KEY," + + " backend_name TEXT NOT NULL," + + " priority INTEGER NOT NULL," + + " next_request_ms INTEGER NOT NULL)"; + + private static final String CREATE_EVENT_BACKEND_INDEX_V1 = + "CREATE INDEX events_backend_id on events(context_id)"; + + private static final String CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1 = + "CREATE UNIQUE INDEX contexts_backend_priority on transport_contexts(backend_name, priority)"; + + private static final String DROP_EVENTS_SQL = "DROP TABLE events"; + + private static final String DROP_EVENT_METADATA_SQL = "DROP TABLE event_metadata"; + + private static final String DROP_CONTEXTS_SQL = "DROP TABLE transport_contexts"; + + static int SCHEMA_VERSION = 2; + + private static final SchemaManager.Migration MIGRATE_TO_V1 = + (db) -> { + db.execSQL(CREATE_EVENTS_SQL_V1); + db.execSQL(CREATE_EVENT_METADATA_SQL_V1); + db.execSQL(CREATE_CONTEXTS_SQL_V1); + db.execSQL(CREATE_EVENT_BACKEND_INDEX_V1); + db.execSQL(CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1); + }; + + private static final SchemaManager.Migration MIGRATE_TO_V2 = + (db) -> { + db.execSQL("ALTER TABLE transport_contexts ADD COLUMN extras BLOB"); + db.execSQL( + "CREATE UNIQUE INDEX contexts_backend_priority_extras on transport_contexts(backend_name, priority, extras)"); + db.execSQL("DROP INDEX contexts_backend_priority"); + }; + + private static final List INCREMENTAL_MIGRATIONS = + Arrays.asList(MIGRATE_TO_V1, MIGRATE_TO_V2); + @Inject - SchemaManager(Context context, DatabaseBootstrapClient databaseBootstrapClient) { - super(context, DB_NAME, null, SCHEMA_VERSION); - this.databaseBootstrapClient = databaseBootstrapClient; + SchemaManager(Context context, @Named("SCHEMA_VERSION") int schemaVersion) { + super(context, DB_NAME, null, schemaVersion); + this.schemaVersion = schemaVersion; } @Override @@ -55,21 +123,47 @@ private void ensureConfigured(SQLiteDatabase db) { @Override public void onCreate(SQLiteDatabase db) { ensureConfigured(db); - databaseBootstrapClient.bootstrap(db); + upgrade(db, 0, schemaVersion); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { ensureConfigured(db); + upgrade(db, oldVersion, newVersion); } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - ensureConfigured(db); + db.execSQL(DROP_EVENTS_SQL); + db.execSQL(DROP_EVENT_METADATA_SQL); + db.execSQL(DROP_CONTEXTS_SQL); + // Indices are dropped automatically when the tables are dropped + + onCreate(db); } @Override public void onOpen(SQLiteDatabase db) { ensureConfigured(db); } + + private void upgrade(SQLiteDatabase db, int fromVersion, int toVersion) { + if (toVersion > INCREMENTAL_MIGRATIONS.size()) { + throw new IllegalArgumentException( + "Migration from " + + fromVersion + + " to " + + toVersion + + " was requested, but cannot be performed. Only " + + INCREMENTAL_MIGRATIONS.size() + + " migrations are provided"); + } + for (int version = fromVersion; version < toVersion; version++) { + INCREMENTAL_MIGRATIONS.get(version).upgrade(db); + } + } + + public interface Migration { + void upgrade(SQLiteDatabase db); + } } diff --git a/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerSchedulerTest.java b/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerSchedulerTest.java index fce96ca8ead..4df9d3127d6 100644 --- a/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerSchedulerTest.java +++ b/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/AlarmManagerSchedulerTest.java @@ -28,6 +28,7 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; +import android.util.Base64; import com.google.android.datatransport.Priority; import com.google.android.datatransport.runtime.TransportContext; import com.google.android.datatransport.runtime.scheduling.persistence.EventStore; @@ -65,6 +66,12 @@ private Intent getIntent(TransportContext transportContext) { AlarmManagerScheduler.EVENT_PRIORITY, String.valueOf(transportContext.getPriority().ordinal())); + if (transportContext.getExtras() != null) { + intentDataBuilder.appendQueryParameter( + AlarmManagerScheduler.EXTRAS, + Base64.encodeToString(transportContext.getExtras(), Base64.DEFAULT)); + } + Intent intent = new Intent(context, AlarmManagerSchedulerBroadcastReceiver.class); intent.setData(intentDataBuilder.build()); return intent; @@ -124,6 +131,16 @@ public void schedule_twoJobs() { verify(alarmManager, times(1)).set(eq(AlarmManager.ELAPSED_REALTIME), gt(1000000L), any()); } + @Test + public void schedule_whenExtrasEvailable_transmitsExtras() { + TransportContext transportContext = + TransportContext.builder().setBackendName("backend1").setExtras("e1".getBytes()).build(); + Intent intent = getIntent(transportContext); + assertThat(scheduler.isJobServiceOn(intent)).isFalse(); + scheduler.schedule(transportContext, 1); + assertThat(scheduler.isJobServiceOn(intent)).isTrue(); + } + @Test public void schedule_smallWaitTImeFirstAttempt_multiplePriorities() { Intent intent1 = getIntent(TRANSPORT_CONTEXT); diff --git a/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoSchedulerTest.java b/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoSchedulerTest.java index 3d045b2b862..17c7e087026 100644 --- a/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoSchedulerTest.java +++ b/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/jobscheduling/JobInfoSchedulerTest.java @@ -21,6 +21,7 @@ import android.app.job.JobScheduler; import android.content.Context; import android.os.PersistableBundle; +import android.util.Base64; import com.google.android.datatransport.Priority; import com.google.android.datatransport.runtime.TransportContext; import com.google.android.datatransport.runtime.scheduling.persistence.EventStore; @@ -135,6 +136,19 @@ public void schedule_twoJobs() { .isEqualTo(TRANSPORT_CONTEXT.getBackendName()); } + @Test + public void schedule_whenExtrasEvailable_transmitsExtras() { + String extras = "e1"; + TransportContext transportContext = + TransportContext.builder().setBackendName("backend1").setExtras(extras.getBytes()).build(); + store.recordNextCallTime(transportContext, 1000000); + scheduler.schedule(transportContext, 1); + JobInfo jobInfo = jobScheduler.getAllPendingJobs().get(0); + PersistableBundle bundle = jobInfo.getExtras(); + assertThat(bundle.get(JobInfoScheduler.EXTRAS)) + .isEqualTo(Base64.encodeToString(extras.getBytes(), Base64.DEFAULT)); + } + @Test public void schedule_smallWaitTImeFirstAttempt_multiplePriorities() { store.recordNextCallTime(TRANSPORT_CONTEXT, 5); diff --git a/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStoreTest.java b/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStoreTest.java index 3d4a6a73fbc..1c5454ff8b0 100644 --- a/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStoreTest.java +++ b/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/persistence/SQLiteEventStoreTest.java @@ -14,6 +14,8 @@ package com.google.android.datatransport.runtime.scheduling.persistence; +import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.*; +import static com.google.android.datatransport.runtime.scheduling.persistence.SchemaManager.SCHEMA_VERSION; import static com.google.common.truth.Truth.assertThat; import com.google.android.datatransport.Priority; @@ -57,14 +59,7 @@ private static SQLiteEventStore newStoreWithConfig(Clock clock, EventStoreConfig clock, new UptimeClock(), config, - new SchemaManager( - RuntimeEnvironment.application, - new DatabaseBootstrapClient( - EventStoreModule.CREATE_EVENTS_SQL_V1, - EventStoreModule.CREATE_EVENT_METADATA_SQL_V1, - EventStoreModule.CREATE_CONTEXTS_SQL_V1, - EventStoreModule.CREATE_EVENT_BACKEND_INDEX_V1, - EventStoreModule.CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1))); + new SchemaManager(RuntimeEnvironment.application, SCHEMA_VERSION)); } @Test @@ -78,8 +73,14 @@ public void persist_correctlyRoundTrips() { @Test public void persist_withEventsOfDifferentPriority_shouldEndBeStoredUnderDifferentContexts() { - TransportContext ctx1 = TRANSPORT_CONTEXT; - TransportContext ctx2 = TRANSPORT_CONTEXT.withPriority(Priority.VERY_LOW); + TransportContext ctx1 = + TransportContext.builder().setBackendName("backend1").setExtras("e1".getBytes()).build(); + TransportContext ctx2 = + TransportContext.builder() + .setBackendName("backend1") + .setExtras("e1".getBytes()) + .setPriority(Priority.VERY_LOW) + .build(); EventInternal event1 = EVENT; EventInternal event2 = EVENT.toBuilder().setPayload("World".getBytes()).build(); @@ -91,6 +92,50 @@ public void persist_withEventsOfDifferentPriority_shouldEndBeStoredUnderDifferen assertThat(store.loadBatch(ctx2)).containsExactly(newEvent2); } + @Test + public void persist_withEventsOfDifferentExtras_shouldEndBeStoredUnderDifferentContexts() { + TransportContext ctx1 = + TransportContext.builder().setBackendName("backend1").setExtras("e1".getBytes()).build(); + TransportContext ctx2 = + TransportContext.builder().setBackendName("backend1").setExtras("e2".getBytes()).build(); + + EventInternal event1 = EVENT; + EventInternal event2 = EVENT.toBuilder().setPayload("World".getBytes()).build(); + + PersistedEvent newEvent1 = store.persist(ctx1, event1); + PersistedEvent newEvent2 = store.persist(ctx2, event2); + + assertThat(store.loadBatch(ctx1)).containsExactly(newEvent1); + assertThat(store.loadBatch(ctx2)).containsExactly(newEvent2); + } + + @Test + public void persist_withEventsOfSameExtras_shouldEndBeStoredUnderSameContexts() { + TransportContext ctx1 = + TransportContext.builder().setBackendName("backend1").setExtras("e1".getBytes()).build(); + TransportContext ctx2 = + TransportContext.builder().setBackendName("backend1").setExtras("e1".getBytes()).build(); + + PersistedEvent newEvent1 = store.persist(ctx1, EVENT); + PersistedEvent newEvent2 = store.persist(ctx2, EVENT); + + assertThat(store.loadBatch(ctx2)).containsExactly(newEvent1, newEvent2); + } + + @Test + public void persist_sameBackendswithDifferentExtras_shouldEndBeStoredUnderDifferentContexts() { + TransportContext ctx1 = + TransportContext.builder().setBackendName("backend1").setExtras(null).build(); + TransportContext ctx2 = + TransportContext.builder().setBackendName("backend1").setExtras("e1".getBytes()).build(); + + PersistedEvent newEvent1 = store.persist(ctx1, EVENT); + PersistedEvent newEvent2 = store.persist(ctx2, EVENT); + + assertThat(store.loadBatch(ctx1)).containsExactly(newEvent1); + assertThat(store.loadBatch(ctx2)).containsExactly(newEvent2); + } + @Test public void persist_withEventCode_correctlyRoundTrips() { EventInternal eventWithCode = EVENT.toBuilder().setCode(5).build(); diff --git a/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/persistence/SchemaManagerTest.java b/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/persistence/SchemaManagerTest.java index cb61b9e7ff5..f2a3fa6ff0e 100644 --- a/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/persistence/SchemaManagerTest.java +++ b/transport/transport-runtime/src/test/java/com/google/android/datatransport/runtime/scheduling/persistence/SchemaManagerTest.java @@ -13,17 +13,18 @@ // limitations under the License. package com.google.android.datatransport.runtime.scheduling.persistence; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_CONTEXTS_SQL_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_EVENTS_SQL_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_EVENT_BACKEND_INDEX_V1; -import static com.google.android.datatransport.runtime.scheduling.persistence.EventStoreModule.CREATE_EVENT_METADATA_SQL_V1; +import static com.google.android.datatransport.runtime.scheduling.persistence.SchemaManager.SCHEMA_VERSION; import static com.google.common.truth.Truth.assertThat; +import android.content.ContentValues; +import android.database.sqlite.SQLiteDatabase; +import com.google.android.datatransport.Priority; import com.google.android.datatransport.runtime.EventInternal; import com.google.android.datatransport.runtime.TransportContext; import com.google.android.datatransport.runtime.time.TestClock; import com.google.android.datatransport.runtime.time.UptimeClock; +import java.util.Map; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -33,6 +34,14 @@ public class SchemaManagerTest { private static final TransportContext CONTEXT1 = TransportContext.builder().setBackendName("b1").build(); + + private static final TransportContext CONTEXT2 = + TransportContext.builder() + .setBackendName("b2") + .setExtras("e2".getBytes()) + .build() + .withPriority(Priority.VERY_LOW); + private static final EventInternal EVENT1 = EventInternal.builder() .setTransportName("42") @@ -42,6 +51,7 @@ public class SchemaManagerTest { .addMetadata("key1", "value1") .addMetadata("key2", "value2") .build(); + private static final EventInternal EVENT2 = EVENT1.toBuilder().setPayload("World".getBytes()).build(); @@ -51,18 +61,9 @@ public class SchemaManagerTest { private final TestClock clock = new TestClock(1); - private final DatabaseBootstrapClient V1_BOOTSTRAP_CLIENT = - new DatabaseBootstrapClient( - CREATE_EVENTS_SQL_V1, - CREATE_EVENT_METADATA_SQL_V1, - CREATE_CONTEXTS_SQL_V1, - CREATE_EVENT_BACKEND_INDEX_V1, - CREATE_CONTEXT_BACKEND_PRIORITY_INDEX_V1); - @Test public void persist_correctlyRoundTrips() { - SchemaManager schemaManager = - new SchemaManager(RuntimeEnvironment.application, V1_BOOTSTRAP_CLIENT); + SchemaManager schemaManager = new SchemaManager(RuntimeEnvironment.application, SCHEMA_VERSION); SQLiteEventStore store = new SQLiteEventStore(clock, new UptimeClock(), CONFIG, schemaManager); PersistedEvent newEvent = store.persist(CONTEXT1, EVENT1); @@ -71,4 +72,107 @@ public void persist_correctlyRoundTrips() { assertThat(newEvent.getEvent()).isEqualTo(EVENT1); assertThat(events).containsExactly(newEvent); } + + @Test + public void upgradingV1ToV2_emptyDatabase_allowsPersistsAfterUpgrade() { + int oldVersion = 1; + int newVersion = 2; + SchemaManager schemaManager = new SchemaManager(RuntimeEnvironment.application, oldVersion); + + SQLiteEventStore store = new SQLiteEventStore(clock, new UptimeClock(), CONFIG, schemaManager); + + schemaManager.onUpgrade(schemaManager.getWritableDatabase(), oldVersion, newVersion); + PersistedEvent newEvent1 = store.persist(CONTEXT1, EVENT1); + + assertThat(store.loadBatch(CONTEXT1)).containsExactly(newEvent1); + } + + @Test + public void upgradingV1ToV2_nonEmptyDB_isLossless() { + int oldVersion = 1; + int newVersion = 2; + SchemaManager schemaManager = new SchemaManager(RuntimeEnvironment.application, oldVersion); + SQLiteEventStore store = new SQLiteEventStore(clock, new UptimeClock(), CONFIG, schemaManager); + // We simulate operations as done by an older SQLLiteEventStore at V1 + // We cannot simulate older operations with a newer client + PersistedEvent event1 = simulatedPersistOnV1Database(schemaManager, CONTEXT1, EVENT1); + + // Upgrade to V2 + schemaManager.onUpgrade(schemaManager.getWritableDatabase(), oldVersion, newVersion); + + assertThat(store.loadBatch(CONTEXT1)).containsExactly(event1); + } + + @Test + public void downgradeV2ToV1_withEmptyDB_allowsPersistanceAfterMigration() { + int fromVersion = 2; + SchemaManager schemaManager = new SchemaManager(RuntimeEnvironment.application, fromVersion); + + SQLiteEventStore store = new SQLiteEventStore(clock, new UptimeClock(), CONFIG, schemaManager); + // We simulate operations as done by an older SQLLiteEventStore at V1 + // We cannot simulate older operations with a newer client + simulatedPersistOnV1Database(schemaManager, CONTEXT1, EVENT1); + + schemaManager.onDowngrade(schemaManager.getWritableDatabase(), fromVersion, -1); + PersistedEvent event2 = store.persist(CONTEXT2, EVENT2); + + assertThat(store.loadBatch(CONTEXT2)).containsExactly(event2); + } + + @Test + public void downgradeV2ToV1_withNonEmptyDB_isLossy() { + int fromVersion = 2; + int toVersion = fromVersion - 1; + SchemaManager schemaManager = new SchemaManager(RuntimeEnvironment.application, fromVersion); + SQLiteEventStore store = new SQLiteEventStore(clock, new UptimeClock(), CONFIG, schemaManager); + PersistedEvent event1 = store.persist(CONTEXT1, EVENT1); + + schemaManager.onDowngrade(schemaManager.getWritableDatabase(), toVersion, fromVersion); + + assertThat(store.loadBatch(CONTEXT1)).doesNotContain(event1); + } + + @Test + public void upgrade_toANonExistentVersion_fails() { + int oldVersion = 1; + int nonExistentVersion = 1000; + SchemaManager schemaManager = new SchemaManager(RuntimeEnvironment.application, oldVersion); + + Assert.assertThrows( + IllegalArgumentException.class, + () -> + schemaManager.onUpgrade( + schemaManager.getWritableDatabase(), oldVersion, nonExistentVersion)); + } + + private PersistedEvent simulatedPersistOnV1Database( + SchemaManager schemaManager, TransportContext transportContext, EventInternal eventInternal) { + SQLiteDatabase db = schemaManager.getWritableDatabase(); + + ContentValues record = new ContentValues(); + record.put("backend_name", transportContext.getBackendName()); + record.put("priority", transportContext.getPriority().ordinal()); + record.put("next_request_ms", 0); + long contextId = db.insert("transport_contexts", null, record); + + ContentValues values = new ContentValues(); + values.put("context_id", contextId); + values.put("transport_name", eventInternal.getTransportName()); + values.put("timestamp_ms", eventInternal.getEventMillis()); + values.put("uptime_ms", eventInternal.getUptimeMillis()); + values.put("payload", eventInternal.getPayload()); + values.put("code", eventInternal.getCode()); + values.put("num_attempts", 0); + long newEventId = db.insert("events", null, values); + + for (Map.Entry entry : eventInternal.getMetadata().entrySet()) { + ContentValues metadata = new ContentValues(); + metadata.put("event_id", newEventId); + metadata.put("name", entry.getKey()); + metadata.put("value", entry.getValue()); + db.insert("event_metadata", null, metadata); + } + + return PersistedEvent.create(newEventId, transportContext, eventInternal); + } }