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