From f57b2417aa37ae194494362f837147b46a572a50 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakob=20M=C3=B6ller?= <jakob.moeller@sap.com>
Date: Fri, 5 Jun 2020 13:22:50 +0200
Subject: [PATCH] Feature/Daily Fetch (#185)

* Adapt Google Calls for Batch Size 1

Signed-off-by: d067928 <jakob.moeller@sap.com>

* Adapt CachedKeyFileHolder.kt to allow Testing Scenarios and switch to Daily Fetching Only

Signed-off-by: d067928 <jakob.moeller@sap.com>
---
 .../rki/coronawarnapp/TestForAPIFragment.kt   |  10 ++
 .../de/rki/coronawarnapp/storage/LocalData.kt |  11 ++
 .../RetrieveDiagnosisKeysTransaction.kt       |  16 ++-
 .../coronawarnapp/util/CachedKeyFileHolder.kt | 118 +++++++++---------
 .../res/layout/fragment_test_for_a_p_i.xml    |   7 ++
 .../src/main/res/values/strings.xml           |   6 +
 .../util/CachedKeyFileHolderTest.kt           |   3 +-
 7 files changed, 104 insertions(+), 67 deletions(-)

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt
index 216e63c5f..5eebf8110 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/TestForAPIFragment.kt
@@ -7,6 +7,7 @@ import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.Switch
 import android.widget.Toast
 import androidx.core.content.pm.PackageInfoCompat
 import androidx.fragment.app.Fragment
@@ -63,6 +64,7 @@ import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_exposure_sum
 import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_exposure_summary_summationRiskScore
 import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_googlePlayServices_version
 import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.label_my_keys
+import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.test_api_switch_last_three_hours_from_server
 import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.text_my_keys
 import kotlinx.android.synthetic.main.fragment_test_for_a_p_i.text_scanned_key
 import kotlinx.coroutines.Dispatchers
@@ -156,6 +158,14 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel
             }
         }
 
+        val last3HoursSwitch = test_api_switch_last_three_hours_from_server as Switch
+        last3HoursSwitch.isChecked = LocalData.last3HoursMode()
+        last3HoursSwitch.setOnClickListener {
+            val isLast3HoursModeEnabled = last3HoursSwitch.isChecked
+            showToast("Last 3 Hours Mode is activated: $isLast3HoursModeEnabled")
+            LocalData.last3HoursMode(isLast3HoursModeEnabled)
+        }
+
         button_api_get_check_exposure.setOnClickListener {
             checkExposure()
         }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt
index 734d06ac1..192bf74d9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt
@@ -469,6 +469,17 @@ object LocalData {
         CoronaWarnApplication.getAppContext().getString(R.string.preference_teletan), null
     )
 
+    fun last3HoursMode(value: Boolean) = getSharedPreferenceInstance().edit(true) {
+        putBoolean(
+            CoronaWarnApplication.getAppContext().getString(R.string.preference_last_three_hours_from_server),
+            value
+        )
+    }
+
+    fun last3HoursMode(): Boolean = getSharedPreferenceInstance().getBoolean(
+        CoronaWarnApplication.getAppContext().getString(R.string.preference_last_three_hours_from_server), false
+    )
+
     /****************************************************
      * ENCRYPTED SHARED PREFERENCES HANDLING
      ****************************************************/
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt
index 49e9ff6f7..0382e7511 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt
@@ -230,17 +230,23 @@ object RetrieveDiagnosisKeysTransaction : Transaction() {
 
     /**
      * Executes the API_SUBMISSION Transaction State
+     *
+     * We currently use Batch Size 1 and thus submit multiple times to the API.
+     * This means that instead of directly submitting all files at once, we have to split up
+     * our file list as this equals a different batch for Google every time.
      */
     private suspend fun executeAPISubmission(
         token: String,
         exportFiles: Collection<File>,
         exposureConfiguration: ExposureConfiguration?
     ) = executeState(API_SUBMISSION) {
-        InternalExposureNotificationClient.asyncProvideDiagnosisKeys(
-            exportFiles,
-            exposureConfiguration,
-            token
-        )
+        exportFiles.forEach { batch ->
+            InternalExposureNotificationClient.asyncProvideDiagnosisKeys(
+                listOf(batch),
+                exposureConfiguration,
+                token
+            )
+        }
         Log.d(TAG, "Diagnosis Keys provided successfully, Token: $token")
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt
index c7ef6e95d..9fe260c58 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CachedKeyFileHolder.kt
@@ -23,10 +23,10 @@ import android.util.Log
 import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.http.WebRequestBuilder
 import de.rki.coronawarnapp.service.diagnosiskey.DiagnosisKeyConstants
+import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.storage.keycache.KeyCacheEntity
 import de.rki.coronawarnapp.storage.keycache.KeyCacheRepository
 import de.rki.coronawarnapp.storage.keycache.KeyCacheRepository.DateEntryType.DAY
-import de.rki.coronawarnapp.storage.keycache.KeyCacheRepository.DateEntryType.HOUR
 import de.rki.coronawarnapp.util.CachedKeyFileHolder.asyncFetchFiles
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toServerFormat
 import kotlinx.coroutines.Deferred
@@ -35,6 +35,7 @@ import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.withContext
 import java.io.File
+import java.lang.IllegalStateException
 import java.util.Date
 import java.util.UUID
 
@@ -65,40 +66,48 @@ object CachedKeyFileHolder {
      * @return list of all files from both the cache and the diff query
      */
     suspend fun asyncFetchFiles(currentDate: Date): List<File> = withContext(Dispatchers.IO) {
-        keyCache.deleteOutdatedEntries()
-        // queries will be executed after the "query plan" was set
-        val deferredQueries: MutableCollection<Deferred<Any>> = mutableListOf()
         val serverDates = getDatesFromServer()
-        val missingDays = getMissingDaysFromDiff(serverDates)
-        if (missingDays.isNotEmpty()) {
-            // we have a date difference
-            deferredQueries.addAll(
-                missingDays
-                    .map { getURLForDay(it) }
-                    .map { url -> async { url.createDayEntryForUrl() } }
-            )
-            // if we have a date difference we need to refetch the current hours
-            keyCache.clearHours()
-        }
-        val currentDateServerFormat = currentDate.toServerFormat()
-        // just fetch the hours if the date is available
-        if (serverDates.contains(currentDateServerFormat)) {
-            // we have an hour difference
-            deferredQueries.addAll(
-                getMissingHoursFromDiff(currentDate)
+        // TODO remove last3HourFetch before Release
+        if (isLast3HourFetchEnabled()) {
+            Log.v(TAG, "Last 3 Hours will be Fetched. Only use for Debugging!")
+            val currentDateServerFormat = currentDate.toServerFormat()
+            // just fetch the hours if the date is available
+            if (serverDates.contains(currentDateServerFormat)) {
+                return@withContext getLast3Hours(currentDate)
                     .map { getURLForHour(currentDate.toServerFormat(), it) }
-                    .map { url -> async { url.createHourEntryForUrl() } }
-            )
-        }
-        // execute the query plan
-        try {
-            deferredQueries.awaitAll()
-        } catch (e: Exception) {
-            // For an error we clear the cache to try again
-            keyCache.clear()
+                    .map { url -> async {
+                        return@async WebRequestBuilder.asyncGetKeyFilesFromServer(url)
+                    } }.awaitAll()
+            } else {
+                throw IllegalStateException(
+                    "you cannot use the last 3 hour mode if the date index " +
+                            "does not contain any data for today"
+                )
+            }
+        } else {
+            // queries will be executed after the "query plan" was set
+            val deferredQueries: MutableCollection<Deferred<Any>> = mutableListOf()
+            keyCache.deleteOutdatedEntries()
+            val missingDays = getMissingDaysFromDiff(serverDates)
+            if (missingDays.isNotEmpty()) {
+                // we have a date difference
+                deferredQueries.addAll(
+                    missingDays
+                        .map { getURLForDay(it) }
+                        .map { url -> async { url.createDayEntryForUrl() } }
+                )
+            }
+            // execute the query plan
+            try {
+                deferredQueries.awaitAll()
+            } catch (e: Exception) {
+                // For an error we clear the cache to try again
+                keyCache.clear()
+                throw e
+            }
+            keyCache.getFilesFromEntries()
+                .also { it.forEach { file -> Log.v(TAG, "cached file:${file.path}") } }
         }
-        keyCache.getFilesFromEntries()
-            .also { it.forEach { file -> Log.v(TAG, "cached file:${file.path}") } }
     }
 
     /**
@@ -114,16 +123,18 @@ object CachedKeyFileHolder {
     }
 
     /**
-     * Calculates the missing hours based on current missing entries in the cache
+     * TODO remove before Release
      */
-    private suspend fun getMissingHoursFromDiff(day: Date): List<String> {
-        val cacheEntries = keyCache.getHours()
-        return getHoursFromServer(day)
-            .also { Log.v(TAG, "${it.size} hours from server") }
-            .filter { it.hourEntryCacheMiss(cacheEntries, day) }
-            .toList()
-            .also { Log.d(TAG, "${it.size} missing hours") }
-    }
+    private const val LATEST_HOURS_NEEDED = 3
+    /**
+     * Calculates the last 3 hours
+     * TODO remove before Release
+     */
+    private suspend fun getLast3Hours(day: Date): List<String> = getHoursFromServer(day)
+        .also { Log.v(TAG, "${it.size} hours from server, but only latest 3 hours needed") }
+        .filter { TimeAndDateExtensions.getCurrentHourUTC() - LATEST_HOURS_NEEDED <= it.toInt() }
+        .toList()
+        .also { Log.d(TAG, "${it.size} missing hours") }
 
     /**
      * Determines whether a given String has an existing date cache entry under a unique name
@@ -135,16 +146,6 @@ object CachedKeyFileHolder {
         .map { date -> date.id }
         .contains(getURLForDay(this).generateCacheKeyFromString())
 
-    /**
-     * Determines whether a given String has an existing hour cache entry under a unique name
-     * given from the URL that is based on this String
-     *
-     * @param cache the given cache entries
-     */
-    private fun String.hourEntryCacheMiss(cache: List<KeyCacheEntity>, day: Date) = !cache
-        .map { hour -> hour.id }
-        .contains(getURLForHour(day.toServerFormat(), this).generateCacheKeyFromString())
-
     /**
      * Creates a date entry in the Key Cache for a given String with a unique Key Name derived from the URL
      * and the URI of the downloaded File for that given key
@@ -155,16 +156,6 @@ object CachedKeyFileHolder {
         DAY
     )
 
-    /**
-     * Creates an hour entry in the Key Cache for a given String with a unique Key Name derived from the URL
-     * and the URI of the downloaded File for that given key
-     */
-    private suspend fun String.createHourEntryForUrl() = keyCache.createEntry(
-        this.generateCacheKeyFromString(),
-        WebRequestBuilder.asyncGetKeyFilesFromServer(this).toURI(),
-        HOUR
-    )
-
     /**
      * Generates a unique key name (UUIDv3) for the cache entry based out of a string (e.g. an url)
      */
@@ -199,4 +190,9 @@ object CachedKeyFileHolder {
      */
     private suspend fun getHoursFromServer(day: Date) =
         WebRequestBuilder.asyncGetHourIndex(day)
+
+    /**
+     * TODO remove before release
+     */
+    private fun isLast3HourFetchEnabled(): Boolean = LocalData.last3HoursMode()
 }
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_test_for_a_p_i.xml b/Corona-Warn-App/src/main/res/layout/fragment_test_for_a_p_i.xml
index 81ff6d05b..c943bec46 100644
--- a/Corona-Warn-App/src/main/res/layout/fragment_test_for_a_p_i.xml
+++ b/Corona-Warn-App/src/main/res/layout/fragment_test_for_a_p_i.xml
@@ -26,6 +26,13 @@
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content" />
 
+            <Switch
+                android:id="@+id/test_api_switch_last_three_hours_from_server"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:theme="@style/switchBase"
+                android:text="@string/test_api_switch_last_three_hours_from_server" />
+
             <TextView
                 android:id="@+id/label_exposure_summary"
                 style="@style/textTitle"
diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml
index cf88f77f9..55a1ff5c6 100644
--- a/Corona-Warn-App/src/main/res/values/strings.xml
+++ b/Corona-Warn-App/src/main/res/values/strings.xml
@@ -108,6 +108,10 @@
     <string name="preference_teletan">
         <xliff:g id="preference">preference_teletan</xliff:g>
     </string>
+    <!-- NOTR -->
+    <string name="preference_last_three_hours_from_server">
+        <xliff:g id="preference">preference_last_three_hours_from_server</xliff:g>
+    </string>
 
     <!-- ####################################
                      Menu
@@ -830,6 +834,8 @@
     <!-- NOTR -->
     <string name="test_api_button_scan_qr_code">Scan Exposure Key</string>
     <!-- NOTR -->
+    <string name="test_api_switch_last_three_hours_from_server">Last 3 Hours Mode</string>
+    <!-- NOTR -->
     <string name="test_api_button_check_exposure">Check Exposure Summary</string>
     <!-- NOTR -->
     <string name="test_api_exposure_summary_headline">Exposure summary</string>
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/CachedKeyFileHolderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/CachedKeyFileHolderTest.kt
index 8985994fe..a079159ca 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/CachedKeyFileHolderTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/CachedKeyFileHolderTest.kt
@@ -50,6 +50,7 @@ class CachedKeyFileHolderTest {
 
         coEvery { keyCacheRepository.getDates() } returns listOf()
         coEvery { keyCacheRepository.getFilesFromEntries() } returns listOf()
+        every { CachedKeyFileHolder["isLast3HourFetchEnabled"]() } returns false
         every { CachedKeyFileHolder["getDatesFromServer"]() } returns arrayListOf<String>()
 
         runBlocking {
@@ -58,8 +59,8 @@ class CachedKeyFileHolderTest {
 
             coVerifyOrder {
                 CachedKeyFileHolder.asyncFetchFiles(date)
-                keyCacheRepository.deleteOutdatedEntries()
                 CachedKeyFileHolder["getDatesFromServer"]()
+                keyCacheRepository.deleteOutdatedEntries()
                 CachedKeyFileHolder["getMissingDaysFromDiff"](arrayListOf<String>())
                 keyCacheRepository.getDates()
                 keyCacheRepository.getFilesFromEntries()
-- 
GitLab