diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt index 09116212874bca048077fac935e2368b015ce8d9..020dd1d4065eaa2df04500b4a2c7561a38ba4f2c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt @@ -92,6 +92,15 @@ object InternalExposureNotificationClient { } } + suspend fun getVersion(): Long = suspendCoroutine { cont -> + exposureNotificationClient.version + .addOnSuccessListener { + cont.resume(it) + }.addOnFailureListener { + cont.resumeWithException(it) + } + } + /** * Takes an ExposureConfiguration object. Inserts a list of files that contain key * information into the on-device database. Provide the keys of confirmed cases retrieved diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt index ee6c36d9f96b314781cfc8224b5f1e5f20a1519a..69598eedc3bdd08e445d4f317330285a3d009b84 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt @@ -1,10 +1,12 @@ package de.rki.coronawarnapp.transaction +import de.rki.coronawarnapp.util.GoogleAPIVersion import javax.inject.Inject import javax.inject.Singleton // TODO Remove once we have refactored the transaction and it's no longer a singleton @Singleton data class RetrieveDiagnosisInjectionHelper @Inject constructor( - val transactionScope: TransactionCoroutineScope + val transactionScope: TransactionCoroutineScope, + val googleAPIVersion: GoogleAPIVersion ) 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 1cc35b3287f4cf11cb2d04b4a2565a1269aa8c03..bd927943211d962ca8b323aa903753513247ebdd 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 @@ -35,6 +35,7 @@ import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.Retriev import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.rollback import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.start import de.rki.coronawarnapp.util.CachedKeyFileHolder +import de.rki.coronawarnapp.util.GoogleAPIVersion import de.rki.coronawarnapp.util.GoogleQuotaCalculator import de.rki.coronawarnapp.util.QuotaCalculator import de.rki.coronawarnapp.util.di.AppInjector @@ -141,6 +142,10 @@ object RetrieveDiagnosisKeysTransaction : Transaction() { quotaChronology = GJChronology.getInstanceUTC() ) + private val googleAPIVersion: GoogleAPIVersion by lazy { + AppInjector.component.transRetrieveKeysInjection.googleAPIVersion + } + suspend fun startWithConstraints() { val currentDate = DateTime(Instant.now(), DateTimeZone.UTC) val lastFetch = DateTime( @@ -316,11 +321,21 @@ object RetrieveDiagnosisKeysTransaction : Transaction() { exportFiles: Collection<File>, exposureConfiguration: ExposureConfiguration? ) = executeState(API_SUBMISSION) { - InternalExposureNotificationClient.asyncProvideDiagnosisKeys( - exportFiles, - exposureConfiguration, - token - ) + if (googleAPIVersion.isAbove(GoogleAPIVersion.V16)) { + InternalExposureNotificationClient.asyncProvideDiagnosisKeys( + exportFiles, + exposureConfiguration, + token + ) + } else { + exportFiles.forEach { batch -> + InternalExposureNotificationClient.asyncProvideDiagnosisKeys( + listOf(batch), + exposureConfiguration, + token + ) + } + } Timber.tag(TAG).d("Diagnosis Keys provided successfully, Token: $token") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt new file mode 100644 index 0000000000000000000000000000000000000000..9b1998f66c493dd8abb62ad9e711dda791a93f8b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt @@ -0,0 +1,38 @@ +package de.rki.coronawarnapp.util + +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.CommonStatusCodes +import dagger.Reusable +import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import javax.inject.Inject +import kotlin.math.abs + +@Reusable +class GoogleAPIVersion @Inject constructor() { + /** + * Indicates if the client runs above a certain version + * + * @return isAboveVersion, if connected to an old unsupported version, return false + */ + suspend fun isAbove(compareVersion: Long): Boolean { + if (!compareVersion.isCorrectVersionLength) { + throw IllegalArgumentException("given version has incorrect length") + } + return try { + val currentVersion = InternalExposureNotificationClient.getVersion() + currentVersion >= compareVersion + } catch (apiException: ApiException) { + if (apiException.statusCode == CommonStatusCodes.API_NOT_CONNECTED) false + else throw apiException + } + } + + // check if a raw long has the correct length to be considered an API version + private val Long.isCorrectVersionLength + get(): Boolean = abs(this).toString().length == GOOGLE_API_VERSION_FIELD_LENGTH + + companion object { + private const val GOOGLE_API_VERSION_FIELD_LENGTH = 8 + const val V16 = 16000000L + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt index 3736f9a07ab10612bb8de055ac92d22235bbbbfd..c2029628c093f7f36ab440854d2335c987aa736d 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt @@ -4,6 +4,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.service.applicationconfiguration.ApplicationConfigurationService import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.GoogleAPIVersion import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.ApplicationComponent import io.mockk.Runs @@ -34,7 +35,8 @@ class RetrieveDiagnosisKeysTransactionTest { mockkObject(AppInjector) val appComponent = mockk<ApplicationComponent>().apply { every { transRetrieveKeysInjection } returns RetrieveDiagnosisInjectionHelper( - TransactionCoroutineScope() + TransactionCoroutineScope(), + GoogleAPIVersion() ) } every { AppInjector.component } returns appComponent @@ -52,6 +54,9 @@ class RetrieveDiagnosisKeysTransactionTest { any() ) } returns mockk() + coEvery { + InternalExposureNotificationClient.getVersion() + } returns 17000000L coEvery { ApplicationConfigurationService.asyncRetrieveExposureConfiguration() } returns mockk() every { LocalData.googleApiToken(any()) } just Runs every { LocalData.lastTimeDiagnosisKeysFromServerFetch() } returns Date() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleAPIVersionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleAPIVersionTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7667cb60400afae27e682d58f46a28007c610242 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleAPIVersionTest.kt @@ -0,0 +1,76 @@ +package de.rki.coronawarnapp.util + +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.CommonStatusCodes.API_NOT_CONNECTED +import com.google.android.gms.common.api.Status +import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import io.kotest.matchers.shouldBe +import io.mockk.Called +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockkObject +import io.mockk.unmockkObject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +@ExperimentalCoroutinesApi +internal class GoogleAPIVersionTest { + + private lateinit var classUnderTest: GoogleAPIVersion + + @BeforeEach + fun setUp() { + mockkObject(InternalExposureNotificationClient) + classUnderTest = GoogleAPIVersion() + } + + @AfterEach + fun tearDown() { + unmockkObject(InternalExposureNotificationClient) + } + + @Test + fun `isAbove API v16 is true for v17`() { + coEvery { InternalExposureNotificationClient.getVersion() } returns 17000000L + + runBlockingTest { + classUnderTest.isAbove(GoogleAPIVersion.V16) shouldBe true + } + + } + + @Test + fun `isAbove API v16 is false for v15`() { + coEvery { InternalExposureNotificationClient.getVersion() } returns 15000000L + + runBlockingTest { + classUnderTest.isAbove(GoogleAPIVersion.V16) shouldBe false + } + } + + @Test + fun `isAbove API v16 throws IllegalArgument for invalid version`() { + assertThrows<IllegalArgumentException> { + runBlockingTest { + classUnderTest.isAbove(1L) + } + coVerify { + InternalExposureNotificationClient.getVersion() wasNot Called + } + } + } + + @Test + fun `isAbove API v16 false when APIException for too low version`() { + coEvery { InternalExposureNotificationClient.getVersion() } throws + ApiException(Status(API_NOT_CONNECTED)) + + runBlockingTest { + classUnderTest.isAbove(GoogleAPIVersion.V16) shouldBe false + } + } +}