diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt index 7ccb3476a11a996eb1c0d6e3bf04f54b4b1c9646..3138af290d66f75e9829c6cdb1fa3443880e3fe1 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.test.eventregistration.ui import android.annotation.SuppressLint import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import de.rki.coronawarnapp.R @@ -10,6 +11,7 @@ import de.rki.coronawarnapp.databinding.FragmentTestEventregistrationBinding import de.rki.coronawarnapp.test.menu.ui.TestMenuItem import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.doNavigate +import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels @@ -49,6 +51,29 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis findNavController().navigate(R.id.showStoredEventsTestFragment) } } + binding.runMatcher.setOnClickListener { + Toast.makeText(context, "Not implemented", Toast.LENGTH_SHORT).show() + } + + binding.downloadReportedCheckIns.setOnClickListener { + Toast.makeText(context, "Not implemented", Toast.LENGTH_SHORT).show() + } + + binding.calculateRisk.setOnClickListener { + Toast.makeText(context, "Not implemented", Toast.LENGTH_SHORT).show() + } + + viewModel.checkInOverlaps.observe2(this) { + val text = it.fold(StringBuilder()) { stringBuilder, checkInOverlap -> + stringBuilder + .append("CheckIn Id ${checkInOverlap.checkInId}") + .append("Date ${checkInOverlap.localDate}") + .append("Min. ${checkInOverlap.overlap.standardMinutes}") + .append("\n") + } + binding.resultText.text = text + binding.resultText.visibility = View.VISIBLE + } } companion object { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt index 105a19bd4b837e72457a9be0033ae564a7a785e7..1d26a529db303ec6843e0cb68be9f00e1dc24bf6 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt @@ -1,15 +1,28 @@ package de.rki.coronawarnapp.test.eventregistration.ui +import androidx.lifecycle.MutableLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.CheckInMatcher +import de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.CheckInOverlap import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory class EventRegistrationTestFragmentViewModel @AssistedInject constructor( - dispatcherProvider: DispatcherProvider + private val dispatcherProvider: DispatcherProvider, + private val checkInMatcher: CheckInMatcher ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + val checkInOverlaps = MutableLiveData<List<CheckInOverlap>>() + + fun runMatcher() { + launch { + val overlaps = checkInMatcher.execute() + checkInOverlaps.postValue(overlaps) + } + } + @AssistedFactory interface Factory : SimpleCWAViewModelFactory<EventRegistrationTestFragmentViewModel> } diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml index 390245bc42225eea7e9109b18285697cd8bcb503..86c86ce5570d7facbf73970d91f3b371b13d392c 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml @@ -10,17 +10,13 @@ <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp" - android:orientation="vertical" - android:paddingBottom="32dp"> + android:orientation="vertical"> <LinearLayout style="@style/Card" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="@dimen/spacing_tiny" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" android:orientation="vertical"> <TextView @@ -32,7 +28,7 @@ android:id="@+id/testQrCodeCreation" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="10dp" + android:layout_marginTop="@dimen/spacing_tiny" android:text="QR Code Creation" /> </LinearLayout> @@ -41,10 +37,10 @@ style="@style/Card" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="@dimen/spacing_tiny" - android:layout_marginStart="8dp" - android:layout_marginEnd="8dp" + android:layout_marginHorizontal="@dimen/spacing_tiny" + android:layout_marginBottom="@dimen/spacing_tiny" android:orientation="vertical"> + <TextView android:layout_width="match_parent" android:layout_height="wrap_content" @@ -54,10 +50,54 @@ android:id="@+id/scanCheckInQrCode" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" android:text="Scan check in QR code" /> </LinearLayout> + <LinearLayout + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_tiny" + android:orientation="vertical"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Download, matching & risk calculation" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/downloadReportedCheckIns" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="download check-ins" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/runMatcher" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:text="Run matcher" /> + + <TextView + android:id="@+id/resultText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:text="Matching result" + android:visibility="gone" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/calculateRisk" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:text="Calculate risk" /> + + </LinearLayout> + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/event_container" style="@style/Card" @@ -108,7 +148,5 @@ app:layout_constraintStart_toEndOf="@id/create_event_button" app:layout_constraintTop_toBottomOf="@id/events_body" /> </androidx.constraintlayout.widget.ConstraintLayout> - - </LinearLayout> </androidx.core.widget.NestedScrollView> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt index 2a082136720b9aaa1cc5007295b39be1345e57c3..82434300f4e0cb3b2bbbc86dc61fa0b73e517978 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt @@ -2,6 +2,8 @@ package de.rki.coronawarnapp.eventregistration import dagger.Binds import dagger.Module +import de.rki.coronawarnapp.eventregistration.checkins.download.DownloadedCheckInsRepo +import de.rki.coronawarnapp.eventregistration.checkins.download.FakeDownloadedCheckInsRepo import de.rki.coronawarnapp.eventregistration.checkins.qrcode.DefaultQRCodeVerifier import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository @@ -16,4 +18,7 @@ abstract class EventRegistrationModule { @Binds abstract fun traceLocationRepository(defaultTraceLocationRepo: DefaultTraceLocationRepository): TraceLocationRepository + + @Binds + abstract fun downloadedCheckInsRepo(repository: FakeDownloadedCheckInsRepo): DownloadedCheckInsRepo } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/CheckInsPackage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/CheckInsPackage.kt index 97f513181b57d32ee384dedf307409a778cf8adf..64a3ccf60c8a01403b848dee2d405bb918ab6b53 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/CheckInsPackage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/CheckInsPackage.kt @@ -1,6 +1,8 @@ package de.rki.coronawarnapp.eventregistration.checkins.download import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import org.joda.time.DateTime +import org.joda.time.Instant interface CheckInsPackage { @@ -9,3 +11,57 @@ interface CheckInsPackage { */ suspend fun extractCheckIns(): List<CheckIn> } + +// TODO replace with actual implementation + +// proprietary dummy implementations +object DummyCheckInPackage : CheckInsPackage { + override suspend fun extractCheckIns(): List<CheckIn> { + return listOf(dummyEventCheckIn1) + } +} + +private const val TYPE_LOCATION = 1 +private const val TYPE_EVENT = 2 + +private val dummyEventCheckIn1: CheckIn = CheckIn( + id = 1L, + guid = "eventOne", + version = 1, + type = TYPE_LOCATION, + description = "Restaurant", + address = "Around the corner", + traceLocationStart = null, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = null, + signature = "signature", + checkInStart = Instant.ofEpochMilli( + DateTime(2021, 2, 2012, 11, 45).millis + ), + checkInEnd = Instant.ofEpochMilli( + DateTime(2021, 2, 20, 12, 15).millis + ), + targetCheckInEnd = null, + createJournalEntry = false +) + +private val dummyEventCheckIn2: CheckIn = CheckIn( + id = 1L, + guid = "eventOne", + version = 1, + type = TYPE_EVENT, + description = "Women in tech meetup", + address = "Technology Park", + traceLocationStart = null, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = null, + signature = "signature2", + checkInStart = Instant.ofEpochMilli( + DateTime(2021, 3, 20, 18, 45).millis + ), + checkInEnd = Instant.ofEpochMilli( + DateTime(2021, 3, 20, 20, 15).millis + ), + targetCheckInEnd = null, + createJournalEntry = false +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/DownloadedCheckInsRepo.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/DownloadedCheckInsRepo.kt index 748cee11448f772318c85beecd0fb9e1542fcb33..a2fd5a465892f8b69dcab2f47bb1c2dbcda44c75 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/DownloadedCheckInsRepo.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/DownloadedCheckInsRepo.kt @@ -1,6 +1,8 @@ package de.rki.coronawarnapp.eventregistration.checkins.download import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import javax.inject.Inject interface DownloadedCheckInsRepo { @@ -10,3 +12,16 @@ interface DownloadedCheckInsRepo { fun removeCheckIns(checkins: List<CheckInsPackage>) } + +class FakeDownloadedCheckInsRepo @Inject constructor() : DownloadedCheckInsRepo { + override val allCheckInsPackages: Flow<List<CheckInsPackage>> + get() = listOf(listOf<CheckInsPackage>(DummyCheckInPackage)).asFlow() + + override fun addCheckIns(checkins: List<CheckInsPackage>) { + // TODO("Not yet implemented") + } + + override fun removeCheckIns(checkins: List<CheckInsPackage>) { + // TODO("Not yet implemented") + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/riskcalculation/CheckInMatcher.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/riskcalculation/CheckInMatcher.kt new file mode 100644 index 0000000000000000000000000000000000000000..30b2f9e00525cbec64bdcf39a34fee42b904d53e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/riskcalculation/CheckInMatcher.kt @@ -0,0 +1,37 @@ +package de.rki.coronawarnapp.eventregistration.checkins.riskcalculation + +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.eventregistration.checkins.download.DownloadedCheckInsRepo +import kotlinx.coroutines.flow.firstOrNull +import javax.inject.Inject + +class CheckInMatcher @Inject constructor( + private val checkInsRepository: CheckInRepository, + private val downloadedCheckInsRepo: DownloadedCheckInsRepo +) { + + suspend fun execute(): List<CheckInOverlap> { + val localCheckIns = checkInsRepository.allCheckIns.firstOrNull() ?: return emptyList() + val downloadedPackages = downloadedCheckInsRepo.allCheckInsPackages.firstOrNull() ?: return emptyList() + val relevantDownloadedCheckIns = + downloadedPackages.flatMap { + filterRelevantEventCheckIns( + localCheckIns, + it + ) + } + if (relevantDownloadedCheckIns.isEmpty()) return emptyList() + + // TODO split by midnight UTC? + + // calculate time overlap + val eventOverlapList = mutableListOf<CheckInOverlap>() + relevantDownloadedCheckIns.forEach { relevantDownloadedCheckIn -> + localCheckIns.forEach { localCheckIn -> + val overlap = calculateOverlap(localCheckIn, relevantDownloadedCheckIn) + if (overlap != null) eventOverlapList.add(overlap) + } + } + return eventOverlapList + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/riskcalculation/Matching.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/riskcalculation/Matching.kt new file mode 100644 index 0000000000000000000000000000000000000000..79077c6905af51302f282f5b9d4f20a272554848 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/riskcalculation/Matching.kt @@ -0,0 +1,38 @@ +package de.rki.coronawarnapp.eventregistration.checkins.riskcalculation + +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.checkins.download.CheckInsPackage +import org.joda.time.Duration +import org.joda.time.Instant +import org.joda.time.LocalDate + +suspend fun filterRelevantEventCheckIns( + localCheckIns: List<CheckIn>, + checkInsPackage: CheckInsPackage +): List<CheckIn> { + val reportedCheckIns = checkInsPackage.extractCheckIns() + return reportedCheckIns.filter { reported -> + localCheckIns.find { local -> + reported.guid == local.guid + } != null + } +} + +fun calculateOverlap( + local: CheckIn, + reported: CheckIn +): CheckInOverlap? { + if (local.guid != reported.guid) return null + // TODO implement calculation + return null +} + +data class CheckInOverlap( + val checkInId: Long, + val localDate: LocalDate, + val overlap: Duration +) + +fun min(first: Instant, second: Instant) = Instant(kotlin.math.min(first.millis, second.millis)) + +fun max(first: Instant, second: Instant) = Instant(kotlin.math.max(first.millis, second.millis))