From 5a9da3ca0cdd9d9321fe2b5257d1a7f831bc6d21 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Fri, 6 Sep 2024 01:09:13 +0200 Subject: [PATCH 1/4] Remove old endpoints, update libraries, fix warnings --- app/build.gradle | 4 +- .../networking/BabyBuddyClient.java | 420 ------------------ .../ClientV2IntegrationTest.kt | 2 + 3 files changed, 4 insertions(+), 422 deletions(-) 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/networking/BabyBuddyClient.java b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java index fff00c2..67316ec 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/BabyBuddyClient.java @@ -368,107 +368,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 { @@ -799,150 +698,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) { @@ -1010,181 +765,6 @@ 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/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) { From 80b2a17ab709982c6a34f02965d815b29f98dfef Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Mon, 23 Sep 2024 00:41:43 +0200 Subject: [PATCH 2/4] Add fancy statistical server-time offset tracking --- .../networking/ServerTimeOffsetTracker.kt | 55 +++++++++++++++ .../ServerTimeOffsetCorrectionTest.kt | 67 +++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ServerTimeOffsetTracker.kt create mode 100644 app/src/test/java/eu/pkgsoftware/babybuddywidgets/ServerTimeOffsetCorrectionTest.kt diff --git a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ServerTimeOffsetTracker.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ServerTimeOffsetTracker.kt new file mode 100644 index 0000000..e57df44 --- /dev/null +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ServerTimeOffsetTracker.kt @@ -0,0 +1,55 @@ +package eu.pkgsoftware.babybuddywidgets.networking + +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 > 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 localToServerTime(millis: Long): Long { + val mOffset = measuredOffset + val nowOffset = millis - currentTimeMillis() + if (nowOffset < mOffset - 1000) { + return millis + } + return millis + mOffset - 1000 + } +} 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..ae65a64 --- /dev/null +++ b/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ServerTimeOffsetCorrectionTest.kt @@ -0,0 +1,67 @@ +package eu.pkgsoftware.babybuddywidgets + +import eu.pkgsoftware.babybuddywidgets.networking.ServerTimeOffsetTracker +import org.junit.Assert +import org.junit.Test +import java.util.Date + +class TestableServerTimeOffsetTracker : 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.localToServerTime(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.localToServerTime(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.localToServerTime(10000)) + } +} \ No newline at end of file From a2ea70f19d9ec4a24c89063d5e174243635279c5 Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Thu, 26 Sep 2024 11:41:00 +0200 Subject: [PATCH 3/4] Make new API implementation use server time offset tracking --- .../pkgsoftware/babybuddywidgets/Constants.kt | 6 ++ .../networking/BabyBuddyClient.java | 11 +-- .../networking/CoordinatedDisconnectDialog.kt | 4 +- .../networking/babybuddy/Client.kt | 13 +++- .../networking/babybuddy/DateHandling.kt | 10 ++- .../ServerTimeOffsetTracker.kt | 10 ++- .../timers/TimerControllersV2.kt | 70 ++++++++++--------- .../ServerTimeOffsetCorrectionTest.kt | 9 ++- 8 files changed, 77 insertions(+), 56 deletions(-) rename app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/{ => babybuddy}/ServerTimeOffsetTracker.kt (85%) 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 67316ec..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; @@ -400,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; @@ -418,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 @@ -761,10 +756,6 @@ public void response(String response) { }); } - private interface WrapTimelineEntry { - TE wrap(JSONObject json) throws ParseException, JSONException; - } - 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/ServerTimeOffsetTracker.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/ServerTimeOffsetTracker.kt similarity index 85% rename from app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ServerTimeOffsetTracker.kt rename to app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/ServerTimeOffsetTracker.kt index e57df44..3fd8211 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/ServerTimeOffsetTracker.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/networking/babybuddy/ServerTimeOffsetTracker.kt @@ -1,4 +1,4 @@ -package eu.pkgsoftware.babybuddywidgets.networking +package eu.pkgsoftware.babybuddywidgets.networking.babybuddy import eu.pkgsoftware.babybuddywidgets.Constants @@ -25,7 +25,7 @@ open class ServerTimeOffsetTracker(initialOffsets: Sequence = sequenceOf() fun addOffsets(offsets: Sequence) { _offsets.addAll(offsets) - while (_offsets.size > MAX_OFFSETS) { + while (_offsets.size > eu.pkgsoftware.babybuddywidgets.networking.babybuddy.MAX_OFFSETS) { _offsets.removeAt(0) } } @@ -44,7 +44,7 @@ open class ServerTimeOffsetTracker(initialOffsets: Sequence = sequenceOf() addOffsets(sequenceOf(newOffset)) } - fun localToServerTime(millis: Long): Long { + fun localToSafeServerTime(millis: Long): Long { val mOffset = measuredOffset val nowOffset = millis - currentTimeMillis() if (nowOffset < mOffset - 1000) { @@ -52,4 +52,8 @@ open class ServerTimeOffsetTracker(initialOffsets: Sequence = sequenceOf() } 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..ffd98fe 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,10 @@ 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.minData +import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.minDate import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.ChangeEntry import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.FeedingEntry import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.NoteEntry @@ -409,7 +413,7 @@ class SleepLoggingController( id = 0, childId = childId, start = timer.start, - end = nowServer(), + end = maxDate(timer.start, nowServer()), _notes = bindings.noteEditor.text.toString() ) ) @@ -428,7 +432,7 @@ class TummyTimeLoggingController( id = 0, childId = childId, start = timer.start, - end = nowServer(), + end = maxDate(timer.start, nowServer()), _notes = bindings.noteEditor.text.toString() ) ) @@ -594,7 +598,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 +756,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 @@ -964,36 +968,38 @@ class LoggingButtonController( suspend fun runSave(activity: String, controller: LoggingControls) { timerModificationsBlocker.wait() timerModificationsBlocker.register { - try { - logicMap[activity]?.state = false - val te = controller.save() - controller.reset() - storeStateForSuspend() - controlsInterface.updateTimeline(te) - } - catch (e: RequestCodeFailure) { - fragment.showError( - true, - R.string.activity_store_failure_message, - Phrase.from( - fragment.requireContext(), - R.string.activity_store_failure_server_error - ) - .put( - "message", - fragment.getString(R.string.activity_store_failure_server_error_general) + exponentialBackoff(fragment.disconnectDialog.getInterface()) { + try { + logicMap[activity]?.state = false + val te = controller.save() + controller.reset() + storeStateForSuspend() + controlsInterface.updateTimeline(te) + } + catch (e: RequestCodeFailure) { + fragment.showError( + true, + R.string.activity_store_failure_message, + Phrase.from( + fragment.requireContext(), + R.string.activity_store_failure_server_error ) - .put("server_message", e.jsonErrorMessages().joinToString(", ")) - .format().toString() + .put( + "message", + fragment.getString(R.string.activity_store_failure_server_error_general) + ) + .put("server_message", e.jsonErrorMessages().joinToString(", ")) + .format().toString() - ) - } - catch (e: IOException) { - fragment.showError( - true, - R.string.activity_store_failure_message, - R.string.activity_store_failure_server_error_generic_ioerror - ) + ) + } + catch (e: IOException) { + fragment.showError( + true, + R.string.activity_store_failure_message, + R.string.activity_store_failure_server_error_generic_ioerror + ) + } } } } diff --git a/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ServerTimeOffsetCorrectionTest.kt b/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ServerTimeOffsetCorrectionTest.kt index ae65a64..e4ae9ab 100644 --- a/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ServerTimeOffsetCorrectionTest.kt +++ b/app/src/test/java/eu/pkgsoftware/babybuddywidgets/ServerTimeOffsetCorrectionTest.kt @@ -1,11 +1,10 @@ package eu.pkgsoftware.babybuddywidgets -import eu.pkgsoftware.babybuddywidgets.networking.ServerTimeOffsetTracker import org.junit.Assert import org.junit.Test import java.util.Date -class TestableServerTimeOffsetTracker : ServerTimeOffsetTracker() { +class TestableServerTimeOffsetTracker : eu.pkgsoftware.babybuddywidgets.networking.babybuddy.ServerTimeOffsetTracker() { var testTime = 0L override fun currentTimeMillis(): Long { @@ -33,7 +32,7 @@ class ServerTimeOffsetCorrectionTest { tracker.updateServerTime(headerFromMillis(-2000)) Assert.assertEquals(-1000, tracker.measuredOffset) - Assert.assertEquals(8000, tracker.localToServerTime(10000)) + Assert.assertEquals(8000, tracker.localToSafeServerTime(10000)) } @Test @@ -51,7 +50,7 @@ class ServerTimeOffsetCorrectionTest { tracker.updateServerTime(headerFromMillis(2000)) Assert.assertEquals(1000, tracker.measuredOffset) - Assert.assertEquals(10000, tracker.localToServerTime(10000)) + Assert.assertEquals(10000, tracker.localToSafeServerTime(10000)) } @Test @@ -62,6 +61,6 @@ class ServerTimeOffsetCorrectionTest { } Assert.assertEquals(-5000, tracker.measuredOffset) - Assert.assertEquals(4000, tracker.localToServerTime(10000)) + Assert.assertEquals(4000, tracker.localToSafeServerTime(10000)) } } \ No newline at end of file From 69e4c83aa51b69bdefd92413ab23243a210b355a Mon Sep 17 00:00:00 2001 From: Paul Konstantin Gerke Date: Thu, 26 Sep 2024 12:12:13 +0200 Subject: [PATCH 4/4] Implement exponentialBackoff in runSave - defunct for other reasons right now --- .../networking/babybuddy/RetryLogic.kt | 14 ++++- .../timers/TimerControllersV2.kt | 54 +++++++++---------- 2 files changed, 39 insertions(+), 29 deletions(-) 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/timers/TimerControllersV2.kt b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControllersV2.kt index ffd98fe..b05a6bc 100644 --- a/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControllersV2.kt +++ b/app/src/main/java/eu/pkgsoftware/babybuddywidgets/timers/TimerControllersV2.kt @@ -34,8 +34,6 @@ 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.minData -import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.minDate import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.ChangeEntry import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.FeedingEntry import eu.pkgsoftware.babybuddywidgets.networking.babybuddy.models.NoteEntry @@ -50,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 @@ -968,38 +965,41 @@ class LoggingButtonController( suspend fun runSave(activity: String, controller: LoggingControls) { timerModificationsBlocker.wait() timerModificationsBlocker.register { - exponentialBackoff(fragment.disconnectDialog.getInterface()) { - try { + try { + // 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( - true, - R.string.activity_store_failure_message, - Phrase.from( - fragment.requireContext(), - R.string.activity_store_failure_server_error + } + catch (e: RequestCodeFailure) { + fragment.showError( + true, + R.string.activity_store_failure_message, + Phrase.from( + fragment.requireContext(), + R.string.activity_store_failure_server_error + ) + .put( + "message", + fragment.getString(R.string.activity_store_failure_server_error_general) ) - .put( - "message", - fragment.getString(R.string.activity_store_failure_server_error_general) - ) - .put("server_message", e.jsonErrorMessages().joinToString(", ")) - .format().toString() + .put("server_message", e.jsonErrorMessages().joinToString(", ")) + .format().toString() - ) - } - catch (e: IOException) { - fragment.showError( - true, - R.string.activity_store_failure_message, - R.string.activity_store_failure_server_error_generic_ioerror - ) - } + ) + } + catch (e: IOException) { + fragment.showError( + true, + R.string.activity_store_failure_message, + R.string.activity_store_failure_server_error_generic_ioerror + ) } } }