diff --git a/app/build.gradle b/app/build.gradle index e2ee373..61f13b3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -122,8 +122,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7' - implementation 'androidx.navigation:navigation-ui-ktx:2.7.7' + implementation 'androidx.navigation:navigation-fragment-ktx:2.8.0' + implementation 'androidx.navigation:navigation-ui-ktx:2.8.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.concurrent:concurrent-futures:1.2.0' diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/Constants.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/Constants.kt index e5f2068..3665b43 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/Constants.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/Constants.kt @@ -1,6 +1,12 @@ package eu.pkgsoftware.babybuddywidgets +import java.text.SimpleDateFormat +import java.util.Locale + object Constants { + @JvmField + val SERVER_DATE_FORMAT = SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.ENGLISH) + enum class FeedingTypeEnum(@JvmField var value: Int, @JvmField var post_name: String) { BREAST_MILK(0, "breast milk"), FORMULA(1, "formula"), diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java index fff00c2..05c92eb 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java @@ -12,7 +12,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; -import java.lang.reflect.Array; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; @@ -30,7 +29,6 @@ import java.util.Objects; import java.util.Random; import java.util.TimeZone; -import java.util.Timer; import androidx.annotation.NonNull; import eu.pkgsoftware.babybuddywidgets.Constants; @@ -368,107 +366,6 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(type, typeId, start, end, notes); } - - public String getUserPath() { - switch (this.type) { - case ACTIVITIES.FEEDING: - return "/feedings/" + this.typeId + "/"; - case EVENTS.CHANGE: - return "/changes/" + this.typeId + "/"; - case ACTIVITIES.TUMMY_TIME: - return "/tummy-time/" + this.typeId + "/"; - case ACTIVITIES.SLEEP: - return "/sleep/" + this.typeId + "/"; - default: - System.err.println("WARNING! getUserPath not implemented for type: " + this.type); - } - return null; - } - - public String getApiPath() { - return "/api/" + this.type + "/" + this.typeId + "/"; - } - } - - public static class ChangeEntry extends TimeEntry { - public boolean wet; - public boolean solid; - - public ChangeEntry(String type, int typeId, Date start, Date end, String notes, boolean wet, boolean solid) { - super(type, typeId, start, end, notes); - this.wet = wet; - this.solid = solid; - } - - @Override - public String toString() { - return "ChangeEntry{" + - "type='" + type + '\'' + - ", start=" + start + - ", end=" + end + - ", notes='" + notes + '\'' + - ", wet=" + wet + - ", solid=" + solid + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - ChangeEntry that = (ChangeEntry) o; - return wet == that.wet && solid == that.solid; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), wet, solid); - } - } - - public static class FeedingEntry extends TimeEntry { - public Constants.FeedingMethodEnum feedingMethod; - public Constants.FeedingTypeEnum feedingType; - - public FeedingEntry( - String type, - int typeId, - Date start, - Date end, - String notes, - Constants.FeedingMethodEnum feedingMethod, - Constants.FeedingTypeEnum feedingType) { - super(type, typeId, start, end, notes); - this.feedingMethod = feedingMethod; - this.feedingType = feedingType; - } - - @Override - public String toString() { - return "FeedingEntry{" + - "type='" + type + '\'' + - ", start=" + start + - ", end=" + end + - ", notes='" + notes + '\'' + - ", feedingMethod=" + feedingMethod + - ", feedingType=" + feedingType + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - FeedingEntry that = (FeedingEntry) o; - return feedingMethod == that.feedingMethod && feedingType == that.feedingType; - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), feedingMethod, feedingType); - } } public class GenericSubsetResponseHeader { @@ -501,9 +398,6 @@ public interface RequestCallback { void response(R response); } - private final SimpleDateFormat SERVER_DATE_FORMAT = new SimpleDateFormat( - "EEE, d MMM yyyy HH:mm:ss z", Locale.ENGLISH - ); private Handler syncMessage; private CredStore credStore; @@ -519,7 +413,7 @@ private void updateServerDateTime(HttpURLConnection con) { return; // Chicken out, no dateString found, let's hope everything works! } try { - Date serverTime = SERVER_DATE_FORMAT.parse(dateString); + Date serverTime = Constants.SERVER_DATE_FORMAT.parse(dateString); Date now = new Date(System.currentTimeMillis()); serverDateOffset = serverTime.getTime() - now.getTime() - 100; // 100 ms offset @@ -799,150 +693,6 @@ public long getServerDateOffsetMillis() { return serverDateOffset; } - public void getTimer(int timer_id, RequestCallback callback) { - dispatchQuery( - "GET", - "api/timers/" + timer_id + "/", - null, - new RequestCallback() { - @Override - public void error(@NonNull Exception e) { - callback.error(e); - } - - @Override - public void response(String response) { - JSONObject obj = null; - try { - obj = new JSONObject(response); - callback.response(Timer.fromJSON(obj)); - } catch (JSONException | ParseException e) { - error(e); - } - } - }); - } - - public void createSleepRecordFromTimer(Timer timer, String notes, RequestCallback callback) { - String data; - try { - data = (new JSONObject()) - .put("timer", timer.id) - .put("notes", notes) - .toString(); - } catch (JSONException e) { - throw new RuntimeException("JSON Structure not built correctly"); - } - - dispatchQuery( - "POST", - "api/sleep/", - data, - new RequestCallback() { - @Override - public void error(@NonNull Exception e) { - callback.error(e); - } - - @Override - public void response(String response) { - callback.response(true); - } - }); - } - - public void createTummyTimeRecordFromTimer(Timer timer, String milestone, RequestCallback callback) { - String data; - try { - data = (new JSONObject()) - .put("timer", timer.id) - .put("milestone", milestone) - .toString(); - } catch (JSONException e) { - throw new RuntimeException("JSON Structure not built correctly"); - } - - dispatchQuery( - "POST", - "api/tummy-times/", - data, - new RequestCallback() { - @Override - public void error(@NonNull Exception e) { - callback.error(e); - } - - @Override - public void response(String response) { - callback.response(true); - } - }); - } - - public void createFeedingRecordFromTimer(Timer timer, String type, String method, Float amount, String notes, RequestCallback callback) { - String data; - try { - data = (new JSONObject()) - .put("timer", timer.id) - .put("type", type) - .put("method", method) - .put("amount", amount) - .put("notes", notes) - .toString(); - } catch (JSONException e) { - throw new RuntimeException("JSON Structure not built correctly"); - } - - dispatchQuery( - "POST", - "api/feedings/", - data, - new RequestCallback() { - @Override - public void error(@NonNull Exception e) { - callback.error(e); - } - - @Override - public void response(String response) { - callback.response(true); - } - }); - } - - public void createChangeRecord(Child child, boolean wet, boolean solid, String notes, RequestCallback callback) { - String data; - try { - data = (new JSONObject()) - .put("child", child.id) - .put("time", now()) - .put("wet", wet) - .put("solid", solid) - .put("color", "") - .put("amount", null) - .put("notes", notes) - .toString(); - } catch (JSONException e) { - throw new RuntimeException("JSON Structure not built correctly"); - } - - dispatchQuery( - "POST", - "api/changes/", - data, - new RequestCallback() { - @Override - public void error(@NonNull Exception e) { - callback.error(e); - } - - @Override - public void response(String response) { - callback.response(true); - } - }); - } - @NonNull private static String addQueryParameters(QueryValues queryValues, @NonNull String path) { if (queryValues == null) { @@ -1006,185 +756,6 @@ public void response(String response) { }); } - private interface WrapTimelineEntry { - TE wrap(JSONObject json) throws ParseException, JSONException; - } - - private class GenericTimelineRequest { - private final Class runtimeClass; - - public GenericTimelineRequest(Class cls) { - runtimeClass = cls; - } - - private TE[] emptyArray() { - return (TE[]) Array.newInstance(runtimeClass, 0); - } - - private void genericTimelineRequest( - String target, - int child_id, - int offset, - int count, - RequestCallback> callback, - WrapTimelineEntry wrapper - ) { - - listGeneric( - target, - offset, - new QueryValues() - .add("child", child_id) - .add("limit", count), - new RequestCallback>() { - @Override - public void error(@NotNull Exception error) { - callback.error(error); - } - - @Override - public void response(GenericSubsetResponseHeader response) { - List result = new ArrayList<>(); - try { - for (int i = 0; i < response.payload.length(); i++) { - result.add(wrapper.wrap(response.payload.getJSONObject(i))); - } - } catch (JSONException | ParseException e) { - error(e); - return; - } - - callback.response( - new GenericListSubsetResponse( - offset, - response.totalCount, - result.toArray(emptyArray()) - ) - ); - } - } - ); - } - } - - public void listSleepEntries(int child_id, int offset, int count, RequestCallback> callback) { - new GenericTimelineRequest(TimeEntry.class).genericTimelineRequest( - ACTIVITIES.SLEEP, - child_id, - offset, - count, - callback, - o -> { - String notes = o.optString("notes"); - return new TimeEntry( - ACTIVITIES.SLEEP, - o.getInt("id"), - parseNullOrDate(o, "start"), - parseNullOrDate(o, "end"), - notes - ); - } - ); - } - - public void listFeedingsEntries(int child_id, int offset, int count, RequestCallback> callback) { - new GenericTimelineRequest(FeedingEntry.class).genericTimelineRequest( - ACTIVITIES.FEEDING, - child_id, - offset, - count, - callback, - o -> { - String notes = o.optString("notes"); - - Constants.FeedingMethodEnum feedingMethod = null; - Constants.FeedingTypeEnum feedingType = null; - - for (Constants.FeedingMethodEnum m : Constants.FeedingMethodEnum.values()) { - if (m.post_name.equals(o.getString("method"))) { - feedingMethod = m; - } - } - for (Constants.FeedingTypeEnum t : Constants.FeedingTypeEnum.values()) { - if (t.post_name.equals(o.getString("type"))) { - feedingType = t; - } - } - - return new FeedingEntry( - ACTIVITIES.FEEDING, - o.getInt("id"), - parseNullOrDate(o, "start"), - parseNullOrDate(o, "end"), - notes, - feedingMethod, - feedingType - ); - } - ); - } - - public void listTummyTimeEntries(int child_id, int offset, int count, RequestCallback> callback) { - new GenericTimelineRequest(TimeEntry.class).genericTimelineRequest( - ACTIVITIES.TUMMY_TIME, - child_id, - offset, - count, - callback, - o -> { - String notes = o.optString("milestone"); - return new TimeEntry( - ACTIVITIES.TUMMY_TIME, - o.getInt("id"), - parseNullOrDate(o, "start"), - parseNullOrDate(o, "end"), - notes - ); - } - ); - } - - public void listChangeEntries(int child_id, int offset, int count, RequestCallback> callback) { - new GenericTimelineRequest(ChangeEntry.class).genericTimelineRequest( - EVENTS.CHANGE, - child_id, - offset, - count, - callback, - o -> { - String notes = o.optString("notes"); - return new ChangeEntry( - EVENTS.CHANGE, - o.getInt("id"), - parseNullOrDate(o, "time"), - parseNullOrDate(o, "time"), - notes, - o.getBoolean("wet"), - o.getBoolean("solid") - ); - } - ); - } - - public void removeTimelineEntry(TimeEntry entry, RequestCallback callback) { - dispatchQuery( - "DELETE", - entry.getApiPath(), - null, - new RequestCallback() { - @Override - public void error(@NotNull Exception error) { - callback.error(error); - } - - @Override - public void response(String response) { - callback.response(true); - } - } - ); - } - public void updateTimelineEntry( @NotNull TimeEntry entry, @NotNull QueryValues values, diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/CoordinatedDisconnectDialog.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/CoordinatedDisconnectDialog.kt index 46fa1fb..8d8ef55 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/CoordinatedDisconnectDialog.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/CoordinatedDisconnectDialog.kt @@ -35,6 +35,8 @@ class CoordinatedDisconnectDialog(val fragment: BaseFragment, val credStore: Cre return progressTrackers.values.max() } + var timeoutGracePeriod = 1000L + private inner class ConnectingDialogInterfaceImpl : ConnectingDialogInterface { val key = "interface-${uniqueCounter}" @@ -62,7 +64,7 @@ class CoordinatedDisconnectDialog(val fragment: BaseFragment, val credStore: Cre } private fun updateDialog() { - if (timeout > 0) { + if (timeout > timeoutGracePeriod) { dialog.show() } else { dialog.hide() diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/Client.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/Client.kt index 5ad75ff..28c4ae4 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/Client.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/Client.kt @@ -20,7 +20,6 @@ import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response import okio.Buffer -import okio.BufferedSink import retrofit2.Call import retrofit2.Retrofit import retrofit2.converter.jackson.JacksonConverterFactory @@ -36,7 +35,6 @@ import kotlin.reflect.full.createType import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.findParameterByName import kotlin.reflect.full.functions -import kotlin.reflect.full.valueParameters import kotlin.reflect.jvm.javaMethod fun genRequestId(): String { @@ -81,6 +79,16 @@ class DebugNetworkInterceptor : Interceptor { } } +class ServerTimeOffsetInterceptor(val tracker: ServerTimeOffsetTracker) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(chain.request()) + response.header("Date")?.let { serverTime -> + tracker.updateServerTime(serverTime) + } + return response + } +} + class InvalidBody() : Exception("Invalid body") data class PaginatedResult ( @@ -91,6 +99,7 @@ data class PaginatedResult ( class Client(val credStore: ServerAccessProviderInterface) { val httpClient = OkHttpClient.Builder() + .addInterceptor(ServerTimeOffsetInterceptor(SystemServerTimeOffsetTracker)) .addInterceptor(AuthInterceptor("Token " + credStore.appToken, credStore.authCookies)) .addInterceptor(DebugNetworkInterceptor()) .build() diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/DateHandling.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/DateHandling.kt index 49100dd..ea1940f 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/DateHandling.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/DateHandling.kt @@ -5,7 +5,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -var SystemServerTimeOffset = -1000L +var SystemServerTimeOffsetTracker = ServerTimeOffsetTracker() val DATE_TIME_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ssX" val DATE_ONLY_FORMAT_STRING = "yyyy-MM-dd" @@ -32,13 +32,17 @@ fun formatDate(d: Date, format: String): String { } fun clientToServerTime(d: Date): Date { - return Date(d.time + SystemServerTimeOffset) + return Date(SystemServerTimeOffsetTracker.localToSafeServerTime(d.time)) } fun serverTimeToClientTime(d: Date): Date { - return Date(d.time - SystemServerTimeOffset) + return Date(SystemServerTimeOffsetTracker.serverToLocalTime(d.time)) } fun nowServer(): Date { return clientToServerTime(Date()) } + +fun maxDate(d1: Date, d2: Date): Date { + return if (d1.after(d2)) d1 else d2 +} diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/RetryLogic.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/RetryLogic.kt index b229e82..e64fbc1 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/RetryLogic.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/RetryLogic.kt @@ -16,10 +16,15 @@ interface ConnectingDialogInterface { class InterruptedException : Exception("Exponential backoff interrupted") -suspend fun exponentialBackoff(conInterface: ConnectingDialogInterface, block: suspend () -> T): T { +suspend fun exponentialBackoff( + conInterface: ConnectingDialogInterface, + forceRetry400: Int = 0, + block: suspend () -> T, +): T { val totalWaitTimeStart = System.currentTimeMillis() var currentRetryDelay = INITIAL_RETRY_INTERVAL var showingConnecting = false + var forceRetry400Counter = 0 try { while (true) { var error: Exception? = null @@ -27,8 +32,13 @@ suspend fun exponentialBackoff(conInterface: ConnectingDialogInterface return block.invoke() } catch (e: RequestCodeFailure) { + println("XXX ${forceRetry400Counter} < ${forceRetry400}") if ((e.code >= 400) and (e.code < 500)) { - throw e + if (forceRetry400Counter < forceRetry400) { + forceRetry400Counter++ + } else { + throw e + } } error = e } diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/ServerTimeOffsetTracker.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/ServerTimeOffsetTracker.kt new file mode 100644 index 0000000..3fd8211 --- /dev/null +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/ServerTimeOffsetTracker.kt @@ -0,0 +1,59 @@ +package eu.pkgsoftware.babybuddywidgets.networking.babybuddy + +import eu.pkgsoftware.babybuddywidgets.Constants + +val MAX_OFFSETS = 20 + +open class ServerTimeOffsetTracker(initialOffsets: Sequence = sequenceOf()) { + private var _offsets = initialOffsets.toMutableList() + + val offsets: List get() = _offsets.toList() + val measuredOffset: Long get() = _offsets.let { + if (it.size < 3) { + -1000 + } else { + val sortedOffsets = it.sorted() + val p50 = sortedOffsets[it.size / 2] + val p20 = sortedOffsets[it.size * 2 / 10] + return p50 + (p20 - p50) * 5 / 3 + } + } + + protected open fun currentTimeMillis(): Long { + return System.currentTimeMillis() + } + + fun addOffsets(offsets: Sequence) { + _offsets.addAll(offsets) + while (_offsets.size > eu.pkgsoftware.babybuddywidgets.networking.babybuddy.MAX_OFFSETS) { + _offsets.removeAt(0) + } + } + + fun updateServerTime(dateHeader: String) { + val date = Constants.SERVER_DATE_FORMAT.parse(dateHeader) + val serverMillis = date?.time ?: return + + // Note: System.currentTimeMillis() includes the connection latency, but including it + // makes the offset larger, which does not hurt the reason for the offset. If latency is + // high, we just assume that the server time is earlier by the same amount. In the worst + // case, we log entries as being earlier than they actually are, as dictated by the current + // connection latency. What we cannot have is a time arriving at the server that is later + // than local server time. + val newOffset = serverMillis - currentTimeMillis() + addOffsets(sequenceOf(newOffset)) + } + + fun localToSafeServerTime(millis: Long): Long { + val mOffset = measuredOffset + val nowOffset = millis - currentTimeMillis() + if (nowOffset < mOffset - 1000) { + return millis + } + return millis + mOffset - 1000 + } + + fun serverToLocalTime(millis: Long): Long { + return millis - measuredOffset + } +} diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControllersV2.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControllersV2.kt index 1823a78..b05a6bc 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControllersV2.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControllersV2.kt @@ -32,6 +32,8 @@ import eu.pkgsoftware.babybuddywidgets.login.Utils import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient import eu.pkgsoftware.babybuddywidgets.networking.BabyBuddyClient.Timer import eu.pkgsoftware.babybuddywidgets.networking.RequestCodeFailure +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.exponentialBackoff +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.maxDate import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.ChangeEntry import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.FeedingEntry import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.NoteEntry @@ -46,7 +48,6 @@ import eu.pkgsoftware.babybuddywidgets.utils.AsyncPromiseFailure import eu.pkgsoftware.babybuddywidgets.utils.ConcurrentEventBlocker import eu.pkgsoftware.babybuddywidgets.utils.Promise import eu.pkgsoftware.babybuddywidgets.widgets.HorizontalDecIncEditor -import eu.pkgsoftware.babybuddywidgets.widgets.HorizontalNumberPicker import eu.pkgsoftware.babybuddywidgets.widgets.SwitchButtonLogic import kotlinx.coroutines.Runnable import kotlinx.coroutines.launch @@ -409,7 +410,7 @@ class SleepLoggingController( id = 0, childId = childId, start = timer.start, - end = nowServer(), + end = maxDate(timer.start, nowServer()), _notes = bindings.noteEditor.text.toString() ) ) @@ -428,7 +429,7 @@ class TummyTimeLoggingController( id = 0, childId = childId, start = timer.start, - end = nowServer(), + end = maxDate(timer.start, nowServer()), _notes = bindings.noteEditor.text.toString() ) ) @@ -594,7 +595,7 @@ class FeedingLoggingController( id = 0, childId = childId, start = timer.start, - end = nowServer(), + end = maxDate(timer.start, nowServer()), feedingType = selectedType!!, feedingMethod = selectedMethod!!, amount = feedingBinding.amountNumberPicker.value?.toDouble(), @@ -752,7 +753,7 @@ class PumpingLoggingController( id = 0, childId = childId, _start = timer.start, - _end = nowServer(), + _end = maxDate(timer.start, nowServer()), amount = amountNumberPicker.value!!.toDouble(), _notes = uiNoteEditor.text.toString(), _legacyTime = timer.start @@ -965,11 +966,16 @@ class LoggingButtonController( timerModificationsBlocker.wait() timerModificationsBlocker.register { try { - logicMap[activity]?.state = false - val te = controller.save() - controller.reset() - storeStateForSuspend() - controlsInterface.updateTimeline(te) + // Note: exponentialBackoff does not work right now because controller.save() + // calls MainActivity.storeActivity() which does not throw exceptions but + // uses a callback to signal success or failure. + exponentialBackoff(fragment.disconnectDialog.getInterface(), forceRetry400 = 5) { + logicMap[activity]?.state = false + val te = controller.save() + controller.reset() + storeStateForSuspend() + controlsInterface.updateTimeline(te) + } } catch (e: RequestCodeFailure) { fragment.showError( diff --git a/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ClientV2IntegrationTest.kt b/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ClientV2IntegrationTest.kt index ea96334..440b2c2 100644 --- a/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ClientV2IntegrationTest.kt +++ b/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ClientV2IntegrationTest.kt @@ -15,6 +15,7 @@ import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.SleepEntry import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.TemperatureEntry import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.TummyTimeEntry import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.WeightEntry +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Assert import org.junit.Test @@ -132,6 +133,7 @@ class ClientV2IntegrationTest { Assert.assertTrue(headCircEntires.entries.size > 0) } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun testObtainLists() = runTest { for (server in serverUrlArray) { diff --git a/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ServerTimeOffsetCorrectionTest.kt b/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ServerTimeOffsetCorrectionTest.kt new file mode 100644 index 0000000..e4ae9ab --- /dev/null +++ b/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ServerTimeOffsetCorrectionTest.kt @@ -0,0 +1,66 @@ +package eu.pkgsoftware.babybuddywidgets + +import org.junit.Assert +import org.junit.Test +import java.util.Date + +class TestableServerTimeOffsetTracker : eu.pkgsoftware.babybuddywidgets.networking.babybuddy.ServerTimeOffsetTracker() { + var testTime = 0L + + override fun currentTimeMillis(): Long { + return testTime + } +} + +class ServerTimeOffsetCorrectionTest { + fun headerFromMillis(now: Long): String { + return Constants.SERVER_DATE_FORMAT.format(Date(now)) + } + + @Test + fun serverLaggingBehindClientUniform() { + val tracker = TestableServerTimeOffsetTracker() + tracker.updateServerTime(headerFromMillis(-1000)) + tracker.updateServerTime(headerFromMillis(-1000)) + tracker.updateServerTime(headerFromMillis(-1000)) + tracker.updateServerTime(headerFromMillis(-1000)) + tracker.updateServerTime(headerFromMillis(-1000)) + tracker.updateServerTime(headerFromMillis(-1000)) + tracker.updateServerTime(headerFromMillis(-1000)) + tracker.updateServerTime(headerFromMillis(-1000)) + tracker.updateServerTime(headerFromMillis(-1000)) + tracker.updateServerTime(headerFromMillis(-2000)) + + Assert.assertEquals(-1000, tracker.measuredOffset) + Assert.assertEquals(8000, tracker.localToSafeServerTime(10000)) + } + + @Test + fun serverAheadOfClientUniform() { + val tracker = TestableServerTimeOffsetTracker() + tracker.updateServerTime(headerFromMillis(1000)) + tracker.updateServerTime(headerFromMillis(1000)) + tracker.updateServerTime(headerFromMillis(1000)) + tracker.updateServerTime(headerFromMillis(1000)) + tracker.updateServerTime(headerFromMillis(1000)) + tracker.updateServerTime(headerFromMillis(1000)) + tracker.updateServerTime(headerFromMillis(1000)) + tracker.updateServerTime(headerFromMillis(1000)) + tracker.updateServerTime(headerFromMillis(1000)) + tracker.updateServerTime(headerFromMillis(2000)) + + Assert.assertEquals(1000, tracker.measuredOffset) + Assert.assertEquals(10000, tracker.localToSafeServerTime(10000)) + } + + @Test + fun serverLaggingBehindRamp() { + val tracker = TestableServerTimeOffsetTracker() + for (i in 0 .. 9) { + tracker.updateServerTime(headerFromMillis(-5000L + i * 1000)) + } + + Assert.assertEquals(-5000, tracker.measuredOffset) + Assert.assertEquals(4000, tracker.localToSafeServerTime(10000)) + } +} \ No newline at end of file