Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

QuotaCalculator v2 #1201

Merged
merged 11 commits into from Sep 22, 2020
Expand Up @@ -52,6 +52,7 @@ import de.rki.coronawarnapp.transaction.RiskLevelTransaction
import de.rki.coronawarnapp.ui.viewLifecycle
import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel
import de.rki.coronawarnapp.util.KeyFileHelper
import de.rki.coronawarnapp.util.di.AppInjector
import kotlinx.android.synthetic.deviceForTesters.fragment_test_for_a_p_i.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
Expand Down Expand Up @@ -80,6 +81,10 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel
}
}

private val enfClient by lazy {
AppInjector.component.enfClient
}

private var myExposureKeysJSON: String? = null
private var myExposureKeys: List<TemporaryExposureKey>? = mutableListOf()
private var otherExposureKey: AppleLegacyKeyExchange.Key? = null
Expand Down Expand Up @@ -397,7 +402,7 @@ class TestForAPIFragment : Fragment(), InternalExposureNotificationPermissionHel
Timber.i("Provide ${googleFileList.count()} files with ${appleKeyList.size} keys with token $token")
try {
// only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API
InternalExposureNotificationClient.asyncProvideDiagnosisKeys(
enfClient.provideDiagnosisKeys(
googleFileList,
ApplicationConfigurationService.asyncRetrieveExposureConfiguration(),
token!!
Expand Down
Expand Up @@ -11,11 +11,9 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import com.google.android.gms.nearby.Nearby
import com.google.android.gms.nearby.exposurenotification.ExposureInformation
import com.google.zxing.integration.android.IntentIntegrator
import com.google.zxing.integration.android.IntentResult
import de.rki.coronawarnapp.CoronaWarnApplication
import de.rki.coronawarnapp.databinding.FragmentTestRiskLevelCalculationBinding
import de.rki.coronawarnapp.exception.ExceptionCategory
import de.rki.coronawarnapp.exception.TransactionException
Expand All @@ -38,6 +36,7 @@ import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel
import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel
import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel
import de.rki.coronawarnapp.util.KeyFileHelper
import de.rki.coronawarnapp.util.di.AppInjector
import de.rki.coronawarnapp.util.security.SecurityHelper
import kotlinx.android.synthetic.deviceForTesters.fragment_test_risk_level_calculation.*
import kotlinx.coroutines.Dispatchers
Expand All @@ -63,8 +62,8 @@ class TestRiskLevelCalculationFragment : Fragment() {
private var binding: FragmentTestRiskLevelCalculationBinding by viewLifecycle()

// reference to the client from the Google framework with the given application context
private val exposureNotificationClient by lazy {
Nearby.getExposureNotificationClient(CoronaWarnApplication.getAppContext())
private val enfClient by lazy {
AppInjector.component.enfClient
}

override fun onCreateView(
Expand Down Expand Up @@ -214,7 +213,7 @@ class TestRiskLevelCalculationFragment : Fragment() {
Timber.i("Provide ${googleFileList.count()} files with ${appleKeyList.size} keys with token $token")
try {
// only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API
InternalExposureNotificationClient.asyncProvideDiagnosisKeys(
enfClient.provideDiagnosisKeys(
googleFileList,
ApplicationConfigurationService.asyncRetrieveExposureConfiguration(),
token
Expand Down Expand Up @@ -340,7 +339,7 @@ class TestRiskLevelCalculationFragment : Fragment() {

suspend fun asyncGetExposureInformation(token: String): List<ExposureInformation> =
suspendCoroutine { cont ->
exposureNotificationClient.getExposureInformation(token)
enfClient.internalClient.getExposureInformation(token)
.addOnSuccessListener {
cont.resume(it)
}.addOnFailureListener {
Expand Down
@@ -0,0 +1,38 @@
package de.rki.coronawarnapp.nearby

import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider
import timber.log.Timber
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton

@Suppress("DEPRECATION")
@Singleton
class ENFClient @Inject constructor(
private val googleENFClient: ExposureNotificationClient,
private val diagnosisKeyProvider: DiagnosisKeyProvider
) : DiagnosisKeyProvider {

// TODO Remove this once we no longer need direct access to the ENF Client,
// i.e. in **[InternalExposureNotificationClient]**
internal val internalClient: ExposureNotificationClient
get() = googleENFClient

override suspend fun provideDiagnosisKeys(
keyFiles: Collection<File>,
configuration: ExposureConfiguration?,
token: String
): Boolean {
Timber.tag(TAG).d(
d4rken marked this conversation as resolved.
Show resolved Hide resolved
"asyncProvideDiagnosisKeys(keyFiles=%s, configuration=%s, token=%s)",
keyFiles, configuration, token
)
return diagnosisKeyProvider.provideDiagnosisKeys(keyFiles, configuration, token)
}

companion object {
d4rken marked this conversation as resolved.
Show resolved Hide resolved
private val TAG: String = ENFClient::class.java.simpleName
}
}
@@ -0,0 +1,34 @@
package de.rki.coronawarnapp.nearby

import android.content.Context
import androidx.core.content.edit
import org.joda.time.Instant
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ENFClientLocalData @Inject constructor(
private val context: Context
) {

private val prefs by lazy {
context.getSharedPreferences("enfclient_localdata", Context.MODE_PRIVATE)
}

var lastQuotaResetAt: Instant
get() = Instant.ofEpochMilli(prefs.getLong(PKEY_QUOTA_LAST_RESET, 0L))
set(value) = prefs.edit(true) {
putLong(PKEY_QUOTA_LAST_RESET, value.millis)
}

var currentQuota: Int
get() = prefs.getInt(PKEY_QUOTA_CURRENT, 0)
set(value) = prefs.edit(true) {
putInt(PKEY_QUOTA_CURRENT, value)
}

companion object {
d4rken marked this conversation as resolved.
Show resolved Hide resolved
private const val PKEY_QUOTA_LAST_RESET = "enfclient.quota.lastreset"
private const val PKEY_QUOTA_CURRENT = "enfclient.quota.current"
}
}
@@ -0,0 +1,24 @@
package de.rki.coronawarnapp.nearby

import android.content.Context
import com.google.android.gms.nearby.Nearby
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
import dagger.Module
import dagger.Provides
import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DefaultDiagnosisKeyProvider
import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider
import javax.inject.Singleton

@Module
class ENFModule {

@Singleton
@Provides
fun exposureNotificationClient(context: Context): ExposureNotificationClient =
Nearby.getExposureNotificationClient(context)

@Singleton
@Provides
fun diagnosisKeySubmitter(submitter: DefaultDiagnosisKeyProvider): DiagnosisKeyProvider =
submitter
}
@@ -1,16 +1,13 @@
package de.rki.coronawarnapp.nearby

import com.google.android.gms.nearby.Nearby
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration.ExposureConfigurationBuilder
import com.google.android.gms.nearby.exposurenotification.ExposureSummary
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import de.rki.coronawarnapp.CoronaWarnApplication
import de.rki.coronawarnapp.risk.TimeVariables
import de.rki.coronawarnapp.storage.LocalData
import de.rki.coronawarnapp.storage.tracing.TracingIntervalRepository
import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToSeconds
import java.io.File
import de.rki.coronawarnapp.util.di.AppInjector
import java.util.Date
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
Expand All @@ -24,7 +21,7 @@ object InternalExposureNotificationClient {

// reference to the client from the Google framework with the given application context
private val exposureNotificationClient by lazy {
Nearby.getExposureNotificationClient(CoronaWarnApplication.getAppContext())
AppInjector.component.enfClient.internalClient
}

/****************************************************
Expand Down Expand Up @@ -101,36 +98,6 @@ object InternalExposureNotificationClient {
}
}

/**
* 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
* from your internet-accessible server to the Google Play service once requested from the
* API. Information about the file format is in the Exposure Key Export File Format and
* Verification document that is linked from google.com/covid19/exposurenotifications.
*
* @param keyFiles
* @param configuration
* @param token
* @return
*/
suspend fun asyncProvideDiagnosisKeys(
keyFiles: Collection<File>,
configuration: ExposureConfiguration?,
token: String
): Void = suspendCoroutine { cont ->
val exposureConfiguration = configuration ?: ExposureConfigurationBuilder().build()
exposureNotificationClient.provideDiagnosisKeys(
keyFiles.toList(),
exposureConfiguration,
token
)
.addOnSuccessListener {
cont.resume(it)
}.addOnFailureListener {
cont.resumeWithException(it)
}
}

/**
* Retrieves key history from the data store on the device for uploading to your
* internet-accessible server. Calling this method prompts Google Play services to display
Expand Down
@@ -0,0 +1,110 @@
@file:Suppress("DEPRECATION")

package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider

import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
import de.rki.coronawarnapp.util.GoogleAPIVersion
import timber.log.Timber
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

@Singleton
class DefaultDiagnosisKeyProvider @Inject constructor(
private val googleAPIVersion: GoogleAPIVersion,
private val submissionQuota: SubmissionQuota,
private val enfClient: ExposureNotificationClient
) : DiagnosisKeyProvider {

override suspend fun provideDiagnosisKeys(
keyFiles: Collection<File>,
configuration: ExposureConfiguration?,
token: String
): Boolean {
return try {
if (keyFiles.isEmpty()) {
Timber.tag(TAG).d("No key files submitted, returning early.")
return true
}

val usedConfiguration = if (configuration == null) {
ralfgehrer marked this conversation as resolved.
Show resolved Hide resolved
Timber.tag(TAG).w("Passed configuration was NULL, creating fallback.")
ExposureConfiguration.ExposureConfigurationBuilder().build()
} else {
configuration
}

if (googleAPIVersion.isAtLeast(GoogleAPIVersion.V16)) {
provideKeys(keyFiles, usedConfiguration, token)
} else {
provideKeysLegacy(keyFiles, usedConfiguration, token)
}
} catch (e: Exception) {
Timber.tag(TAG).e(
e, "Error during provideDiagnosisKeys(keyFiles=%s, configuration=%s, token=%s)",
keyFiles, configuration, token
)
throw e
}
}

private suspend fun provideKeys(
files: Collection<File>,
configuration: ExposureConfiguration,
token: String
): Boolean {
Timber.tag(TAG).d("Using non-legacy key provision.")

if (!submissionQuota.consumeQuota(1)) {
Timber.tag(TAG).w("Not enough quota available.")
// TODO Currently only logging, we'll be more strict in a future release
// return false
}

performSubmission(files, configuration, token)
return true
}

/**
* We 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 provideKeysLegacy(
keyFiles: Collection<File>,
configuration: ExposureConfiguration,
token: String
): Boolean {
Timber.tag(TAG).d("Using LEGACY key provision.")

if (!submissionQuota.consumeQuota(keyFiles.size)) {
Timber.tag(TAG).w("Not enough quota available.")
// TODO What about proceeding with partial submission?
// TODO Currently only logging, we'll be more strict in a future release
// return false
}

keyFiles.forEach { performSubmission(listOf(it), configuration, token) }
return true
}

private suspend fun performSubmission(
keyFiles: Collection<File>,
configuration: ExposureConfiguration,
token: String
): Void = suspendCoroutine { cont ->
Timber.tag(TAG).d("Performing key submission.")
enfClient
.provideDiagnosisKeys(keyFiles.toList(), configuration, token)
.addOnSuccessListener { cont.resume(it) }
.addOnFailureListener { cont.resumeWithException(it) }
}

companion object {
private val TAG: String = DefaultDiagnosisKeyProvider::class.java.simpleName
d4rken marked this conversation as resolved.
Show resolved Hide resolved
}
}
@@ -0,0 +1,25 @@
package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider

import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import java.io.File

interface DiagnosisKeyProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this called a provider? this class takes something / "consumes" it and does not provide something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it's the interface that the class wrapping GoogleENFClient.provideDiagnosisKeys has to implement. So while it does both, it is more responsible for "providing keys into the ENF" than consuming from our end. 🤔


/**
* 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
* from your internet-accessible server to the Google Play service once requested from the
* API. Information about the file format is in the Exposure Key Export File Format and
* Verification document that is linked from google.com/covid19/exposurenotifications.
*
* @param keyFiles
* @param configuration
* @param token
* @return
*/
suspend fun provideDiagnosisKeys(
keyFiles: Collection<File>,
configuration: ExposureConfiguration?,
token: String
): Boolean
}