diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt index 1c5536f5901d0c9fcd0868403dbb057efe9c9ffd..43c90b8caf570ddcb58fa976c916431039d38209 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt @@ -10,7 +10,6 @@ import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.R import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.coronatest.CoronaTestRepository -import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.statistics.source.StatisticsProvider import de.rki.coronawarnapp.statistics.ui.homecards.StatisticsHomeCard @@ -67,7 +66,6 @@ class HomeFragmentTest : BaseUITest() { @MockK lateinit var cwaSettings: CWASettings @MockK lateinit var appConfigProvider: AppConfigProvider @MockK lateinit var statisticsProvider: StatisticsProvider - @MockK lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler @MockK lateinit var appShortcutsHelper: AppShortcutsHelper @MockK lateinit var tracingSettings: TracingSettings @MockK lateinit var traceLocationOrganizerSettings: TraceLocationOrganizerSettings @@ -298,7 +296,6 @@ class HomeFragmentTest : BaseUITest() { coronaTestRepository = coronaTestRepository, cwaSettings = cwaSettings, statisticsProvider = statisticsProvider, - deadmanNotificationScheduler = deadmanNotificationScheduler, appShortcutsHelper = appShortcutsHelper, tracingSettings = tracingSettings, traceLocationOrganizerSettings = traceLocationOrganizerSettings, diff --git a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index 0ab2b436cb66ae70971a996d43e7695c6eb9704a..014e3d72bfe3bb566dd2dfdab77b99e76068c387 100644 --- a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.TimeStamper import kotlinx.coroutines.CoroutineScope import timber.log.Timber import javax.inject.Inject @@ -16,11 +17,13 @@ class DefaultRiskLevelStorage @Inject constructor( presenceTracingRiskRepository: PresenceTracingRiskRepository, @AppScope scope: CoroutineScope, riskCombinator: RiskCombinator, + timeStamper: TimeStamper, ) : BaseRiskLevelStorage( riskResultDatabaseFactory, presenceTracingRiskRepository, scope, - riskCombinator + riskCombinator, + timeStamper, ) { // 2 days, 6 times per day, data is considered stale after 48 hours with risk calculation diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index 23a0450fef17a9260ddcd2b06328d3501d15b7c8..545f04c1e9d90ab98ae70084ab92fb845e8647de 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -7,6 +7,7 @@ import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao.PersistedScanInstance import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedExposureWindow import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedScanInstances +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.AppScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.firstOrNull @@ -20,11 +21,13 @@ class DefaultRiskLevelStorage @Inject constructor( presenceTracingRiskRepository: PresenceTracingRiskRepository, @AppScope val scope: CoroutineScope, riskCombinator: RiskCombinator, + timeStamper: TimeStamper, ) : BaseRiskLevelStorage( riskResultDatabaseFactory, presenceTracingRiskRepository, scope, riskCombinator, + timeStamper, ) { // 14 days, 6 times per day diff --git a/Corona-Warn-App/src/main/assets/privacy_de.html b/Corona-Warn-App/src/main/assets/privacy_de.html index 7b8ec6c8add9b0c70012ff94f914dc660e871165..4e1f055fe0b4fd91d0a28c44df4638e46dcce848 100644 --- a/Corona-Warn-App/src/main/assets/privacy_de.html +++ b/Corona-Warn-App/src/main/assets/privacy_de.html @@ -65,12 +65,11 @@ über den Datenschutz verarbeitet werden. </p> <p> - Wenn Sie Corona-positiv getestet sind und eine länderübergreifende Warnung - auslösen, können auch die Nutzer der offiziellen Corona-Apps anderer - Länder, denen Sie begegnet sind, gewarnt werden. In diesem Fall sind das - RKI und die zuständigen Gesundheitsbehörden der am länderübergreifenden - Warnsystem teilnehmenden Länder für die Datenverarbeitung gemeinsam - verantwortlich. Einzelheiten erfahren Sie unter Punkt 7. + Wenn Sie nach einem positiven Corona-Test per App eine länderübergreifende Warnung auslösen, + können auch die Nutzer von offiziellen Corona-Apps anderer Länder, denen Sie begegnet sind, + gewarnt werden. In diesem Fall sind das RKI und die zuständigen Gesundheitsbehörden der an den + jeweiligen länderübergreifenden Warnsystemen teilnehmenden Länder für die Datenverarbeitung + gemeinsam verantwortlich. Einzelheiten erfahren Sie unter Punkt 7. </p> <h1> 2. Ist die Nutzung der App freiwillig? @@ -86,16 +85,14 @@ 3. Auf welcher Rechtsgrundlage werden Ihre Daten verarbeitet? </h1> <p> - Ihre Daten werden vom RKI grundsätzlich nur verarbeitet, wenn Sie zuvor Ihr - ausdrückliches Einverständnis erteilt haben. Die Rechtsgrundlage ist Art. 6 - Abs. 1 S. 1 lit. a DSGVO sowie im Falle von Gesundheitsdaten Art. 9 Abs. 2 - lit. a DSGVO. Sie können ein einmal erteiltes Einverständnis jederzeit - wieder zurücknehmen (sogenanntes Widerrufsrecht). Weitere Informationen zu - Ihrem Widerrufsrecht finden Sie unter Punkt 12. Die Verarbeitung von - Zugriffsdaten für die Bereitstellung der täglichen Statistiken (siehe hierzu Punkt 6 - d.) erfolgt im Rahmen der Information der Öffentlichkeit durch das RKI gem. - § 4 Abs. 4 BGA-NachfG auf Basis von Art. 6 Abs. 1 S. 1 lit e. DSGVO i.V.m § - 3 BDSG. + Ihre Daten werden vom RKI grundsätzlich nur verarbeitet, wenn Sie zuvor Ihr ausdrückliches + Einverständnis erteilt haben. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. a DSGVO sowie im + Falle von Gesundheitsdaten Art. 9 Abs. 2 lit. a DSGVO. Sie können ein einmal erteiltes + Einverständnis jederzeit wieder zurücknehmen (sogenanntes Widerrufsrecht). Weitere Informationen + zu Ihrem Widerrufsrecht finden Sie unter Punkt 12. Die Verarbeitung von Zugriffsdaten für die + Bereitstellung der täglichen Statistiken (siehe hierzu Punkt 6.e.) erfolgt im Rahmen der + Information der Öffentlichkeit durch das RKI gem. § 4 Abs. 4 BGA-NachfG auf Basis von Art. 6 + Abs. 1 lit e. DSGVO i.V.m § 3 BDSG. </p> <h1> 4. An wen richtet sich die App? @@ -108,18 +105,18 @@ 5. Welche Daten werden verarbeitet? </h1> <p> - Das gesamte System der App ist so programmiert, dass so wenig - personenbezogene Daten wie möglich verarbeitet werden. Das bedeutet, dass - das System bei der Risiko-Ermittlung, der Warnung anderer und dem Abruf des Testergebnisses - keine Daten erfasst, die es dem RKI oder anderen Nutzern - ermöglichen, auf Ihre Identität, Ihren Namen, Ihren Standort oder andere - persönliche Details zu schließen. + Das gesamte System der App ist so programmiert, dass so wenig personenbezogene Daten wie möglich + verarbeitet werden. Das bedeutet, dass das System bei der Risiko-Ermittlung, der Warnung anderer + und dem Abruf des Testergebnisses keine Daten erfassen muss, die es dem RKI oder anderen Nutzern + ermöglichen, auf Ihre Identität, Ihren Namen, Ihren Standort oder andere persönliche Details zu + schließen. Eine Ausnahme gilt nur für die optionale Funktion „Schnelltest-Ergebnis nachweisen“, + mit der Sie eine auf Ihren Namen ausgestellte Bestätigung für negative Schnelltest-Ergebnisse + anzeigen lassen können (siehe hierzu Punkt 6 c.) </p> <p> - Die App verzichtet daher auch standardmäßig auf - die Auswertung Ihres Nutzungsverhaltens durch Analyse-Tools. Nur wenn Sie ausdrücklich der - freiwilligen Datenspende zustimmen, werden bestimmte Daten über Ihre Nutzung der App an das RKI - übermittelt (siehe hierzu Punkt 5 f.). + Die App verzichtet zudem standardmäßig auf die Auswertung Ihres Nutzungsverhaltens durch + Analyse-Tools. Nur wenn Sie ausdrücklich der freiwilligen Datenspende zustimmen, werden + bestimmte Daten über Ihre Nutzung der App an das RKI übermittelt (siehe hierzu Punkt 5 g.). </p> <p> Die von der App verarbeiteten Daten lassen sich den folgenden Kategorien @@ -234,7 +231,19 @@ </li> </ul> <h2> - c. Event-Daten + c. Schnelltestdaten +</h2> +<p> + Sie können die Ergebnisse der von Ihnen bei einer Teststelle durchgeführten Antigen-Schnelltests + über die App abrufen. Sollten Sie dieses Angebot wahrnehmen, wird Ihre Teststelle einen + individuellen QR-Code für Sie erzeugen, den Sie mit der App scannen müssen. Der QR-Code enthält + in kodierter Form eine eindeutige Kennzahl für Ihren Schnelltest sowie den Testzeitpunkt. Sofern + Sie im Fall eines negativen Schnelltest-Befunds zu Nachweiszwecken eine namentliche Darstellung + des Testergebnisses in der App wünschen (siehe zu dieser Funktion Punkt 6.c.), enthält der + QR-Code in kodierter Form weitere von Ihnen angegebene Daten. +</p> +<h2> + d. Event-Daten </h2> <p> Wenn Sie ein Event (beispielsweise eine Feier oder ein Konzert) oder einen Ort (beispielsweise @@ -266,7 +275,7 @@ entsprechenden Schieberegler ausschalten. </p> <p> - Sie können Ihre bisherigen Check-Ins unter „Meine Check-ins“ überprüfen, löschen und den + Sie können Ihre bisherigen Check-ins unter „Meine Check-ins“ überprüfen, löschen und den Auscheck-Zeitpunkt anpassen. </p> <p> @@ -278,12 +287,12 @@ QR-Codes“ jederzeit löschen. </p> <p> - Die Nutzung des Event-Check-Ins ist freiwillig. Sie entscheiden selbst darüber, ob Sie für Ihr + Die Nutzung des Event-Check-ins ist freiwillig. Sie entscheiden selbst darüber, ob Sie für Ihr Event oder Ihren Ort einen QR-Code erstellen und ob Sie sich bei einem Event oder Ort einchecken. </p> <h2> - d. Gesundheitsdaten + e. Gesundheitsdaten </h2> <p> Gesundheitsdaten sind alle Daten, die Informationen zum Gesundheitszustand @@ -311,7 +320,7 @@ Die Einzelheiten werden unter Punkt 6 erläutert. </p> <h2> - e. Einträge im Kontakt-Tagebuch + f. Einträge im Kontakt-Tagebuch </h2> <p> Wenn Sie im Kontakt-Tagebuch notieren, wann und wo Sie welche Personen getroffen haben und @@ -333,7 +342,7 @@ Verfügung stellen können. </p> <h2> - f. Nutzungsdaten (Datenspende) + g. Nutzungsdaten (Datenspende) </h2> <p> Wenn Sie die Datenspende aktivieren, übermittelt die App bestimmte Daten über Ihre App-Nutzung @@ -400,10 +409,10 @@ <p> Die Teilnahme an der Datenspende ist freiwillig. Die Aktivierung der Datenspende setzt die Bestätigung der Echtheit Ihrer App voraus (Beachten Sie bitte die weiteren Informationen hierzu - unter Punkt 5 h. und Punkt 11). + unter Punkt 5 i. und Punkt 11). </p> <h2> - g. Teilnahme an einer Befragung + h. Teilnahme an einer Befragung </h2> <p> Einigen Nutzern wird in der App die Teilnahme an einer Befragung des RKI angeboten. In der Regel @@ -418,10 +427,10 @@ Befragung teilnehmen möchten und Daten hierfür an das RKI übermittelt werden sollen. Die Befragungen finden auf einer Webseite außerhalb der App statt, auf die Sie weitergeleitet werden. Die Teilnahme an einer Befragung setzt die Bestätigung der Echtheit Ihrer App voraus - (Beachten Sie bitte die weitere Informationen hierzu unter Punkt 5 h. und Punkt 11). + (Beachten Sie bitte die weitere Informationen hierzu unter Punkt 5 i. und Punkt 11). </p> <h2> - h. Bestätigung der Echtheit Ihrer App + i. Bestätigung der Echtheit Ihrer App </h2> <p> Einige Funktionen der App setzen voraus, dass vorab die Echtheit Ihrer App geprüft und gegenüber @@ -457,13 +466,13 @@ Hierzu ruft die App vom Serversystem mehrmals täglich eine aktuelle Positiv-Liste mit den Angaben von Nutzern, die über die offizielle Corona-App eines am länderübergreifenden Warnsystem teilnehmenden Landes (siehe hierzu Punkt 7) eine Warnung ausgelöst haben ab. Diese Positiv-Liste - enthält die Zufalls-IDs der warnenden Nutzer sowie eventuelle Angaben zum Symptombeginn.. Falls + enthält die Zufalls-IDs der warnenden Nutzer sowie eventuelle Angaben zum Symptombeginn. Falls die warnenden Nutzer bei einem Event eingecheckt waren, enthält die Positiv-Liste auch die betreffenden Event-IDs und die Dauer des Check-ins (Eincheck- und Auscheck-Zeit). </p> <p> Die Zufalls-IDs und Event-IDs in den Positiv-Listen enthalten zusätzlich einen - Ãœbertragungsrisiko-Wert und eine Angabe zur Art der Diagnose (siehe hierzu Punkt 6 c.). + Ãœbertragungsrisiko-Wert und eine Angabe zur Art der Diagnose (siehe hierzu Punkt 6 d.). </p> <p> Die App gibt die Zufalls-IDs aus der Positiv-Liste an das COVID-19-Benachrichtigungssystem @@ -473,7 +482,7 @@ Begegnungsdaten. </p> <p> - Ebenso gleicht die App Event-IDs aus der Positiv-Liste mit den Event-IDs Ihrer Check-Ins ab, um + Ebenso gleicht die App Event-IDs aus der Positiv-Liste mit den Event-IDs Ihrer Check-ins ab, um festzustellen, ob Sie sich zeitgleich mit Corona-positiv getesteten Nutzern bei einem Event oder Ort aufgehalten haben. </p> @@ -507,45 +516,43 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende b. Testergebnis abrufen </h2> <p> - Wenn Sie einen Corona-Test gemacht haben, können Sie Ihr Testergebnis über - die App abrufen. Die App benachrichtigt Sie, sobald Ihr Testergebnis - vorliegt. Dies setzt voraus, dass das Testlabor an das Serversystem - angeschlossen ist und Sie im Rahmen der Testdurchführung gesondert Ihr - Einverständnis zur Ãœbermittlung Ihres Testergebnisses erteilt haben. - Testergebnisse von Laboren, die nicht an das Serversystem der App - angeschlossen sind, können nicht angezeigt werden. Wenn Sie keinen QR-Code - erhalten haben, können Sie diese Funktion ebenfalls nicht nutzen. + Wenn Sie einen Corona-Test (PCR-Test oder Antigen-Schnelltest) gemacht haben, können Sie Ihr + Testergebnis über die App abrufen. Die App benachrichtigt Sie, sobald Ihr Testergebnis vorliegt. + Dies setzt voraus, dass die Testeinrichtung (z.B. Testlabor oder Testzentrum) an das + Serversystem angeschlossen ist und Sie im Rahmen der Testdurchführung gesondert Ihr + Einverständnis zur Ãœbermittlung Ihres Testergebnisses erteilt haben. Testergebnisse von + Testeinrichtungen, die nicht an das Serversystem der App angeschlossen sind, können nicht + angezeigt werden. Wenn Sie keinen QR-Code erhalten haben, können Sie diese Funktion ebenfalls + nicht nutzen. </p> <p> <u>Scan des QR-Codes</u> </p> <p> - Damit Sie das Testergebnis per App abrufen können, müssen Sie den QR-Code - mit der Kamera Ihres Smartphones scannen. Der QR-Code enthält eine - Kennzahl, die beim Scannen ausgelesen wird und Ihrem Test zugeordnet ist. - Die ausgelesene Kennzahl wird von der App gehasht. Das bedeutet, dass die - Kennzahl nach einem bestimmten mathematischen Verfahren so verfremdet wird, - dass sie nicht mehr erkennbar ist. Die eindeutige Zuordnung der gehashten - Kennzahl zu Ihrem Testergebnis ist aber weiterhin möglich. Sobald Ihr - Smartphone eine Verbindung zum Internet hat, wird die gehashte Kennzahl von - der App an das Serversystem übermittelt. Das Serversystem stellt sodann - einen digitalen Zugangsschlüssel (ein sogenanntes Token) zur Verfügung, der - in der App gespeichert wird. Das Token ist auf dem Serversystem mit der - gehashten Kennzahl verknüpft. Die App löscht nun die auf Ihrem Smartphone - gehashte Kennzahl und behält nur das Token. Der QR-Code ist damit - verbraucht (ungültig), das heißt er kann von niemanden mehr verwendet - werden. So ist sichergestellt, dass Ihr QR-Code von keinem anderen Nutzer - für die Abfrage des Testergebnisses verwendet werden kann. + Damit Sie das Testergebnis per App abrufen können, müssen Sie den QR-Code mit der Kamera Ihres + Smartphones scannen. Der QR-Code enthält eine Kennzahl, die beim Scannen ausgelesen wird und + Ihrem Test zugeordnet ist. Handelt es sich um einen Antigen-Schnelltest, sind zudem die unter + Punkt 5 c. beschriebenen Schnelltestdaten im QR-Code enthalten. Die ausgelesene Kennzahl wird + von der App gehasht. Das bedeutet, dass die Kennzahl nach einem bestimmten mathematischen + Verfahren so verfremdet wird, dass sie nicht mehr erkennbar ist. Die eindeutige Zuordnung der + gehashten Kennzahl zu Ihrem Testergebnis ist aber weiterhin möglich. Sobald Ihr Smartphone eine + Verbindung zum Internet hat, wird die gehashte Kennzahl von der App an das Serversystem + übermittelt. Das Serversystem stellt sodann einen digitalen Zugangsschlüssel (ein sogenanntes + Token) zur Verfügung, der in der App gespeichert wird. Das Token ist auf dem Serversystem mit + der gehashten Kennzahl verknüpft. Die App löscht nun die auf Ihrem Smartphone gehashte Kennzahl + und behält nur das Token. Der QR-Code ist damit verbraucht (ungültig), das heißt er kann von + niemanden mehr verwendet werden. So ist sichergestellt, dass Ihr QR-Code von keinem anderen + Nutzer für die Abfrage des Testergebnisses verwendet werden kann. </p> <p> <u>Hinterlegung des Testergebnisses</u> </p> <p> - Sobald Ihr Testergebnis vorliegt, wird es vom Labor nur unter Angabe der - gehashten Kennzahl in der vom RKI betriebenen Testergebnis-Datenbank - hinterlegt. Die Testergebnis-Datenbank befindet sich auf einem speziellen - Server innerhalb des Serversystems. Das Labor erzeugt die gehashte Kennzahl - auf Basis des gleichen QR-Codes, den auch Sie erhalten haben. + Sobald Ihr Testergebnis vorliegt, wird es von der Testeinrichtung nur unter Angabe der gehashten + Kennzahl in der vom RKI betriebenen Testergebnis-Datenbank hinterlegt. Die + Testergebnis-Datenbank befindet sich auf einem speziellen Server innerhalb des Serversystems. + Die Testeinrichtung erzeugt die gehashte Kennzahl auf Basis des gleichen QR-Codes, den auch Sie + erhalten haben. </p> <p> <u>Abruf des Testergebnisses</u> @@ -572,18 +579,56 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende App. Eine Kopie der TAN verbleibt auf dem Serversystem. </p> <h2> - c. Andere warnen + c. Nachweis des Schnelltest-Ergebnisses +</h2> +<p> + Wenn Sie das Ergebnis eines Antigen-Schnelltests abrufen und in der Testeinrichtung die Option + zur namentlichen Anzeige für den Fall eines negativen Testergebnisses gewählt haben, wird ein + negativer Befund unter Angabe Ihres Namens, Ihres Geburtsdatums und des Testzeitpunkts + angezeigt. Die App verwendet hierzu die entsprechenden beim Scan des QR-Codes ausgelesenen + Schnelltestdaten. Die Schnelltestdaten werden gelöscht, sobald das negative Schnelltest-Ergebnis + nicht mehr in der App angezeigt wird. +</p> +<p> + Sie können das in der App angezeigte Testergebnis bei Bedarf vorzeigen, um die Durchführung + eines negativen Schnelltests gegenüber Dritten nachzuweisen. Informieren Sie sich hierzu bitte, + welche Anforderungen an Ihrem Aufenthaltsort im Einzelfall für die Anerkennung von digitalen + Testnachweisen erfüllt werden müssen. + Bitte beachten Sie: +</p> +<ul> + <li>Das RKI übernimmt keine Gewähr für die Anerkennung des angezeigten Schnelltest-Ergebnisses + durch zuständige Behörden und andere berechtigte Stellen, die von Ihnen die Vorlage eines + Testnachweises verlangen dürfen oder müssen (z. B. Geschäfte, Arbeitgeber). + </li> + <li>Sie sind nicht verpflichtet, die Nachweisfunktion der App zu verwenden. Falls Sie gegenüber + Dritten Ihr Testergebnis nachweisen müssen, können Sie den Nachweis im Rahmen der + gesetzlichen (auch Bundesland-spezifischen) Vorgaben auch in einer anderen Form vorlegen. + </li> +</ul> +<p> + Bei Abruf eines positiven Schnelltest-Ergebnisses erfolgt keine namentliche Anzeige. In diesem + Fall werden Ihr Name und Ihr Geburtsdatum sofort aus dem App-Speicher gelöscht. Ihre weiteren + Schnelltestdaten (Kennzahl, Testzeitpunkt) werden gelöscht, sobald das positive + Schnelltest-Ergebnis nicht mehr in der App angezeigt wird. +</p> +<h2> + d. Andere warnen </h2> <p> - Wenn Sie Corona-positiv getestet sind und Ihre Zufalls-IDs mit der App - teilen, können andere Nutzer dieser oder einer anderen offiziellen - Corona-App, denen Sie begegnet sind, länderübergreifend gewarnt werden. In - diesem Fall übermittelt die App folgende Daten an das Serversystem: + Wenn Sie Corona-positiv getestet sind und Ihre Zufalls-IDs mit der App teilen, können andere + Nutzer denen Sie begegnet sind, gewarnt werden. Daneben werden Nutzer gewarnt, die zeitgleich + mit Ihnen an denselben Events oder Orten eingecheckt waren. In diesem Fall übermittelt die App + folgende Daten an das Serversystem: </p> <ul> <li> Ihre eigenen Zufalls-IDs der letzten 14 Tage </li> + <li> + Event-IDs von Events oder Orten, bei denen Sie in den letzten 14 Tagen eingecheckt waren, + einschließlich der erfassten Eincheck- und Auscheck-Zeiten + </li> <li> eventuelle Angaben zum Symptombeginn </li> @@ -592,19 +637,15 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende </li> </ul> <p> - Vor der Weitergabe Ihres Testergebnisses (genauer gesagt: der Ãœbermittlung - Ihrer Zufalls-IDs) an das Serversystem fügt die App den Daten einen - Ãœbertragungsrisiko-Wert und eine Angabe zur Art der durchgeführten Tests - hinzu. Da die Warnfunktion der App nur bei im Labor bestätigten - Testergebnissen genutzt werden kann, ist die Art des Tests für alle Nutzer - gleich. Der Ãœbertragungsrisiko-Wert ist ein Schätzwert zur - Ansteckungswahrscheinlichkeit an den einzelnen Tagen des 14-Tage-Zeitraums. - Da die Ansteckungswahrscheinlichkeit von der Dauer und dem Verlauf der - Infektion abhängt, kann so beispielsweise berücksichtigt werden, dass am - Tag einer Risiko-Begegnung die Gefahr einer Ansteckung in der Regel je - geringer ist, desto mehr Zeit seit Symptombeginn verstrichen ist. Diese - zusätzlichen Ãœbertragungsrisiko-Werte ermöglichen eine genauere Bestimmung - der Ansteckungswahrscheinlichkeit für andere Nutzer. + Vor der Weitergabe Ihres Testergebnisses (genauer gesagt: der Ãœbermittlung Ihrer Zufalls-IDs und + Event-IDs sowie der erfassten Eincheck- und Auscheck-Zeiten) an das Serversystem fügt die App + den Daten einen Ãœbertragungsrisiko-Wert und eine Angabe zur Art der durchgeführten Tests hinzu. + Der Ãœbertragungsrisiko-Wert ist ein Schätzwert zur Ansteckungswahrscheinlichkeit an den + einzelnen Tagen des 14-Tage-Zeitraums. Da die Ansteckungswahrscheinlichkeit von der Dauer und + dem Verlauf der Infektion abhängt, kann so beispielsweise berücksichtigt werden, dass am Tag + einer Risiko-Begegnung die Gefahr einer Ansteckung in der Regel je geringer ist, desto mehr Zeit + seit Symptombeginn verstrichen ist. Diese zusätzlichen Ãœbertragungsrisiko-Werte ermöglichen eine + genauere Bestimmung der Ansteckungswahrscheinlichkeit für andere Nutzer. </p> <p> Die in der App abgefragten Angaben zum Symptombeginn sind optional. Diese @@ -618,23 +659,28 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende <u>Wenn Sie Ihr Testergebnis nicht in der App abgerufen haben:</u> </p> <p> - Auch wenn Sie Ihr positives Testergebnis nicht in der App abgerufen haben, können Sie Ihre - Mitmenschen warnen. Wählen Sie hierzu das Verfahren „TAN anfragen“. Die App fordert Sie dann - auf, die Hotline der App anzurufen. Ein Hotline-Mitarbeiter wird Ihnen dann einige Fragen - stellen, um sicherzugehen, dass Sie tatsächlich Corona-positiv getestet worden sind. Damit soll - verhindert werden, dass versehentlich oder absichtlich Falschwarnungen ausgelöst werden. Nach - ausreichender Beantwortung dieser Fragen werden Sie nach Ihrer Handy-/Telefonnummer und Ihrem - Namen gefragt. Dies dient dazu, Sie später zurückrufen zu können, um Ihnen eine einmalige TAN - zur Eingabe in der App mitzuteilen. Ihre Handy-/Telefonnummer und Ihr Name werden nur zu diesem - Zweck vorübergehend gespeichert und spätestens nach einer Stunde gelöscht. Unmittelbar nach - Ihrem Anruf wird der Hotline-Mitarbeiter über einen speziellen Zugang zum Serversystem eine - einmalige TAN erzeugen und Sie zurückrufen, um Ihnen diese mitzuteilen. Eine TAN ist nur eine - Stunde gültig und wird daher unmittelbar nach der Weitergabe an Sie, spätestens aber nach Ablauf - einer Stunde, von der Hotline gelöscht. Nach Eingabe einer gültigen TAN in der App wird diese an - das Serversystem übermittelt. Anhand der TAN wird somit die Ãœberprüfung ermöglicht, dass - tatsächlich ein positives Testergebnis vorliegt und Falschmeldungen können vermieden werden. - Anschließend erhält die App vom Serversystem einen Token, wie dies auch nach dem Scan eines - gültigen QR-Codes der Fall ist (siehe oben Punkt 6 b. unter „Testergebnis abrufen“). + Bei einem positiven Antigen-Schnelltest-Ergebnis können Sie Ihre Mitmenschen nur warnen, wenn + Sie das Testergebnis in der App abgerufen haben. +</p> +<p> + Bei einem positiven PCR-Testergebnis können Sie Ihre Mitmenschen hingegen auch dann warnen, wenn + Sie das Testergebnis außerhalb der App erhalten haben. Wählen Sie hierzu das Verfahren „TAN + anfragen“. Die App fordert Sie dann auf, die Hotline der App anzurufen. Ein Hotline-Mitarbeiter + wird Ihnen dann einige Fragen stellen, um sicherzugehen, dass Sie tatsächlich Corona-positiv + getestet worden sind. Damit soll verhindert werden, dass versehentlich oder absichtlich + Falschwarnungen ausgelöst werden. Nach ausreichender Beantwortung dieser Fragen werden Sie nach + Ihrer Handy-/Telefonnummer und Ihrem Namen gefragt. Dies dient dazu, Sie später zurückrufen zu + können, um Ihnen eine einmalige TAN zur Eingabe in der App mitzuteilen. Ihre + Handy-/Telefonnummer und Ihr Name werden nur zu diesem Zweck vorübergehend gespeichert und + spätestens nach einer Stunde gelöscht. Unmittelbar nach Ihrem Anruf wird der Hotline-Mitarbeiter + über einen speziellen Zugang zum Serversystem eine einmalige TAN erzeugen und Sie zurückrufen, + um Ihnen diese mitzuteilen. Eine TAN ist nur eine Stunde gültig und wird daher unmittelbar nach + der Weitergabe an Sie, spätestens aber nach Ablauf einer Stunde, von der Hotline gelöscht. Nach + Eingabe einer gültigen TAN in der App wird diese an das Serversystem übermittelt. Anhand der TAN + wird somit die Ãœberprüfung ermöglicht, dass tatsächlich ein positives Testergebnis vorliegt und + Falschmeldungen können vermieden werden. Anschließend erhält die App vom Serversystem einen + Token, wie dies auch nach dem Scan eines gültigen QR-Codes der Fall ist (siehe oben Punkt 6 b. + unter „Testergebnis abrufen“). </p> <p> Bitte beachten Sie, dass Ihre Warnung in seltenen Fällen dazu führen kann, dass Personen in @@ -644,7 +690,7 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende außer mit Ihnen keine anderen Kontakte hatte. </p> <h2> - d. Informatorische Nutzung der App + e. Informatorische Nutzung der App </h2> <p> Die täglichen Statistiken, die in der App erscheinen, erhält die App @@ -655,7 +701,7 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende jeweiligen Anbietern der aufgerufenen Webseite festgelegt. </p> <h2> - e. Kontakt-Tagebuch + f. Kontakt-Tagebuch </h2> <p> Das Kontakt-Tagebuch ist eine Zusatzfunktion der App. Ihre Einträge im Kontakt-Tagebuch dienen @@ -670,7 +716,7 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende Ansteckungen verhindern. </p> <h2> - f. Datenspende + g. Datenspende </h2> <p> Die Datenspende ist eine Zusatzfunktionen der App. Die im Rahmen der Datenspende an das RKI @@ -711,7 +757,7 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende oder wen Sie getroffen haben. </p> <h2> - g. Befragungen + h. Befragungen </h2> <p> Befragungen finden auf einer Webseite außerhalb der App statt, auf die Sie weitergeleitet @@ -720,7 +766,7 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende Befragung auf der Befragungs-Webseite beschrieben. </p> <h2> - h. Bestätigung der Echtheit Ihrer App + i. Bestätigung der Echtheit Ihrer App </h2> <p> Zur Bestätigung der Echtheit Ihrer App wird eine Funktion des Betriebssystems Ihres Smartphones @@ -758,12 +804,15 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende <p> Die nationalen Serversysteme der an den Austausch-Servern angebundenen Corona-Apps übermitteln ihre eigenen Positiv-Listen regelmäßig an die Austausch-Server und erhalten die Positiv-Listen - der anderen Länder. + der anderen Länder. Länderübergreifende Warnungen können nur aufgrund eines positiven PCR-Tests + und nur aufgrund von im COVID-19-Benachrichtigungssystem erfassten Begegnungen ausgelöst werden. + Event-IDs und Zufalls-IDs aufgrund von positiven Antigen-Schnelltests werden daher nicht an die + Austausch-Server übermittelt. </p> <p> Das jeweilige Serversystem führt die erhaltenen Positiv-Listen mit der eigenen Positiv-Liste zusammen, so dass die Risiko-Ermittlung auch Risiko-Begegnungen mit Nutzern einer anderen - Corona-App berücksichtigen kann (siehe Ziffer 6 c.). Die anderen teilnehmenden Länder verfahren + Corona-App berücksichtigen kann (siehe Ziffer 6 d.). Die anderen teilnehmenden Länder verfahren entsprechend mit den vom RKI bereitgestellten Positiv-Listen. </p> <p> @@ -865,7 +914,7 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende </p> <ul> <li> - Die Funktionen „Testergebnis abrufen“ und „Event-Check-In“ benötigen Zugriff auf die Kamera, + Die Funktionen „Testergebnis abrufen“ und „Event-Check-in“ benötigen Zugriff auf die Kamera, um QR-Codes scannen zu können. </li> </ul> @@ -890,7 +939,7 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende </p> <ul> <li> - Die Funktionen „Testergebnis abrufen“ und „Event-Check-In“ benötigen Zugriff auf die Kamera, + Die Funktionen „Testergebnis abrufen“ und „Event-Check-in“ benötigen Zugriff auf die Kamera, um QR-Codes scannen zu können. </li> </ul> @@ -946,12 +995,15 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende 10. An wen werden Ihre Daten weitergegeben? </h1> <p> - Wenn Sie andere Nutzer über die App warnen, werden Ihr Testergebnis (in Form Ihrer Zufalls-IDs - der letzten 14 Tage) sowie optionale Angaben zum Symptombeginn an die jeweils verantwortlichen - Gesundheitsbehörden der an den Austausch-Servern teilnehmenden Länder und von dort an die - Serversysteme der an den länderübergreifenden Warnungen teilnehmenden Corona-Apps weitergegeben. - Die Serversysteme der nationalen Corona-Apps verteilen diese Informationen dann als Bestandteil - der Positiv-Listen an ihre jeweiligen eigenen Nutzer. + Wenn Sie andere Nutzer aufgrund eines positiven PCR-Tests über die App warnen, werden Ihr + Testergebnis (in Form Ihrer Zufalls-IDs der letzten 14 Tage) sowie optionale Angaben zum + Symptombeginn an die jeweils verantwortlichen Gesundheitsbehörden der an den Austausch-Servern + teilnehmenden Länder und von dort an die Serversysteme der an den länderübergreifenden Warnungen + teilnehmenden Corona-Apps weitergegeben. Die Serversysteme der nationalen Corona-Apps verteilen + diese Informationen dann als Bestandteil der Positiv-Listen an ihre jeweiligen eigenen Nutzer. + Event-IDs werden nur über das Serversystem des RKI an Nutzer der Corona-Warn-App verteilt. Im + Fall einer Warnung aufgrund eines positiven Antigen-Schnelltests erfolgt keine Weitergabe Ihrer + Daten an die Austausch-Server. </p> <p> Mit dem Betrieb und der Wartung des gemeinsam betriebenen Austausch-Servers der teilnehmenden @@ -978,29 +1030,31 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende 11. Werden Ihre Daten in Länder außerhalb der EU übermittelt? </h1> <p> - Wenn Sie eine Warnung auslösen, werden Ihre Zufalls-IDs auch in die Schweiz zu dem vom RKI - gemeinsam mit dem Bundesamt für Gesundheit der Schweizerischen Eidgenossenschaft betriebenen - Austausch-Server übermittelt. Für die Schweiz hat die EU einen Angemessenheitsbeschluss - erlassen, in dem die Angemessenheit des Datenschutzniveaus festgestellt wird (Art. 45 DSGVO). - Daneben können die aktuellen Positiv-Listen unabhängig vom Aufenthaltsort des Nutzers (etwa im - Urlaub oder auf Geschäftsreise) abgerufen werden. Zudem kann es im Rahmen der Bestätigung der - Echtheit Ihrer App zu einer Ãœbermittlung von Daten in ein Land außerhalb der EU kommen. Die von - Ihrem Smartphone erzeugte Kennung, die Informationen über die Version Ihres Smartphones und der - App enthält, wird an den Betriebssystemanbieter Ihres Smartphones (Apple oder Google) - übermittelt. Dabei kann es auch zu einer Datenübermittlung in Drittländer, insbesondere in die - USA, kommen. Dort besteht möglicherweise kein dem europäischen Recht entsprechendes - Datenschutzniveau und Ihre europäischen Datenschutzrechte können eventuell nicht durchgesetzt - werden. Insbesondere besteht die Möglichkeit, dass Sicherheitsbehörden im Drittland auf die - übermittelten Daten beim Betriebssystemanbieter zugreifen und diese auswerten, beispielsweise - indem sie Daten mit anderen Informationen verknüpfen. Dies betrifft jedoch nur die übermittelte - Kennung. Weitere Angaben aus der App, beispielsweise Begegnungsdaten, sind davon nicht erfasst. -</p> -<p> - Im Ãœbrigen werden die von der App - übermittelten Daten ausschließlich auf Servern in Deutschland oder in einem - anderem Land in der EU (oder dem Europäischen Wirtschaftsraum) verarbeitet, - die somit den strengen Anforderungen der Datenschutz-Grundverordnung - (DSGVO) unterliegen. + Wenn Sie eine Warnung aufgrund eines positiven PCR-Tests auslösen, werden Ihre Zufalls-IDs auch + in die Schweiz zu dem vom RKI gemeinsam mit dem Bundesamt für Gesundheit der Schweizerischen + Eidgenossenschaft betriebenen Austausch-Server übermittelt. Im Fall einer Warnung aufgrund eines + positiven Antigen-Schnelltests erfolgt eine solche Ãœbermittlung Ihrer Daten nicht. +</p> +<p> + Für die Schweiz hat die EU einen Angemessenheitsbeschluss erlassen, in dem die Angemessenheit + des Datenschutzniveaus festgestellt wird (Art. 45 DSGVO). Daneben können die aktuellen + Positiv-Listen unabhängig vom Aufenthaltsort des Nutzers (etwa im Urlaub oder auf + Geschäftsreise) abgerufen werden. Zudem kann es im Rahmen der Bestätigung der Echtheit Ihrer App + zu einer Ãœbermittlung von Daten in ein Land außerhalb der EU kommen. Die von Ihrem Smartphone + erzeugte Kennung, die Informationen über die Version Ihres Smartphones und der App enthält, wird + an den Betriebssystemanbieter Ihres Smartphones (Apple oder Google) übermittelt. Dabei kann es + auch zu einer Datenübermittlung in Drittländer, insbesondere in die USA, kommen. Dort besteht + möglicherweise kein dem europäischen Recht entsprechendes Datenschutzniveau und Ihre + europäischen Datenschutzrechte können eventuell nicht durchgesetzt werden. Insbesondere besteht + die Möglichkeit, dass Sicherheitsbehörden im Drittland auf die übermittelten Daten beim + Betriebssystemanbieter zugreifen und diese auswerten, beispielsweise indem sie Daten mit anderen + Informationen verknüpfen. Dies betrifft jedoch nur die übermittelte Kennung. Weitere Angaben aus + der App, beispielsweise Begegnungsdaten, sind davon nicht erfasst. +</p> +<p> + Im Ãœbrigen werden die von der App übermittelten Daten ausschließlich auf Servern in Deutschland + oder in einem anderen Land in der EU (oder dem Europäischen Wirtschaftsraum) verarbeitet, die + somit den strengen Anforderungen der Datenschutz-Grundverordnung (DSGVO) unterliegen. </p> <h1> 12. Wie können Sie Ihr Einverständnis zurücknehmen? @@ -1044,7 +1098,7 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende Ihr Einverständnis zur Ãœbermittlung Ihres Testergebnisses (genauer gesagt: Ihrer Zufalls-IDs und Event-IDs sowie der erfassten Eincheck- und Auscheck-Zeiten der letzten 14 Tage) zur Warnung Ihrer Mitmenschen können Sie zurücknehmen, indem Sie den Test anzeigen und anschließend „Andere - warnen“ deaktivieren. Sie können unter „Meine Check-Ins“ auch Einträge zu Events oder Orten + warnen“ deaktivieren. Sie können unter „Meine Check-ins“ auch Einträge zu Events oder Orten löschen, und so verhindern, dass Daten zu diesen Events im Rahmen der Warnung verwendet werden. Diese Möglichkeit besteht, solange Sie Ihre Zufalls-IDs und Event-IDs noch nicht zur Warnung anderer Nutzer übermittelt haben. @@ -1060,7 +1114,7 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende </p> <p> Um bereits an das Serversystem übermittelte Event-IDs aus dem App-Speicher zu löschen, können - Sie unter „Meine Check-Ins“ auch Einträge zu Events oder Orten löschen. Event-IDs können dann + Sie unter „Meine Check-ins“ auch Einträge zu Events oder Orten löschen. Event-IDs können dann nicht mehr Ihrer Person oder Ihrem Smartphone zugeordnet werden. </p> <p> @@ -1072,7 +1126,7 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende Hinweise unter Punkt 5 b. </p> <h2> - d. Einverständnis „Event-Check-In“ + d. Einverständnis „Event-Check-in“ </h2> <p> Sie können unter „Meine Check-ins“ Einträge zu Events oder Orten jederzeit löschen. So @@ -1165,5 +1219,5 @@ Das für die letzten 14 Tage jeweils berechnete Risiko wird Ihnen in der Kalende datenschutz@rki.de. </p> <p> - Stand: 16.04.2021 + Stand: 30.04.2021 </p> diff --git a/Corona-Warn-App/src/main/assets/privacy_en.html b/Corona-Warn-App/src/main/assets/privacy_en.html index a7aeff7c47b5d658f483862bce26861aef56f7ee..0f8b3bcd162ba987e8325087ca0f9a445d21eec1 100644 --- a/Corona-Warn-App/src/main/assets/privacy_en.html +++ b/Corona-Warn-App/src/main/assets/privacy_en.html @@ -64,13 +64,12 @@ that your personal data is processed in accordance with data protection regulations. </p> <p> - If you have tested positive for coronavirus, you can use the transnational - warning feature to also warn users of the official coronavirus apps of - other countries whom you have encountered. In this case, the RKI and the - competent health authorities of the countries participating in the - transnational warning system are so-called joint controllers, meaning they - are jointly responsible for data processing. Please refer to Section 7 for - more details. + If, after testing positive for coronavirus, you use the app’s transnational warning feature, it + is also possible to warn users of official coronavirus apps of other countries whom you have + encountered. In this case, the RKI and the competent health authorities of the countries + participating in the respective transnational warning systems are so-called joint controllers, + meaning they are jointly responsible for data processing. Please refer to Section 7 for more + details. </p> <h1> 2. Is using the app voluntary? @@ -91,7 +90,7 @@ giving your consent, you can withdraw it at any time (so-called right of withdrawal). Please refer to Section 12 for further information about your right of withdrawal. On the basis of Art. 6(1) Sentence 1(e) GDPR in conjunction with Sect. 3 of the German Federal Data Protection Act - (BDSG), the processing of access data for the provision of daily statistics (see Section 6 d.) + (BDSG), the processing of access data for the provision of daily statistics (see Section 6 e.) is performed as part of the RKI’s duty to inform the public pursuant to Sect. 4(4) of the Act on Successor Agencies to the Federal Health Agency (BGA-NachfG). </p> @@ -105,15 +104,16 @@ <h1>5. What data is processed? </h1> <p> - The app’s entire system has been programmed to process as little personal - data as possible. This means that, when you use exposure logging, warn other users, or retrieve - a test result, the system does not collect any data that would allow the RKI or other users to - infer your identity, your name, your - location or other personal details. + The app’s entire system has been programmed to process as little personal data as possible. This + means that, when you use exposure logging, warn other users, or retrieve a test result, the + system does not need to collect any data that would allow the RKI or other users to infer your + identity, your name, your location or other personal details. The only exception to this is the + optional feature for proving a rapid test result, which allows you to display a confirmation + issued in your name for negative rapid test results (see Section 6 c.). </p> <p> The app therefore also refrains by default from using analysis tools to evaluate the way you use - it. Only if you expressly agree to voluntarily share data (see Section 5 f.), will certain data + it. Only if you expressly agree to voluntarily share data (see Section 5 g.), will certain data about your use of the app be transmitted to the RKI. </p> <p> @@ -226,7 +226,19 @@ </li> </ul> <h2> - c. Event data + c. Rapid test data +</h2> +<p> + If you have taken rapid antigen tests at a testing centre, you can retrieve the results of these + through the app. If you choose to use this service, your testing centre will generate an + individual QR code for you to scan with the app. The QR code contains a unique code for your + rapid test, and the time you were tested, in encoded form. If, in the event of a negative rapid + test result, you wish to have the test result displayed along with your name in the app for + verification purposes (see Section 6 c. for more information about this feature), the QR code + will contain further data provided by you in encoded form. +</p> +<h2> + d. Event data </h2> <p> If you visit an event (such as a party or concert) or a place (such as a shop or restaurant), @@ -251,7 +263,7 @@ stored on your smartphone. </p> <p> - An entry will also be created in your contact journal by default. Sections 5 e. and 6.e. explain + An entry will also be created in your contact journal by default. Sections 5 f. and 6.f. explain this in more detail. If you do not want to create an entry in your contact journal for an event or place, you can simply switch off this feature using the corresponding toggle switch. </p> @@ -271,7 +283,7 @@ check-out time. </p> <h2> - d. Health data + e. Health data </h2> <p> Health data is any data containing information about a person’s health. @@ -299,7 +311,7 @@ Section 6 explains this in more detail. </p> <h2> - e. Entries in the contact journal + f. Entries in the contact journal </h2> <p> If you use the contact journal to note when and where you met certain people and record certain @@ -319,7 +331,7 @@ tracing purposes, and how you can provide it. </p> <h2> - f. Usage data (data sharing) + g. Usage data (data sharing) </h2> <p> If you enable data sharing, the app will transmit certain data about your use of the app @@ -380,9 +392,9 @@ <p> Participation in data sharing is voluntary. To enable the data sharing feature, the authenticity of your app first needs to be confirmed (please note the further information about this under - Sections 5 h. and 11). + Sections 5 i. and 11). </p> -g. Participation in a survey +h. Participation in a survey </h2> <p> Some app users are offered to participate in a survey by the RKI. This offer to participate in @@ -396,10 +408,10 @@ g. Participation in a survey in a survey and whether data should be transmitted to the RKI for this purpose. The surveys take place on a website outside of the app, which you will be redirected to. To enable participation in a survey, the authenticity of your app first needs to be confirmed (please note the further - information about this in Sections 5 h. and 11). + information about this in Sections 5 i. and 11). </p> <h2> - h. Confirmation of the authenticity of your app + i. Confirmation of the authenticity of your app </h2> <p> Before you can use some of the app’s features, the authenticity of your app first needs to be @@ -443,7 +455,7 @@ g. Participation in a survey </p> <p> The random IDs and event IDs on the positive lists also contain a transmission risk value and an - indication of the type of diagnosis (see Section 6 c.). + indication of the type of diagnosis (see Section 6 d.). </p> <p> The app passes the random IDs from the positive list to the COVID-19 exposure @@ -490,45 +502,40 @@ g. Participation in a survey b. Retrieving a test result </h2> <p> - If you have taken a coronavirus test, you can retrieve your test result via - the app. The app will notify you as soon as your test result is available. - For this to work, the testing laboratory needs to be connected to the - server system and, as part of the testing procedure, you must have given - separate consent to your test result being sent. It is not possible to - display test results from laboratories that are not connected to the server - system. If you have not received a QR code, then you cannot use this - feature either. + If you have taken a coronavirus test (PCR test or rapid antigen test), you can retrieve your + test result via the app. The app will notify you as soon as your test result is available. For + this to work, the testing facility (e.g. testing laboratory or testing centre) needs to be + connected to the server system and, as part of the testing procedure, you must have given + separate consent to your test result being sent. It is not possible to display test results from + testing facilities that are not connected to the app’s server system. If you have not received a + QR code, then you cannot use this feature either. </p> <p> <u>Scanning the QR code</u> </p> <p> - In order to retrieve your test result via the app, you will need to scan - the QR code using your smartphone’s camera. The QR code contains a code - number that is read during scanning and is assigned to your test. After - reading the code number, the app ‘hashes’ it. This means that the app - performs a certain mathematical procedure in order to convert the code - number in such a way that it can no longer be identified. However, it is - still possible to clearly assign the hashed code number to your test - result. As soon as your smartphone is connected to the internet, the app - will transmit the hashed code number to the server system. The server - system then provides a digital access key (a so-called token), which is - stored in the app. The token is linked to the hashed code number in the - server system. The app now deletes the code number that has been hashed on - your smartphone and keeps only the token. Once the QR code has been used in - this way, it becomes invalid and can no longer be used by anyone. This - ensures that no other users can use your QR code to retrieve your test - result. + In order to retrieve your test result via the app, you will need to scan the QR code using your + smartphone’s camera. The QR code contains a code number that is read during scanning and is + assigned to your test. If the test is a rapid antigen test, then the QR code will also contain + the rapid test data described in Section 5 c. After reading the code number, the app ‘hashes’ + it. This means that the app performs a certain mathematical procedure in order to convert the + code number in such a way that it can no longer be identified. However, it is still possible to + clearly assign the hashed code number to your test result. As soon as your smartphone is + connected to the internet, the app will transmit the hashed code number to the server system. + The server system then provides a digital access key (a so-called token), which is stored in the + app. The token is linked to the hashed code number in the server system. The app now deletes the + code number that has been hashed on your smartphone and keeps only the token. Once the QR code + has been used in this way, it becomes invalid and can no longer be used by anyone. This ensures + that no other users can use your QR code to retrieve your test result. </p> <p> <u>Filing of the test result</u> </p> <p> - As soon as your test result is available, the laboratory stores it in the - RKI’s test result database using only the hashed code number. The test - result database is located on a special server within the server system. - The laboratory generates the hashed code number based on the same QR code - that you received. + As soon as your test result is available, the testing facility stores it in the RKI’s test + result database using only the hashed code number. The test result database is located on a + special server within the server system. The testing facility generates the hashed code number + based on the same QR code that you received. </p> <p> <u>Retrieval of the test result</u> @@ -554,14 +561,46 @@ g. Participation in a survey app. A copy of the TAN remains on the server system. </p> <h2> - c. Warning others + c. Proof of a rapid test result +</h2> +<p> + If you retrieve the result of a rapid antigen test and, when you were at the testing facility, + you selected the option to have your name displayed in the event of a negative test result, then + a negative result will be displayed along with your name, date of birth and the time you were + tested. To do this, the app uses the corresponding rapid test data which it reads when scanning + the QR code. The rapid test data will be deleted as soon as the negative rapid test result is no + longer displayed in the app. +</p> +<p> + If necessary, you can show the test result displayed in the app to prove to third parties that + you took a rapid test and the result of that test was negative. Please find out about applicable + requirements for the recognition of digital test certificates where you are located. Please + note: +</p> +<ul> + <li>The RKI cannot guarantee that a rapid test result displayed in the app will be recognised by + the competent authorities and other authorised bodies that may or must require you to + provide proof of testing (e.g. shops, employers). + </li> + <li>You are not obliged to use the app’s certification feature. If you have to prove your test + result to third parties, you can also present the proof in another form subject to the legal + requirements (which may vary depending on the federal state). + </li> +</ul> +<p> + Your name will not be displayed if you retrieve a positive rapid test result. In this case, your + name and date of birth will be immediately deleted from the app memory. Your other rapid test + data (code, time you were tested) will be deleted as soon as the positive rapid test result is + no longer displayed in the app. +</p> +<h2> + d. Warning others </h2> <p> If you have tested positive for coronavirus and share your random IDs with the app, then it is - possible to warn other app users whom you have encountered. This applies to other users of the - Corona-Warn-App as well as users of any other official coronavirus app in participating - countries. In addition, users who were checked in at the same events or places at the same time - as you will be warned. In this case, the app transmits the following data to the server system: + possible to warn other users whom you have encountered. In addition, users who were checked in + at the same events or places at the same time as you will be warned. In this case, the app + transmits the following data to the server system: </p> <ul> <li> @@ -581,16 +620,12 @@ g. Participation in a survey <p> Before passing on your test result (more precisely: before transmitting your random IDs and event IDs, including the recorded check-in and check-out times) to the server system, the app - adds a transmission risk value to the data and also specifies the type of test performed. Since - the - app’s warning feature can only be used for lab-confirmed test results, the - type of test is the same for all users. The transmission risk value is an - estimate of how infectious you were on each day of the 14-day period. Since - how infectious a person is or was depends on the duration and course of the - infection, it can be taken into account, for example, that the more time - has passed since the onset of symptoms, the lower the risk of a person - spreading the virus on the day of a possible exposure. These additional - transmission risk values allow a more precise determination of the + adds a transmission risk value to the data and also specifies the type of test performed. The + transmission risk value is an estimate of how infectious you were on each day of the 14-day + period. Since how infectious a person is or was depends on the duration and course of the + infection, it can be taken into account, for example, that the more time has passed since the + onset of symptoms, the lower the risk of a person spreading the virus on the day of a possible + exposure. These additional transmission risk values allow a more precise determination of the likelihood that you have infected other users. </p> <p> @@ -606,21 +641,26 @@ g. Participation in a survey <u>If you have not retrieved your test result in the app:</u> </p> <p> - Even if you have not retrieved your positive test result via the app, you can still warn fellow - users. To do this, select the “Request TAN†procedure. The app will then prompt you to call the - app hotline. A hotline worker will then ask you a few questions to make sure that you really - have tested positive for coronavirus. This is to prevent false warnings being transmitted, - either by accident or intentionally. Once you have answered these questions sufficiently, you - will be asked for your mobile/telephone number and your name. This is so that you can be called - back later and given a unique TAN to enter in the app. Your mobile/telephone number and your - name will be temporarily stored for this purpose only and deleted after an hour at the latest. - Immediately after your call, the hotline worker will generate a unique TAN via a special access - to the server system and then call you back to tell you this TAN. A TAN is only valid for one - hour and will therefore be deleted no later than one hour after it has been passed on to you. - After a valid TAN is entered in the app, it is transmitted to the server system. The TAN thus - makes it possible to check that a positive test result really does exist and thus prevent false - alarms. The app then receives a token from the server system, as it does after a valid QR code - is scanned (see “Retrieving a test result†in Section 6 b. above). + In the event of a positive rapid antigen test result, you can only warn other people if you + retrieved the test result in the app. +</p> +<p> + In the event of a positive PCR test result, on the other hand, you can warn others even if you + received the test result outside the app. To do this, select the “Request TAN†procedure. The + app will then prompt you to call the app hotline. A hotline worker will then ask you a few + questions to make sure that you really have tested positive for coronavirus. This is to prevent + false warnings being transmitted, either by accident or intentionally. Once you have answered + these questions sufficiently, you will be asked for your mobile/telephone number and your name. + This is so that you can be called back later and given a unique TAN to enter in the app. Your + mobile/telephone number and your name will be temporarily stored for this purpose only and + deleted after an hour at the latest. Immediately after your call, the hotline worker will + generate a unique TAN via a special access to the server system and then call you back to tell + you this TAN. A TAN is only valid for one hour and will therefore be deleted no later than one + hour after it has been passed on to you. After a valid TAN is entered in the app, it is + transmitted to the server system. The TAN thus makes it possible to check that a positive test + result really does exist and thus prevent false alarms. The app then receives a token from the + server system, as it does after a valid QR code is scanned (see “Retrieving a test result†in + Section 6 b. above). </p> <p> Please note that in rare cases, if you use the warning feature, people you know personally who @@ -629,7 +669,7 @@ g. Participation in a survey which the possible exposure is displayed. </p> <h2> - d. Using the app for information purposes only + e. Using the app for information purposes only </h2> <p> The app automatically receives the daily statistics that appear in the app @@ -640,7 +680,7 @@ g. Participation in a survey of the websites accessed. </p> <h2> - e. Contact journal + f. Contact journal </h2> <p> The contact journal is an additional feature of the app. What you enter in the contact journal @@ -653,7 +693,7 @@ g. Participation in a survey the risk of causing undetected infections. </p> <h2> - f. Data sharing + g. Data sharing </h2> <p> Share Data is an additional feature of the app. The usage data and other voluntary @@ -694,7 +734,7 @@ g. Participation in a survey have met. </p> <h2> - g. Surveys + h. Surveys </h2> <p> The surveys take place on a website outside of the app, which you will be redirected to. The app @@ -702,7 +742,7 @@ g. Participation in a survey survey are described in the information about the survey on the survey website. </p> <h2> - h. Confirmation of the authenticity of your app + i. Confirmation of the authenticity of your app </h2> <p> A feature of your smartphone’s operating system is used to confirm the authenticity of your app. @@ -736,16 +776,18 @@ g. Participation in a survey </li> </ul> <p> - The national server systems of the coronavirus apps connected to the - exchange server regularly transmit their own positive lists to the exchange - server and receive the positive lists of the other countries. However, event IDs are not - transmitted to the exchange servers. + The national server systems of the coronavirus apps connected to the exchange servers regularly + transmit their own positive lists to the exchange servers and receive the positive lists of the + other countries. Transnational warnings can only be transmitted based on a positive PCR test and + only based on encounters recorded in the COVID-19 exposure notification system. Event IDs and + random IDs based on positive rapid antigen tests are therefore not transmitted to the exchange + servers. </p> <p> Each server system merges the positive lists received in this way with its own positive list, which allows the exposure logging feature to also take into account possible exposures involving users of another coronavirus app - (see point 6 c.) The other participating countries proceed in the same way + (see point 6 d.) The other participating countries proceed in the same way with the positive lists provided by the RKI. </p> <p> @@ -926,14 +968,15 @@ g. Participation in a survey 10. Who will receive your data? </h1> <p> - If you warn other users via the app, your test result (in the form of your random IDs from the - last 14 days) as well as optional information you provide about the onset of your symptoms will - be forwarded to the competent health authorities of each of the countries participating in the - exchange servers. From there, they will be passed on to the server systems of the coronavirus - apps of those countries participating in the transnational warning system. The server systems of - the national coronavirus apps then distribute this information to their own users as part of the - positive lists. Event IDs are only distributed to users of the Corona-Warn-App via the RKI’s - server system. + If you warn other users of a positive PCR test via the app, your test result (in the form of + your random IDs from the last 14 days) as well as optional information you provide about the + onset of your symptoms will be forwarded to the competent health authorities of each of the + countries participating in the exchange servers. From there, they will be passed on to the + server systems of the coronavirus apps of those countries participating in the transnational + warning system. The server systems of the national coronavirus apps then distribute this + information to their own users as part of the positive lists. Event IDs are only distributed to + users of the Corona-Warn-App via the RKI’s server system. In the event of a warning based on a + positive rapid antigen test, your data will not be passed on to the exchange servers. </p> <p> The competent national health authorities have commissioned the EU Commission, as data @@ -962,12 +1005,10 @@ g. Participation in a survey 11. Is your data transferred to countries outside the EU? </h1> <p> - If you activate the warning feature, your random IDs will also be transmitted to Switzerland to - the exchange server that is operated by the RKI together with the Federal Office of Public - Health of the Swiss Confederation. The EU has issued an adequacy decision for Switzerland, which - determines the adequacy of the level of data protection in the country (Art. 45 GDPR). In - addition, please note that users can retrieve the latest positive lists regardless of where they - are (even if they are abroad on holiday or on a business trip, for example). + If you activate the warning feature based on a positive PCR test, your random IDs will also be + transmitted to Switzerland to the exchange server that is operated by the RKI together with the + Federal Office of Public Health of the Swiss Confederation. In the event of a warning based on a + positive rapid antigen test, no such transmission of your data will take place. </p> <p> In addition, the confirmation of the authenticity of your app may involve the transfer of data @@ -1142,5 +1183,5 @@ g. Participation in a survey 13353 Berlin, or by emailing datenschutz@rki.de. </p> <p> - Last amended: 16 April 2021 + Last amended: 30 April 2021 </p> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/assets/privacy_tr.html b/Corona-Warn-App/src/main/assets/privacy_tr.html index dbc58b0aae99f117915f3b8a7d61673e2d19f0e9..aa5acac5f18ee234ab5659df098ac049f653b610 100644 --- a/Corona-Warn-App/src/main/assets/privacy_tr.html +++ b/Corona-Warn-App/src/main/assets/privacy_tr.html @@ -65,12 +65,11 @@ de sorumludur. </p> <p> - Korona testiniz pozitif çıkmışsa ve sınır ötesi bir uyarı tetiklediÄŸinizde, - katılımcı ülkelerdeki resmi Korona uygulamalarının kullanıcılarıyla bir - karşılaÅŸma söz konusu olursa, bu kiÅŸilerin uyarılması saÄŸlanır. Bu durumda - hem RKI, hem de sınır ötesi uyarı sistemine katılan ülkelerin yetkili - saÄŸlık kurumları, verilerin iÅŸlenmesinden müştereken sorumlu olur. - Ayrıntılar için Madde 7’ye bakın. + Uygulama ile yapılan korona testiniz pozitif çıkmışsa ve sınır ötesi bir uyarı tetiklediÄŸinizde, + katılımcı ülkelerdeki resmi Korona uygulamalarının kullanıcılarıyla bir karşılaÅŸma söz konusu + olursa, bu kiÅŸilerin uyarılması saÄŸlanır. Bu durumda hem RKI hem de sınır ötesi uyarı + sistemlerine katılan ilgili ülkelerin yetkili saÄŸlık kurumları, verilerin iÅŸlenmesinden + müştereken sorumlu olur. Ayrıntılar için Madde 7’ye bakın. </p> <h1> 2. Uygulamanın kullanılması isteÄŸe baÄŸlı mı? @@ -107,17 +106,18 @@ 5. Hangi veriler iÅŸlenir? </h1> <p> - Uygulamanın tüm sistemi, mümkün olduÄŸunca az kiÅŸisel verileri iÅŸleyecek - ÅŸekilde programlanmıştır. Bu demektir ki sistem, risk deÄŸerlendirmesi, diÄŸer kiÅŸilerin - uyarılması ve test sonucunun alınması için, RKI’nin veya diÄŸer - kullanıcıların sizin kimliÄŸinizi, adınızı, konumunuzu veya diÄŸer kiÅŸisel - bilgilerinizi öğrenmesine olanak tanıyan verileri toplamamaktadır. + Uygulamanın tüm sistemi, mümkün olduÄŸunca az kiÅŸisel verileri iÅŸleyecek ÅŸekilde + programlanmıştır. Bu demektir ki sistem, risk deÄŸerlendirmesi, diÄŸer kiÅŸilerin uyarılması ve + test sonucunun alınması için, RKI’nin veya diÄŸer kullanıcıların sizin kimliÄŸinizi, adınızı, + konumunuzu veya diÄŸer kiÅŸisel bilgilerinizi öğrenmesine olanak tanıyan verileri toplamamaktadır. + Kendi adınıza düzenlenen negatif hızlı test sonucu için onayı görüntüleyebileceÄŸiniz (bkz. Madde + 6c) Opsiyonel “hızlı test sonucunu kanıtlama†fonksiyonu için istisna söz konusudur. </p> <p> Dolayısıyla Uygulama, standart olarak analiz araçları üzerinden kullanıcı davranışınızın herhangi bir deÄŸerlendirmesini yapmaz. Sadece isteÄŸe baÄŸlı veri bağışına açıkça onay vermeniz durumunda, Uygulama kullanımınıza iliÅŸkin belirli veriler, RKI’ye aktarılacaktır (bkz. Madde 5 - f.). + g.). </p> <p> Uygulama tarafından iÅŸlenen veriler aÅŸağıdaki kategorilerde @@ -230,7 +230,18 @@ </li> </ul> <h2> - c. Olay verileri + c. Hızlı test verileri +</h2> +<p> + Test merkezinde tarafınızca yapılan antijen hızlı testlerin sonuçlarını Uygulama üzerinden + açabilirsiniz. Bu tekliften faydalanmak için test merkeziniz sizin için Uygulama ile taramanızın + gerektiÄŸi özel bir kare kodu oluÅŸturur. Kare kodda kodlanmış halde hızlı testiniz için sarih kod + ve test zamanı mevcuttur. Uygulamada negatif hızlı test bulgusuna iliÅŸkin test sonuçlarını + kanıtlama amaçları için isminizle görüntülemek için kare kodda kodlanmış halde tarafınızca + kaydedilen diÄŸer veriler bulunur. +</p> +<h2> + d. Olay verileri </h2> <p> Bir olayı (örneÄŸin bir etkinlik, bir parti veya konser) veya bir konumu (örneÄŸin bir maÄŸaza veya @@ -278,7 +289,7 @@ oluÅŸturmaya ve bir olay veya konumda giriÅŸ denetimi yaptırmaya siz kendiniz karar verirsiniz. </p> <h2> - d. SaÄŸlık verileri + e. SaÄŸlık verileri </h2> <p> SaÄŸlık verileri, bir kiÅŸinin saÄŸlığı hakkında bilgiler içeren tüm @@ -306,7 +317,7 @@ Ayrıntılar 6. Maddede açıklanmıştır. </p> <h2> - e. Temas güncesindeki veriler + f. Temas güncesindeki veriler </h2> <p> Temas günlüğünüze, hangi kiÅŸilerle ne zaman ve nerede karşılaÅŸtığınızı ve karşılaÅŸmanın @@ -327,7 +338,7 @@ söyleyecektir. </p> <h2> - f. Veri bağışı + g. Veri bağışı </h2> <p> Veri bağışını etkinleÅŸtirdiÄŸinizde Uygulama, Uygulama kullanımınıza iliÅŸkin belirli verileri @@ -389,7 +400,7 @@ bkz. Madde 5 h. ve 11.). </p> <h2> - g. Ankete katılım + h. Ankete katılım </h2> <p> Uygulamada bazı kullanıcılara bir RKI anketine katılma olanağı verilir. Ankete katılma teklifi @@ -405,7 +416,7 @@ doÄŸrulanmasını gerektirir (bu konuyla ilgili daha fazla bilgi için bkz. Madde 5 h. ve 11.). </p> <h2> - h. Uygulamanızın orijinalliÄŸinin doÄŸrulanması + i. Uygulamanızın orijinalliÄŸinin doÄŸrulanması </h2> <p> Uygulamadaki bazı iÅŸlevler, Uygulamanızın orijinalliÄŸinin önceden kontrol edilmesini ve RKI için @@ -437,14 +448,17 @@ saÄŸlık bilgileri temin etmektir. </p> <p> - Bu amaç doÄŸrultusunda Uygulama, arka planda çalışarak sunucu sisteminden, - Korona testi pozitif çıkan ve sınır ötesi uyarı sistemine katılan ülkelerin - resmi Korona uygulamaları aracılığıyla bir uyarı tetikleyen kullanıcılardan - rastgele kimlik numaraları ve varsa semptomların baÅŸlangıcına iliÅŸkin - bilgiyi içeren listeleri günde birçok kez çağırır (bundan böyle: <strong>pozitif liste</strong>). - Pozitif listedeki rastgele kimlik - numaraları, ek olarak ayrıca bir taşıma riski deÄŸeri ve tanı tipi hakkında - bilgi de içerir (bkz. Madde 6 c.). + Bu amaç doÄŸrultusunda Uygulama, sınır ötesi uyarı sistemine katılan bir ülkenin resmi Korona + uygulamaları aracılığıyla bir uyarı tetikleyen kullanıcılara ait bilgileri içeren güncel bir + pozitif listeyi sunucu sisteminden günde birkaç kez çağırır (bkz. Madde 7). Bu pozitif liste, + uyarı tetikleyen kullanıcılara ait rastgele kimlik numaralarını ve varsa semptomların + baÅŸlangıcına iliÅŸkin bilgileri içerir. Uyarı tetikleyen kullanıcılar bir olayda giriÅŸ denetimi + yaptırırlarsa, bu durumda pozitif liste, ilgili olay kimliklerini ve olayda kalma süresini + (giriÅŸ ve çıkış saatleri) içerir. +</p> +<p> + Pozitif listedeki rastgele kimlik numaralarını ve olay kimliklerini, ek olarak bir taşıma riski + deÄŸeri ve tanı tipi hakkında bilgi de içerir (bkz. Madde 6 c.). </p> <p> Uygulama pozitif listeden aldığı bu rastgele kimlik numaralarını, COVID-19 @@ -471,14 +485,13 @@ b. Test sonucu çağırın </h2> <p> - Bir korona testi yaptırdıysanız, test sonucunuzu Uygulama üzerinden - görüntüleyebilirsiniz. Test sonucunuz hazır olur olmaz, Uygulama sizi bu - konuda bilgilendirecektir. Ancak bu bilgilendirme, test laboratuvarının - sunucu sistemine baÄŸlı olmasını ve ayrıca test süreci kapsamında sizin test - sonuçlarınızın aktarılmasına onay vermenizi gerektirir. Uygulamanın sunucu - sistemine baÄŸlı olmayan laboratuvarlardan gönderilen test sonuçları - görüntülenemez. Bir QR kod almadıysanız, bu iÅŸlevi yine aynı ÅŸekilde - kullanamazsınız. + Bir korona testi (PCR testi ya da hızlı antijen testi) yaptırdıysanız, test sonucunuzu Uygulama + üzerinden görüntüleyebilirsiniz. Test sonucunuz hazır olur olmaz, Uygulama sizi bu konuda + bilgilendirecektir. Ancak bu bilgilendirme, test kuruluÅŸunun (örn. test laboratuvarının ya da + test merkezinin) sunucu sistemine baÄŸlı olmasını ve ayrıca test süreci kapsamında sizin test + sonuçlarınızın aktarılmasına onay vermenizi gerektirir. Uygulamanın sunucu sistemine baÄŸlı + olmayan test kuruluÅŸları tarafından gönderilen test sonuçları görüntülenemez. Bir QR kod + almadıysanız, bu iÅŸlevi yine aynı ÅŸekilde kullanamazsınız. </p> <p> Sizin için bir enfeksiyon riski hesaplanırsa, bu deÄŸer Uygulamada görüntülenecektir. @@ -493,33 +506,30 @@ <u>QR kodu tarayın</u> </p> <p> - Test sonucunu Uygulama aracılığıyla çağırmak için, bu QR kodu akıllı - telefonunuzun kamerasıyla taramanız gerekir. Bu QR kod, bir kod numarası - içerir, bu kod numarası tarama iÅŸlemiyle okunur ve testinize ataması - yapılır. Okunan kod numarası, Uygulama tarafından karma hale getirilir. - Bunun anlamı, kod numarasının spesifik bir matematik yöntemi ile sürece - yabancılaÅŸtırılması ve böylece artık tanınmaz hale gelmesidir. Bu karma kod - numarasının test sonucunuza benzersiz olarak atanması, - <br/> - hâlâ mümkündür. Akıllı telefonunuz internete baÄŸlanır baÄŸlanmaz, Uygulama - bu karma kod numarasını sunucu sistemine iletir. Bunun üzerine sunucu - sistemi bir dijital eriÅŸim kodu, yani bir belirteç (token) sunar ve bu - belirteç Uygulamaya kaydedilir. Belirteç, sunucu sistemindeki karma kod - numarasına baÄŸlıdır. Uygulama daha sonra akıllı telefonunuzdaki karma kod - numarasını siler ve sadece belirteci tutar. Böylece QR kod artık - kullanılmış (geçersiz) olur, yani artık kimse tarafından kullanılamaz. Bu - sayede QR kodunuzun test sonucunu sorgulamak için baÅŸka bir kullanıcı - tarafından kullanılmasının önüne geçilmiÅŸ olur. + Test sonucunu Uygulama aracılığıyla çağırmak için, bu QR kodu akıllı telefonunuzun kamerasıyla + taramanız gerekir. Bu QR kod, bir kod numarası içerir, bu kod numarası tarama iÅŸlemiyle okunur + ve testinize ataması yapılır. Hızlı antijen testi söz konusuysa kare kod, Madde 5c. altında + açıklanan hızlı test verilerini içerir. Okunan kod numarası, Uygulama tarafından karma hale + getirilir. Bunun anlamı, kod numarasının spesifik bir matematik yöntemi ile sürece + yabancılaÅŸtırılması ve böylece artık tanınmaz hale gelmesidir. Bu karma kod numarasının test + sonucunuza benzersiz olarak atanması, hâlâ mümkündür. Akıllı telefonunuz internete baÄŸlanır + baÄŸlanmaz, Uygulama bu karma kod numarasını sunucu sistemine iletir. Bunun üzerine sunucu + sistemi bir dijital eriÅŸim kodu, yani bir belirteç (token) sunar ve bu belirteç Uygulamaya + kaydedilir. Belirteç, sunucu sistemindeki karma kod numarasına baÄŸlıdır. Uygulama daha sonra + akıllı telefonunuzdaki karma kod numarasını siler ve sadece belirteci tutar. Böylece QR kod + artık kullanılmış (geçersiz) olur, yani artık kimse tarafından kullanılamaz. Bu sayede QR + kodunuzun test sonucunu sorgulamak için baÅŸka bir kullanıcı tarafından kullanılmasının önüne + geçilmiÅŸ olur. + </p> <p> <u>Test sonucunun saklanması</u> </p> <p> - Test sonucunuz test laboratuvarında hazır olduÄŸunda, bu sonucu hemen ilgili - karma kod numarası ile birlikte RKI tarafından iÅŸletilen test sonuçları - veri tabanına kaydedilmek üzere gönderir. Test sonuçları veri tabanı, - sunucu sistemi içinde ayrı bir sunucuda bulunur. Test laboratuvarı, aynı - matematik yöntemini kullanarak, size verilen QR kodda yer alan kod numarası + Test sonucunuz test kuruluÅŸları tarafından hazır olduÄŸunda, bu sonucu hemen ilgili karma kod + numarası ile birlikte RKI tarafından iÅŸletilen test sonuçları veri tabanına kaydedilmek üzere + gönderir. Test sonuçları veri tabanı, sunucu sistemi içinde ayrı bir sunucuda bulunur. Test + kuruluÅŸu, aynı matematik yöntemini kullanarak, size verilen QR kodda yer alan kod numarası bazında karma kod numarasını oluÅŸturur. </p> <p> @@ -546,14 +556,44 @@ TAN’ın bir kopyası sunucu sisteminde kalır. </p> <h2> - c. DiÄŸerlerini uyarın + c. Hızlı test sonucunun kanıtı +</h2> +<p> + Hızlı antjen testi sonucunu açarsanız ve test kuruluÅŸunda negatif test sonucu durumunda sonucun + isminizle görüntülenmesi seçeneÄŸini seçerseniz negatif test bulgusu isminizle, doÄŸum tarihinizle + ve testin yapılma zamanıyla birlikte görüntülenir. Uygulama bunun için kara kodunun taranması + sırasında okunan ilgili hızlı test verilerini kullanır. Hızlı test veriler, Uygulamada negatif + hızlı test sonuçları gösterilmediÄŸi anda silinir. +</p> +<p> + Uygulamada gösterilen test sonucunu hızlı testin sonucunun negatif olduÄŸunu kanıtlamak için + gerekirse üçüncü kiÅŸilere gösterebilirsiniz. Bunun için lütfen münferit durumda dijital test + kanıtının kabul edilmesi için hangi taleplerin saÄŸlanması gerektiÄŸi hakkında bilgi edininiz. + Dikkate alınması gerekenler: +</p> +<ul> + <li>RKI görüntülenen hızlı test sonuçlarının sizden test kanıtı talep eden veya edebilen (örn. + iÅŸ yerleri, iÅŸverenler) yetkili makamlar ve diÄŸer yetkili merkezler tarafından kabul + edileceÄŸine iliÅŸkin garanti vermez. + </li> + <li>Uygulamanın kanıtlama fonksiyonunu kullanmakla yükümlü deÄŸilsiniz. Test sonucunuzu üçüncü + kiÅŸilere karşı kanıtlamak zorundaysanız, kanıtı yasal (Federal eyaletlere özel olarak) + talimatlar kapsamında baÅŸka ÅŸekillerde de ibraz edebilirsiniz. + </li> +</ul> +<p> + Sonucu pozitif çıkan test açılırken test isminizle görüntülenmez. Bu durumda adınız ve soyadınız + Uygulamadan hemen silinir. DiÄŸer hızlı test verileriniz Uygulamada poztif çıkan hızlı test + sonucu görüntülenmediÄŸi anda (kodlar, test zamanı) silinir. +</p> +<h2> + d. DiÄŸerlerini uyarın </h2> <p> - Korona testiniz pozitif çıkmışsa ve siz rastgele kimlik numaralarınızı - Uygulama aracılığıyla paylaşırsanız, bu Uygulamanın veya diÄŸer resmi Korona - uygulamalarının kullanıcılarıyla bir karşılaÅŸma söz konusu olduÄŸunda, bu - kiÅŸilerin uyarılması saÄŸlanır. Bu durumda, Uygulama ÅŸu verileri sunucu - sistemine iletir: + Korona testiniz pozitif çıkmışsa ve siz rastgele kimlik numaralarınızı Uygulama aracılığıyla + paylaşırsanız, kullanıcılarla bir karşılaÅŸma söz konusu olduÄŸunda, bu kiÅŸilerin uyarılması + saÄŸlanır. Bunun yanı sıra sizinle eÅŸ zamanlı aynı olaylar veya konumlarda giriÅŸ denetimi + yaptıran kullanıcılar uyarılır. Bu durumda, Uygulama ÅŸu verileri sunucu sistemine iletir: </p> <ul> <li> @@ -573,15 +613,11 @@ <p> Test sonuçlarınız sunucu sistemine aktarılmadan (daha doÄŸrusu, rastgele kimlik numaralarınız, olay kimlikleriniz ve kaydedilen giriÅŸ ve çıkış denetim zamanlarınız aktarılmadan) önce, - Uygulama, verilere bir taşıma - riski deÄŸeri ve gerçekleÅŸtirilen test tipi hakkında bilgi ekler. - Uygulamanın uyarı iÅŸlevi, sadece laboratuvarda onaylanan test sonuçlarında - kullanılabildiÄŸinden, test tipi tüm kullanıcılar için aynıdır. Taşıma riski - deÄŸeri, 14 günlük dönemin her bir günündeki enfeksiyon olasılığının bir - tahmini deÄŸeridir. Enfeksiyon olasılığı, enfeksiyonun süresine ve seyrine - baÄŸlı olduÄŸundan, örneÄŸin bir maruz kalma gününde, semptomların - baÅŸlangıcından bu yana ne kadar zaman geçmiÅŸse, o gün enfeksiyon riski o - kadar düşük olur. Bu ek taşıma riski deÄŸerleri, diÄŸer kullanıcılar için + Uygulama, verilere bir taşıma riski deÄŸeri ve gerçekleÅŸtirilen test tipi hakkında bilgi ekler. + Taşıma riski deÄŸeri, 14 günlük dönemin her bir günündeki enfeksiyon olasılığının bir tahmini + deÄŸeridir. Enfeksiyon olasılığı, enfeksiyonun süresine ve seyrine baÄŸlı olduÄŸundan, örneÄŸin bir + maruz kalma gününde, semptomların baÅŸlangıcından bu yana ne kadar zaman geçmiÅŸse, o gün + enfeksiyon riski o kadar düşük olur. Bu ek taşıma riski deÄŸerleri, diÄŸer kullanıcılar için enfeksiyon olasılığının daha kesin bir ÅŸekilde belirlenmesini mümkün kılar. </p> <p> @@ -596,23 +632,24 @@ <u>Uygulama üzerinden test sonucunuzu almadıysanız:</u> </p> <p> - Pozitif test sonucunuzu Uygulama üzerinden çağırmamış olsanız bile, diÄŸer insanları - uyarabilirsiniz. Bunun için, “TAN iste†prosedürünü seçin. Bunun üzerine Uygulama sizden yardım - hattını aramanızı ister. Orada bir yardım hattı çalışanı, Korona testinizin gerçekten pozitif - çıktığından emin olmak için size birkaç soru soracaktır. Bunun amacı, yanlış uyarıların - istemeden veya kasıtlı olarak tetiklenmesini önlemektir. Bu soruları doÄŸru yanıtladıktan sonra, - size cep telefonu / telefon numaranız ve adınız sorulacaktır. Bu, daha sonra sizi telefonla - arayarak, Uygulamaya girmeniz gereken ve bir benzersiz TAN kodu size bildirmek içindir. Cep - telefonunuz / telefon numaranız ve adınız, yalnızca bu süreçte geçici olarak kaydedilecek ve en - geç bir saat içinde silinecektir. Aramanızdan hemen sonra, yardım hattı çalışanı, sunucu - sistemindeki özel bir eriÅŸim yolu üzerinden benzersiz TAN’ınızı oluÅŸturacak ve bu konuda bilgi - vermek için sizi arayacaktır. Bir TAN yalnızca bir saat süreyle geçerli kalır ve dolayısıyla - size iletildikten hemen sonra, ancak en geç bir saat içinde yardım hattındaki sistemden silinir. - Uygulamaya geçerli bir TAN girildikten sonra, bu kod sunucu sistemine iletilir. TAN’ın kullanımı - sayesinde gerçekten pozitif bir test sonucu olup olmadığını kontrol etmek ve böylece yanlış - mesajları önlemek mümkün olur. Bunun ardından geçerli bir QR kodunu taranmasında olduÄŸu gibi - (Madde 6 b “Test sonuçlarının çaÄŸrılmasıâ€na bakın), Uygulama, sunucu sisteminden bir belirteç - alır. + Hızlı antijen testinizin sonucu pozitif çıkarsa test sonucunuzu Uygulama üzerinden açarsanız , + diÄŸer insanları uyarabilirsiniz. PCR test sonucu pozitif çıktığında yakınlarınızı test sonucunu + uygulamanın dışında alsanız dahi uyarabilirsiniz. Bunun için, “TAN iste†prosedürünü seçin. + Bunun üzerine Uygulama sizden yardım hattını aramanızı ister. Orada bir yardım hattı çalışanı, + Korona testinizin gerçekten pozitif çıktığından emin olmak için size birkaç soru soracaktır. + Bunun amacı, yanlış uyarıların istemeden veya kasıtlı olarak tetiklenmesini önlemektir. Bu + soruları doÄŸru yanıtladıktan sonra, size cep telefonu / telefon numaranız ve adınız + sorulacaktır. Bu, daha sonra sizi telefonla arayarak, Uygulamaya girmeniz gereken ve bir + benzersiz TAN kodu size bildirmek içindir. Cep telefonunuz / telefon numaranız ve adınız, + yalnızca bu süreçte geçici olarak kaydedilecek ve en geç bir saat içinde silinecektir. + Aramanızdan hemen sonra, yardım hattı çalışanı, sunucu sistemindeki özel bir eriÅŸim yolu + üzerinden benzersiz TAN’ınızı oluÅŸturacak ve bu konuda bilgi vermek için sizi arayacaktır. Bir + TAN yalnızca bir saat süreyle geçerli kalır ve dolayısıyla size iletildikten hemen sonra, ancak + en geç bir saat içinde yardım hattındaki sistemden silinir. Uygulamaya geçerli bir TAN + girildikten sonra, bu kod sunucu sistemine iletilir. TAN’ın kullanımı sayesinde gerçekten + pozitif bir test sonucu olup olmadığını kontrol etmek ve böylece yanlış mesajları önlemek mümkün + olur. Bunun ardından geçerli bir QR kodunu taranmasında olduÄŸu gibi (Madde 6 b “Test + sonuçlarının çaÄŸrılmasıâ€na bakın), Uygulama, sunucu sisteminden bir belirteç alır. </p> <p> Bazı nadir durumlarda, verdiÄŸiniz bir uyarının, kiÅŸisel çevrenizde bu Uygulamayı kullanan ve @@ -622,7 +659,7 @@ olabilir. </p> <h2> - d. Uygulamanın bilgilenme amaçlı kullanımı + e. Uygulamanın bilgilenme amaçlı kullanımı </h2> <p> Uygulama otomatik olarak sunucu sistemi üzerinden günlük istatistikleri @@ -633,7 +670,7 @@ web sitesinin ilgili saÄŸlayıcısı tarafından belirlenmektedir. </p> <h2> - e. Temas güncesi + f. Temas güncesi </h2> <p> Temas güncesi, Uygulamanın ek bir iÅŸlevidir. Temas güncesindeki veriler, bir hatırlatma görevi @@ -649,7 +686,7 @@ enfeksiyonların önüne geçebilir. </p> <h2> - f. Veri bağışı + g. Veri bağışı </h2> <p> Veri bağışı, Uygulamanın ek bir iÅŸlevidir. Veri bağışı kapsamında RKI’ye aktarılan kullanım @@ -690,7 +727,7 @@ öğrenmez. </p> <h2> - g. Anketler + h. Anketler </h2> <p> Anketler, Uygulama dışında yönlendirileceÄŸiniz bir web sitesinde gerçekleÅŸtirilir. Uygulama, @@ -698,7 +735,7 @@ amaçları, anket web sitesinde anketteki bilgilerde yer almaktadır. </p> <h2> - h. Uygulamanızın orijinalliÄŸinin doÄŸrulanması + i. Uygulamanızın orijinalliÄŸinin doÄŸrulanması </h2> <p> Uygulamanızın orijinalliÄŸinin doÄŸrulanması için akıllı telefonunuzun iÅŸletim sisteminin bir @@ -731,7 +768,10 @@ <p> Veri deÄŸiÅŸim sunucularına baÄŸlı Korona uygulamalarının ulusal sunucu sistemleri, kendi pozitif listelerini düzenli olarak veri deÄŸiÅŸim sunucularına iletir ve onlardan diÄŸer ülkelerin pozitif - listelerini alır. Ancak olay kimlikleri veri deÄŸiÅŸim sunucusuna aktarılmaz. + listelerini alır. Ãœlkeler arası uyarılar sadece PCR testinin pozitif çıkması ve sadece KOVÄ°D-19 + bilgilendirme sistem tarafından kaydedilen görüşmeler nedeniyle tetiklenebilir. Olay kimlikleri + ve tesadüf kimlikleri hızlı antijen testinin pozitif çıkması nedeniyle deÄŸiÅŸim sunucusuna + aktarılmaz. </p> <p> Ä°lgili sunucu sistemi, aldığı pozitif listeleri kendi pozitif listesiyle birleÅŸtirir, bu sayede @@ -915,12 +955,15 @@ 10. Verileriniz kime aktarılır? </h1> <p> - Uygulama aracılığıyla diÄŸer kullanıcıları uyardığınızda, test sonucunuz son 14 güne ait rastgele - kimlik numaralarınız halinde ve semptomların baÅŸlangıcına iliÅŸkin isteÄŸe baÄŸlı bilgiler, veri - deÄŸiÅŸim sunucularına katılan ülkelerin sorumlu saÄŸlık kurumlarına gönderilir ve oradan da sınır - ötesi uyarı sistemine katılan resmi Korona uygulamalarının sunucu sistemlerine aktarılır. Ulusal - Korona uygulamalarının sunucu sistemleri, bu verileri pozitif listelerin bir ögesi olarak kendi - kullanıcılarına dağıtır. + Uygulama aracılığıyla diÄŸer kullanıcıları pozitif PCR testi nedeniyle uyardığınızda, test + sonucunuz son 14 güne ait rastgele kimlik numaralarınız halinde ve semptomların baÅŸlangıcına + iliÅŸkin isteÄŸe baÄŸlı bilgiler, veri deÄŸiÅŸim sunucularına katılan ülkelerin sorumlu saÄŸlık + kurumlarına gönderilir ve oradan da sınır ötesi uyarı sistemine katılan resmi Korona + uygulamalarının sunucu sistemlerine aktarılır. Ulusal Korona uygulamalarının sunucu sistemleri, + bu verileri pozitif listelerin bir ögesi olarak kendi kullanıcılarına dağıtır. Corona-Warn-App + kullanıcılarına olay kimlikleri yalnızca RKI sunucu sistemi aracılığıyla dağıtılır. Pozitif + antijen hızlı testi nedeniyle uyarı durumunda verileriniz deÄŸiÅŸim sunucusuna aktarılmaz. + </p> <p> @@ -945,21 +988,27 @@ 11. Verileriniz AB dışındaki ülkelere aktarılacak mı? </h1> <p> - Bir uyarıyı tetiklediÄŸinizde, rastgele kimlik numaralarınız, Ä°sviçre Federal SaÄŸlık Dairesi ile - birlikte RKI tarafından iÅŸletilen Ä°sviçre’deki veri deÄŸiÅŸim sunucusuna da gönderilir. AB, - Ä°sviçre veri koruma seviyesinin yeterliliÄŸinin belirlendiÄŸi bir yeterlilik kararı çıkarmıştır - (GVKT madde 45). Bundan baÅŸka güncel pozitif listeler, kullanıcının konumundan bağımsız olarak - (örneÄŸin tatilde veya iÅŸ gezisinde) çaÄŸrılır. Ayrıca, Uygulamanızın orijinalliÄŸinin doÄŸrulanması - kapsamında verilerin AB dışındaki bir ülkeye aktarılması söz konusu olabilir. Akıllı telefonunuz - tarafından oluÅŸturulan ve akıllı telefonunuzun ve Uygulamanın sürümüyle ilgili bilgileri içeren - kimlik kodu, akıllı telefonunuzun iÅŸletim sisteminin saÄŸlayıcısına (Apple veya Google) - aktarılır. Bu süreçte verilerin üçüncü ülkelere, özellikle ABD’ye aktarılması da söz konusu - olabilir. Orada muhtemelen Avrupa hukuku standartlarına uygun kiÅŸisel verilerin koruma seviyesi - bulunmayabilir ve Avrupa’daki veri koruma haklarınız uygulanmayabilir. Bu baÄŸlamda özellikle - üçüncü ülke güvenlik makamlarının iÅŸletim sistemi saÄŸlayıcısına aktarılan bu verilere eriÅŸme ve - örneÄŸin verileri diÄŸer bilgilerle iliÅŸkilendirerek bunları deÄŸerlendirmeye alma olasılığı - bulunmaktadır. Ancak bu, yalnızca aktarılan kimlik kodlarını etkiler. KarşılaÅŸma verileri gibi - Uygulamadaki diÄŸer veriler bu prosedüre dahil edilmez.</p> + Pozitif PCR testi nedeniyle uyarıyı tetiklediÄŸinizde, rastgele kimlik numaralarınız, Ä°sviçre + Federal SaÄŸlık Dairesi ile birlikte RKI tarafından iÅŸletilen Ä°sviçre’deki veri deÄŸiÅŸim + sunucusuna da gönderilir. Pozitif antijen hızlı testi nedeniyle uyarı yapılması durumunda + verilerinizin bu ÅŸekilde aktarılması gerçekleÅŸtirilmez. +</p> +<p> + AB, Ä°sviçre veri koruma seviyesinin yeterliliÄŸinin belirlendiÄŸi bir yeterlilik kararı + çıkarmıştır (GVKT madde 45). Bundan baÅŸka güncel pozitif listeler, kullanıcının konumundan + bağımsız olarak (örneÄŸin tatilde veya iÅŸ gezisinde) çaÄŸrılır. Ayrıca, Uygulamanızın + orijinalliÄŸinin doÄŸrulanması kapsamında verilerin AB dışındaki bir ülkeye aktarılması söz konusu + olabilir. Akıllı telefonunuz tarafından oluÅŸturulan ve akıllı telefonunuzun ve Uygulamanın + sürümüyle ilgili bilgileri içeren kimlik kodu, akıllı telefonunuzun iÅŸletim sisteminin + saÄŸlayıcısına (Apple veya Google) aktarılır. Bu süreçte verilerin üçüncü ülkelere, özellikle + ABD’ye aktarılması da söz konusu olabilir. Orada muhtemelen Avrupa hukuku standartlarına uygun + kiÅŸisel verilerin koruma seviyesi bulunmayabilir ve Avrupa’daki veri koruma haklarınız + uygulanmayabilir. Bu baÄŸlamda özellikle üçüncü ülke güvenlik makamlarının iÅŸletim sistemi + saÄŸlayıcısına aktarılan bu verilere eriÅŸme ve örneÄŸin verileri diÄŸer bilgilerle iliÅŸkilendirerek + bunları deÄŸerlendirmeye alma olasılığı bulunmaktadır. Ancak bu, yalnızca aktarılan kimlik + kodlarını etkiler. KarşılaÅŸma verileri gibi Uygulamadaki diÄŸer veriler bu prosedüre dahil + edilmez. +</p> <p> Ancak Uygulama tarafından aktarılan veriler, sadece Almanya’daki sunucularda veya bir diÄŸer AB (veya Avrupa Ekonomik Alanı) ülkesindeki sunucularda iÅŸlenir, dolayısıyla Genel Veri Koruma Tüzüğünün (GVKT) katı gerekliliklerine tabi @@ -1012,7 +1061,6 @@ olanak, diÄŸer kullanıcıları uyarmak için henüz rastgele kimlik numaralarınızı göndermediÄŸiniz sürece söz konusudur. </p> - <p> Rastgele kimlik numaralarınızı gönderdikten sonra, verdiÄŸiniz rıza beyanını geri almanın tek yolu artık Uygulamayı silmektir. Sunucu sistemine halen iletilmiÅŸ olan rastgele kimlik @@ -1122,5 +1170,5 @@ veya e-posta yoluyla: datenschutz@rki.de. </p> <p> - Baskı 16.04.2021 + Baskı 30.04.2021 </p> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt index 61a3c11368011a9e30387a568b32cbe9e4661fb5..b0c270e62abf366fd24bf2dcf23f00e0d5fe95c9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt @@ -40,10 +40,8 @@ import de.rki.coronawarnapp.util.device.ForegroundState import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.ApplicationComponent import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking import org.conscrypt.Conscrypt import timber.log.Timber import java.security.Security @@ -114,17 +112,12 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { .launchIn(GlobalScope) if (onboardingSettings.isOnboarded) { - // TODO this is on the main thread, not very nice... - runBlocking { - val isAllowedToSubmitKeys = coronaTestRepository.coronaTests.first().any { it.isSubmissionAllowed } - if (!isAllowedToSubmitKeys) { - deadmanNotificationScheduler.schedulePeriodic() - } - } - contactDiaryWorkScheduler.schedulePeriodic() } + Timber.v("Setting up deadman notification scheduler") + deadmanNotificationScheduler.setup() + Timber.v("Setting up risk work schedulers.") exposureWindowRiskWorkScheduler.setup() presenceTracingRiskWorkScheduler.setup() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CoronaTestConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CoronaTestConfig.kt index 54c0ff93229178b39104338d8f5dacbf94f78c24..103a51ca46d67eca897a299b94272534f4f0dadd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CoronaTestConfig.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CoronaTestConfig.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.appconfig import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper +import org.joda.time.Duration interface CoronaTestConfig { val coronaRapidAntigenTestParameters: CoronaRapidAntigenTestParametersContainer @@ -9,7 +10,7 @@ interface CoronaTestConfig { } data class CoronaRapidAntigenTestParametersContainer( - val hoursToDeemTestOutdated: Long = DEFAULT_HOURS + val hoursToDeemTestOutdated: Duration = Duration.standardHours(DEFAULT_HOURS) ) { companion object { const val DEFAULT_HOURS: Long = 48 diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CoronaTestConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CoronaTestConfigMapper.kt index d7025379b976024313e0e1ab2f116b830cef5991..5d07b483db522fd67b059af632cd437706a22351 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CoronaTestConfigMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CoronaTestConfigMapper.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.appconfig.CoronaRapidAntigenTestParametersContainer import de.rki.coronawarnapp.appconfig.CoronaTestConfig import de.rki.coronawarnapp.appconfig.CoronaTestConfigContainer import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid.ApplicationConfigurationAndroid +import org.joda.time.Duration import timber.log.Timber import javax.inject.Inject @@ -25,7 +26,9 @@ class CoronaTestConfigMapper @Inject constructor() : CoronaTestConfig.Mapper { private fun ApplicationConfigurationAndroid.mapCoronaTestParameters(): CoronaTestConfig { val coronaRapidAntigenTestParameters = if (coronaTestParameters.hasCoronaRapidAntigenTestParameters()) { CoronaRapidAntigenTestParametersContainer( - coronaTestParameters.coronaRapidAntigenTestParameters.hoursToDeemTestOutdated.toLong() + Duration.standardHours( + coronaTestParameters.coronaRapidAntigenTestParameters.hoursToDeemTestOutdated.toLong() + ) ) } else { Timber.d("coronaRapidAntigenTestParameters is missing") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt index 02bbd1094ab780bca94d6ccff43a31e8555f7bae..df07ac03d4ea7e926c5a414586ec28fbdd7dfbe6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt @@ -79,7 +79,15 @@ class AppConfigServer @Inject constructor( val cacheControl = CacheControl.parse(headers) - val maxCacheAge = Duration.standardSeconds(cacheControl.maxAgeSeconds.toLong()) + val maxCacheAge = cacheControl.maxAgeSeconds.let { + if (it == 0) { + // Server currently returns `Cache-Control max-age=0, no-cache, no-store` which breaks our caching + Timber.tag(TAG).w("Server returned max-age=0: %s", cacheControl) + Duration.standardSeconds(300) + } else { + Duration.standardSeconds(it.toLong()) + } + } return InternalConfigData( rawData = rawConfig, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/exceptions/MappedException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/exceptions/MappedException.kt new file mode 100644 index 0000000000000000000000000000000000000000..dcff207f6139917379e8d999519175bc1e5b8284 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/exceptions/MappedException.kt @@ -0,0 +1,29 @@ +package de.rki.coronawarnapp.bugreporting.exceptions + +import android.content.Context +import com.google.android.gms.common.api.ApiException +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.util.HumanReadableError + +interface KnownThrowable { + fun matches(throwable: Throwable): Boolean + + fun getReadableError(context: Context): HumanReadableError +} + +enum class MappedException : KnownThrowable { + // ApiException with StatusCode 17 + ENS_NOT_INSTALLED { + override fun matches(throwable: Throwable): Boolean = throwable is ApiException && throwable.statusCode == 17 + + override fun getReadableError(context: Context): HumanReadableError = HumanReadableError( + title = "${context.getString(R.string.errors_generic_details_headline)}: 3\n" + + context.getString(R.string.errors_generic_headline), + description = context.getString(R.string.errors_google_update_needed) + ) + }, +} + +fun Throwable.findKnownError(context: Context): HumanReadableError? { + return MappedException.values().find { it.matches(this@findKnownError) }?.getReadableError(context) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..49e16cef4548fbb8969147575de3e0042c78dd89 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt @@ -0,0 +1,61 @@ +package de.rki.coronawarnapp.bugreporting.ui + +import android.content.Context +import android.text.SpannableString +import android.text.method.LinkMovementMethod +import android.text.util.Linkify +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.util.ContextExtensions.getColorStateListCompat +import de.rki.coronawarnapp.util.tryHumanReadableError +import java.util.regex.Pattern + +private fun MaterialAlertDialogBuilder.setMessageView( + message: String, + textHasLinks: Boolean, +) { + // create spannable and add links, removed stack trace links into nowhere + val spannable = SpannableString(message) + val httpPattern: Pattern = Pattern.compile("[a-z]+://[^ \\n]*") + Linkify.addLinks(spannable, httpPattern, "") + + val paddingStartEnd = context.resources.getDimension(R.dimen.spacing_normal).toInt() + val paddingLeftRight = context.resources.getDimension(R.dimen.spacing_small).toInt() + + val textView = TextView(context).apply { + text = spannable + linksClickable = true + movementMethod = LinkMovementMethod.getInstance() + setPadding( + paddingStartEnd, + paddingLeftRight, + paddingStartEnd, + paddingLeftRight + ) + setTextAppearance(R.style.body1) + setLinkTextColor(context.getColorStateListCompat(R.color.button_primary)) + setTextIsSelectable(!textHasLinks) + } + setView(textView) +} + +fun Throwable.toErrorDialogBuilder(context: Context) = MaterialAlertDialogBuilder(context).apply { + val error = this@toErrorDialogBuilder + val humanReadable = error.tryHumanReadableError(context) + + setTitle(humanReadable.title ?: context.getString(R.string.errors_generic_headline_short)) + setMessageView(humanReadable.description, textHasLinks = true) + + setPositiveButton(R.string.errors_generic_button_positive) { _, _ -> } + + setNeutralButton(R.string.errors_generic_button_negative) { _, _ -> + AlertDialog.Builder(context).apply { + setMessageView( + error.toString() + "\n\n" + error.stackTraceToString(), + textHasLinks = false + ) + }.show() + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/onboarding/ContactDiaryOnboardingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/onboarding/ContactDiaryOnboardingFragment.kt index 3155f696f2dc4ed38b301836766f82adb3c1515e..eabc87c544c1e73ae5587ecda4f0ea81f134a465 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/onboarding/ContactDiaryOnboardingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/onboarding/ContactDiaryOnboardingFragment.kt @@ -7,6 +7,7 @@ import androidx.core.net.toUri import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.google.android.material.transition.MaterialSharedAxis import de.rki.coronawarnapp.R import de.rki.coronawarnapp.contactdiary.ui.ContactDiarySettings import de.rki.coronawarnapp.databinding.ContactDiaryOnboardingFragmentBinding @@ -32,6 +33,10 @@ class ContactDiaryOnboardingFragment : Fragment(R.layout.contact_diary_onboardin override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + binding.apply { contactDiaryOnboardingNextButton.setOnClickListener { vm.onNextButtonClick() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewFragment.kt index 74c6aa6b9eff4319acba474286d65dd70f1bbe81..d08d046dac2fc4fc162f94e3bf7e2bad324a3da4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewFragment.kt @@ -6,6 +6,7 @@ import android.view.accessibility.AccessibilityEvent import androidx.appcompat.widget.Toolbar import androidx.core.app.ShareCompat import androidx.fragment.app.Fragment +import com.google.android.material.transition.MaterialSharedAxis import de.rki.coronawarnapp.R import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.DiaryOverviewAdapter import de.rki.coronawarnapp.contactdiary.util.MarginRecyclerViewDecoration @@ -26,6 +27,13 @@ class ContactDiaryOverviewFragment : Fragment(R.layout.contact_diary_overview_fr private val vm: ContactDiaryOverviewViewModel by cwaViewModels { viewModelFactory } private val binding: ContactDiaryOverviewFragmentBinding by viewBindingLazy() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt index 2921477f4d0bbddf6ae3ea9a7041d0df92055cd8..3e35ebfa07e11b1194ca6d8a214f785906ef1b10 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.coronatest import androidx.annotation.VisibleForTesting import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.coronatest.errors.CoronaTestNotFoundException import de.rki.coronawarnapp.coronatest.migration.PCRTestMigration import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode @@ -123,7 +124,7 @@ class CoronaTestRepository @Inject constructor( internalData.updateBlocking { val toBeRemoved = values.singleOrNull { it.identifier == identifier } - ?: throw IllegalArgumentException("No found for $identifier") + ?: throw CoronaTestNotFoundException("No found for $identifier") getProcessor(toBeRemoved.type).onRemove(toBeRemoved) @@ -236,7 +237,7 @@ class CoronaTestRepository @Inject constructor( ) { internalData.updateBlocking { val original = values.singleOrNull { it.identifier == identifier } - ?: throw IllegalArgumentException("No found for $identifier") + ?: throw CoronaTestNotFoundException("No test found for $identifier") val processor = getProcessor(original.type) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt index b7a7d6ae325b0532a13dd3e4af231c21120d5070..e84382983255ea28c965c417216bc1c86cb9c59a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt @@ -5,7 +5,10 @@ import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import timber.log.Timber val CoronaTestRepository.latestPCRT: Flow<PCRCoronaTest?> get() = this.coronaTests @@ -32,3 +35,27 @@ val CoronaTestRepository.isRiskCalculationNecessary: Flow<Boolean> get() = coronaTests.map { tests -> tests.none { it.isPositive } } + +// This in memory to not show duplicate errors +// CoronaTest.lastError is also only in memory +private val consumedErrors = mutableMapOf<String, Throwable?>() + +val CoronaTestRepository.testErrorsSingleEvent: Flow<List<CoronaTest>> + get() = coronaTests + .map { tests -> + tests + .filter { + val consumedClass = consumedErrors[it.identifier]?.javaClass + consumedClass != it.lastError?.javaClass + } + .onEach { + Timber.v("Unconsumed error for %s: %s", it.identifier, it.lastError?.toString()) + consumedErrors[it.identifier] = it.lastError + } + .filter { it.lastError != null } + } + .flatMapMerge { tests -> + // First we emit the tests with errors + // then an empty list because the errors should only be displayed once + flowOf(tests, emptyList()) + } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/errors/CoronaTestNotFoundException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/errors/CoronaTestNotFoundException.kt new file mode 100644 index 0000000000000000000000000000000000000000..5b0b92c1131c40252541088f50f3c530d04cf8b1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/errors/CoronaTestNotFoundException.kt @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.coronatest.errors + +class CoronaTestNotFoundException( + message: String +) : IllegalArgumentException(message) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotification.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotification.kt index 3cebace1c18fa97124d78c0cb557b9120764a2d8..8c2a92737f1825e4b682066390375bdf4af833ee 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotification.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotification.kt @@ -1,9 +1,11 @@ package de.rki.coronawarnapp.coronatest.notification import android.content.Context +import android.os.Bundle import androidx.navigation.NavDeepLinkBuilder import dagger.Reusable import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.notification.GeneralNotifications import de.rki.coronawarnapp.notification.NotificationConstants.POSITIVE_RESULT_NOTIFICATION_ID import de.rki.coronawarnapp.notification.NotificationConstants.POSITIVE_RESULT_NOTIFICATION_INITIAL_OFFSET @@ -22,20 +24,25 @@ class ShareTestResultNotification @Inject constructor( private val notificationHelper: GeneralNotifications, ) { - fun scheduleSharePositiveTestResultReminder() { + fun scheduleSharePositiveTestResultReminder(testType: CoronaTest.Type) { notificationHelper.scheduleRepeatingNotification( + testType, timeStamper.nowUTC.plus(POSITIVE_RESULT_NOTIFICATION_INITIAL_OFFSET), POSITIVE_RESULT_NOTIFICATION_INTERVAL, POSITIVE_RESULT_NOTIFICATION_ID ) } - fun showSharePositiveTestResultNotification(notificationId: Int) { + fun showSharePositiveTestResultNotification(notificationId: Int, testType: CoronaTest.Type) { Timber.d("showSharePositiveTestResultNotification(notificationId=$notificationId)") + + val args = Bundle().apply { putSerializable("testType", testType) } + val pendingIntent = NavDeepLinkBuilder(context) .setGraph(R.navigation.nav_graph) .setComponentName(MainActivity::class.java) .setDestination(R.id.submissionTestResultAvailableFragment) + .setArguments(args) .createPendingIntent() val notification = notificationHelper.newBaseBuilder().apply { @@ -50,8 +57,8 @@ class ShareTestResultNotification @Inject constructor( ) } - fun cancelSharePositiveTestResultNotification() { - notificationHelper.cancelFutureNotifications(POSITIVE_RESULT_NOTIFICATION_ID) + fun cancelSharePositiveTestResultNotification(testType: CoronaTest.Type) { + notificationHelper.cancelFutureNotifications(POSITIVE_RESULT_NOTIFICATION_ID, testType) Timber.v("Future positive test result notifications have been canceled") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotificationService.kt index dddb09c4b7b328f9304001911108288c0bfd907f..155b74a704e991dc9f1efaefde9105e0383e2660 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotificationService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotificationService.kt @@ -1,6 +1,11 @@ package de.rki.coronawarnapp.coronatest.notification import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.latestPCRT +import de.rki.coronawarnapp.coronatest.latestRAT +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR +import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.RAPID_ANTIGEN import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.notification.NotificationConstants.POSITIVE_RESULT_NOTIFICATION_TOTAL_COUNT import de.rki.coronawarnapp.util.coroutine.AppScope @@ -22,47 +27,98 @@ class ShareTestResultNotificationService @Inject constructor( fun setup() { Timber.d("setup()") + coronaTestRepository.coronaTests .onEach { tests -> - when { - tests.any { it.isSubmissionAllowed } -> { - maybeScheduleSharePositiveTestResultReminder() - } - tests.isNotEmpty() -> { - notification.cancelSharePositiveTestResultNotification() - } - tests.isEmpty() -> { - resetSharePositiveTestResultNotification() - } + + // schedule reminder if test wasn't submitted + tests.filter { test -> + test.isSubmissionAllowed && !test.isSubmitted + }.forEach { test -> + maybeScheduleSharePositiveTestResultReminder(test.type) } + + // cancel the reminder when test is submitted + tests + .filter { it.isSubmitted } + .forEach { notification.cancelSharePositiveTestResultNotification(it.type) } } .catch { Timber.e(it, "Failed to schedule positive test result reminder.") } .launchIn(appScope) + + // if no PCR test is stored or if it was deleted, we reset the reminder + coronaTestRepository.latestPCRT + .onEach { + if (it == null) { + resetSharePositiveTestResultNotification(PCR) + } + } + .catch { Timber.e(it, "Failed to reset positive test result reminder for PCR test.") } + .launchIn(appScope) + + // if no RAT test is stored or if it was deleted, we reset the reminder + coronaTestRepository.latestRAT + .onEach { + if (it == null) { + resetSharePositiveTestResultNotification(RAPID_ANTIGEN) + } + } + .catch { Timber.e(it, "Failed to reset positive test result reminder for RAT test.") } + .launchIn(appScope) } - fun maybeShowSharePositiveTestResultNotification(notificationId: Int) { + fun maybeShowSharePositiveTestResultNotification(notificationId: Int, testType: CoronaTest.Type) { Timber.d("maybeShowSharePositiveTestResultNotification(notificationId=$notificationId)") - if (cwaSettings.numberOfRemainingSharePositiveTestResultReminders > 0) { - cwaSettings.numberOfRemainingSharePositiveTestResultReminders -= 1 - notification.showSharePositiveTestResultNotification(notificationId) - } else { - notification.cancelSharePositiveTestResultNotification() + if (testType == PCR) { + if (cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr > 0) { + cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr -= 1 + notification.showSharePositiveTestResultNotification(notificationId, testType) + } else { + notification.cancelSharePositiveTestResultNotification(testType) + } + } else if (testType == RAPID_ANTIGEN) { + if (cwaSettings.numberOfRemainingSharePositiveTestResultRemindersRat > 0) { + cwaSettings.numberOfRemainingSharePositiveTestResultRemindersRat -= 1 + notification.showSharePositiveTestResultNotification(notificationId, testType) + } else { + notification.cancelSharePositiveTestResultNotification(testType) + } } } - private fun maybeScheduleSharePositiveTestResultReminder() { - if (cwaSettings.numberOfRemainingSharePositiveTestResultReminders < 0) { - Timber.v("Schedule positive test result notification") - cwaSettings.numberOfRemainingSharePositiveTestResultReminders = POSITIVE_RESULT_NOTIFICATION_TOTAL_COUNT - notification.scheduleSharePositiveTestResultReminder() - } else { - Timber.v("Positive test result notification has already been scheduled") + private fun maybeScheduleSharePositiveTestResultReminder(testType: CoronaTest.Type) { + when (testType) { + PCR -> { + if (cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr < 0) { + Timber.v("Schedule positive test result notification for PCR test") + cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr = + POSITIVE_RESULT_NOTIFICATION_TOTAL_COUNT + notification.scheduleSharePositiveTestResultReminder(testType) + } else { + Timber.v("Positive test result notification for PCR test has already been scheduled") + } + } + RAPID_ANTIGEN -> { + if (cwaSettings.numberOfRemainingSharePositiveTestResultRemindersRat < 0) { + Timber.v("Schedule positive test result notification for RAT test") + cwaSettings.numberOfRemainingSharePositiveTestResultRemindersRat = + POSITIVE_RESULT_NOTIFICATION_TOTAL_COUNT + notification.scheduleSharePositiveTestResultReminder(testType) + } else { + Timber.v("Positive test result notification for RAT test has already been scheduled") + } + } } } - private fun resetSharePositiveTestResultNotification() { - notification.cancelSharePositiveTestResultNotification() - cwaSettings.numberOfRemainingSharePositiveTestResultReminders = Int.MIN_VALUE - Timber.v("Positive test result notification counter has been reset") + private fun resetSharePositiveTestResultNotification(testType: CoronaTest.Type) { + notification.cancelSharePositiveTestResultNotification(testType) + + when (testType) { + PCR -> cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr = Int.MIN_VALUE + RAPID_ANTIGEN -> cwaSettings.numberOfRemainingSharePositiveTestResultRemindersRat = Int.MIN_VALUE + } + + Timber.v("Share positive test result notification counter has been reset for all test types") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt index 651c5721e2244a230dd33447a5f6f0896b014279..9a5c1801af361fe74a511694f49ccfa804c94e82 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt @@ -34,9 +34,11 @@ sealed class CoronaTestQRCode : Parcelable, TestRegistrationRequest { data class RapidAntigen( val hash: RapidAntigenHash, val createdAt: Instant, - val firstName: String?, - val lastName: String?, - val dateOfBirth: LocalDate?, + val firstName: String? = null, + val lastName: String? = null, + val dateOfBirth: LocalDate? = null, + val testid: String? = null, + val salt: String? = null ) : CoronaTestQRCode() { @IgnoredOnParcel diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt index f74aa2d8736ef1f73adba4bf0fead09fe931f3f7..c1a95544ce0b759c88f7dc2f792d35c17cbcd194 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.coronatest.qrcode import com.google.common.io.BaseEncoding import com.google.gson.Gson import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.util.HashExtensions.toSHA256 import de.rki.coronawarnapp.util.hashing.isSha256Hash import de.rki.coronawarnapp.util.serialization.fromJson import okio.internal.commonToUtf8String @@ -21,14 +22,16 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona Timber.v("extract(rawString=%s)", rawString) val payload = CleanPayload(extractData(rawString)) - payload.requireValidPersonalData() + payload.requireValidData() return CoronaTestQRCode.RapidAntigen( hash = payload.hash, createdAt = payload.createdAt, firstName = payload.firstName, lastName = payload.lastName, - dateOfBirth = payload.dateOfBirth + dateOfBirth = payload.dateOfBirth, + testid = payload.testId, + salt = payload.salt ) } @@ -68,7 +71,9 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona @SerializedName("timestamp") val timestamp: Long?, @SerializedName("fn") val firstName: String?, @SerializedName("ln") val lastName: String?, - @SerializedName("dob") val dateOfBirth: String? + @SerializedName("dob") val dateOfBirth: String?, + @SerializedName("testid") val testid: String?, + @SerializedName("salt") val salt: String? ) private data class CleanPayload(val raw: RawPayload) { @@ -104,7 +109,20 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona } } - fun requireValidPersonalData() { + val testId: String? by lazy { + if (raw.testid.isNullOrEmpty()) null else raw.testid + } + + val salt: String? by lazy { + if (raw.salt.isNullOrEmpty()) null else raw.salt + } + + fun requireValidData() { + requireValidPersonalData() + requireValidHash() + } + + private fun requireValidPersonalData() { val allOrNothing = listOf( firstName != null, lastName != null, @@ -113,6 +131,16 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona val complete = allOrNothing.all { it } || allOrNothing.all { !it } if (!complete) throw InvalidQRCodeException("QRCode contains incomplete personal data: $raw") } + + private fun requireValidHash() { + val isQrCodeWithPersonalData = firstName != null && lastName != null && dateOfBirth != null + val generatedHash = + "${raw.dateOfBirth}#${raw.firstName}#${raw.lastName}#${raw.timestamp}#${raw.testid}#${raw.salt}" + .toSHA256() + if (isQrCodeWithPersonalData && !generatedHash.equals(hash, true)) { + throw InvalidQRCodeException("Generated hash doesn't match QRCode hash") + } + } } companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestResultAvailableNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestResultAvailableNotificationService.kt index b6e85bebc655114e7f1b5c25aea02dd645f9c43e..48685eab773107a54f0fa7cc209163e4adaa8108 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestResultAvailableNotificationService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestResultAvailableNotificationService.kt @@ -22,18 +22,19 @@ open class TestResultAvailableNotificationService( private val notificationHelper: GeneralNotifications, private val cwaSettings: CWASettings, private val notificationId: NotificationId, + private val logTag: String, ) { suspend fun showTestResultAvailableNotification(test: CoronaTest) { - Timber.d("showTestResultAvailableNotification(test=%s)", test) + Timber.tag(logTag).v("showTestResultAvailableNotification(test=%s)", test) if (foregroundState.isInForeground.first()) { - Timber.d("App in foreground, skipping notification.") + Timber.tag(logTag).d("App in foreground, skipping notification.") return } if (!cwaSettings.isNotificationsTestEnabled.value) { - Timber.i("Don't show test result available notification because user doesn't want to be informed") + Timber.tag(logTag).i("User has disabled test result notifications.") return } @@ -63,7 +64,7 @@ open class TestResultAvailableNotificationService( setContentIntent(pendingIntent) }.build() - Timber.i("Showing TestResultAvailable notification!") + Timber.tag(logTag).i("Showing test result notification($notificationId) for %s", test) notificationHelper.sendNotification( notificationId = notificationId, notification = notification, @@ -71,6 +72,7 @@ open class TestResultAvailableNotificationService( } fun cancelTestResultAvailableNotification() { + Timber.tag(logTag).i("Canceling test result notification($notificationId)") notificationHelper.cancelCurrentNotification(notificationId) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensions.kt index 7ea0ce6d7a53ffae6f3e3b4790cc7c5c084bc3fd..9d7c69f3701fa452dac5bfbc577a7bcdc64565c3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensions.kt @@ -13,13 +13,13 @@ import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.TestNegative import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.TestPending import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.TestPositive import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.TestResultReady -import de.rki.coronawarnapp.exception.http.CwaServerError +import de.rki.coronawarnapp.exception.http.BadRequestException fun PCRCoronaTest?.toSubmissionState() = when { this == null -> NoTest isSubmitted -> SubmissionStatePCR.SubmissionDone(testRegisteredAt = registeredAt) isProcessing -> FetchingResult - lastError != null -> if (lastError is CwaServerError) TestPending else TestInvalid + lastError is BadRequestException -> TestInvalid else -> when (state) { INVALID -> TestError POSITIVE -> when { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt index 88cd240a69b8b3b32f85299bd645dc620953c6b6..9c01a64e92c3555b3bb0e79c6bae69fe73345203 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt @@ -20,7 +20,6 @@ import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor import de.rki.coronawarnapp.coronatest.type.CoronaTestService import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest.TestResultDataCollector -import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.exception.reporting.report @@ -36,8 +35,7 @@ class PCRProcessor @Inject constructor( private val timeStamper: TimeStamper, private val submissionService: CoronaTestService, private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, - private val testResultDataCollector: TestResultDataCollector, - private val deadmanNotificationScheduler: DeadmanNotificationScheduler, + private val testResultDataCollector: TestResultDataCollector ) : CoronaTestProcessor { override val type: CoronaTest.Type = CoronaTest.Type.PCR @@ -46,7 +44,9 @@ class PCRProcessor @Inject constructor( Timber.tag(TAG).d("create(data=%s)", request) request as CoronaTestQRCode.PCR - val registrationData = submissionService.asyncRegisterDeviceViaGUID(request.qrCodeGUID) + val registrationData = submissionService.asyncRegisterDeviceViaGUID(request.qrCodeGUID).also { + Timber.tag(TAG).d("Request %s gave us %s", request, it) + } testResultDataCollector.saveTestResultAnalyticsSettings(registrationData.testResult) // This saves received at @@ -61,7 +61,9 @@ class PCRProcessor @Inject constructor( analyticsKeySubmissionCollector.reportRegisteredWithTeleTAN() - return createCoronaTest(request, registrationData) + return createCoronaTest(request, registrationData).copy( + isResultAvailableNotificationSent = true + ) } private suspend fun createCoronaTest( @@ -70,13 +72,15 @@ class PCRProcessor @Inject constructor( ): PCRCoronaTest { analyticsKeySubmissionCollector.reset() - val testResult = response.testResult.validOrThrow() + val testResult = response.testResult.let { + Timber.tag(TAG).v("Raw test result $it") + testResultDataCollector.updatePendingTestResultReceivedTime(it) - testResultDataCollector.updatePendingTestResultReceivedTime(testResult) + it.toValidatedResult() + } if (testResult == PCR_POSITIVE) { analyticsKeySubmissionCollector.reportPositiveTestResultReceived() - deadmanNotificationScheduler.cancelScheduledWork() } analyticsKeySubmissionCollector.reportTestRegistered() @@ -103,16 +107,15 @@ class PCRProcessor @Inject constructor( return test } - val newTestResult = submissionService.asyncRequestTestResult(test.registrationToken) - Timber.tag(TAG).d("Test result was %s", newTestResult) + val newTestResult = submissionService.asyncRequestTestResult(test.registrationToken).let { + Timber.tag(TAG).d("Raw test result was %s", it) + testResultDataCollector.updatePendingTestResultReceivedTime(it) - newTestResult.validOrThrow() - - testResultDataCollector.updatePendingTestResultReceivedTime(newTestResult) + it.toValidatedResult() + } if (newTestResult == PCR_POSITIVE) { analyticsKeySubmissionCollector.reportPositiveTestResultReceived() - deadmanNotificationScheduler.cancelScheduledWork() } test.copy( @@ -191,11 +194,11 @@ class PCRProcessor @Inject constructor( companion object { private val FINAL_STATES = setOf(PCR_POSITIVE, PCR_NEGATIVE, PCR_REDEEMED) - private const val TAG = "PCRProcessor" + internal const val TAG = "PCRProcessor" } } -private fun CoronaTestResult.validOrThrow(): CoronaTestResult { +private fun CoronaTestResult.toValidatedResult(): CoronaTestResult { val isValid = when (this) { PCR_OR_RAT_PENDING, PCR_NEGATIVE, @@ -210,6 +213,10 @@ private fun CoronaTestResult.validOrThrow(): CoronaTestResult { RAT_REDEEMED -> false } - if (!isValid) throw IllegalArgumentException("Invalid testResult $this") - return this + return if (isValid) { + this + } else { + Timber.tag(PCRProcessor.TAG).e("Server returned invalid PCR testresult $this") + PCR_INVALID + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationService.kt index d8d631cb6882dac0f50ed6f9592be76f620ec28c..1a2dd6872c13a09509dd74816e9bae3b6f8dbb1a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationService.kt @@ -36,31 +36,43 @@ class PCRTestResultAvailableNotificationService @Inject constructor( navDeepLinkBuilderProvider, notificationHelper, cwaSettings, - NotificationConstants.PCR_TEST_RESULT_AVAILABLE_NOTIFICATION_ID + NotificationConstants.PCR_TEST_RESULT_AVAILABLE_NOTIFICATION_ID, + logTag = TAG, ) { + fun setup() { Timber.tag(TAG).d("setup() - PCRTestResultAvailableNotificationService") + @Suppress("RedundantLambdaArrow") coronaTestRepository.latestPCRT .onEach { _ -> + // We want the flow to trigger us, but not work with outdated data due to queue processing val test = coronaTestRepository.latestPCRT.first() + Timber.tag(TAG).v("PCR test change: %s", test) + if (test == null) { cancelTestResultAvailableNotification() return@onEach } - val alreadySent = test.isResultAvailableNotificationSent + val notSentYet = !test.isResultAvailableNotificationSent val isInteresting = INTERESTING_STATES.contains(test.testResult) - Timber.tag(TAG).v("alreadySent=$alreadySent, isInteresting=$isInteresting") + val isTestViewed = test.isViewed + Timber.tag(TAG).v("notSentYet=$notSentYet, isInteresting=$isInteresting, isTestViewed=$isTestViewed") - if (!alreadySent && isInteresting) { - coronaTestRepository.updateResultNotification(identifier = test.identifier, sent = true) - showTestResultAvailableNotification(test) - notificationHelper.cancelCurrentNotification( - NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID - ) - } else { - cancelTestResultAvailableNotification() + when { + notSentYet && isInteresting -> { + Timber.tag(TAG).d("Showing PCR test result notification.") + showTestResultAvailableNotification(test) + coronaTestRepository.updateResultNotification(identifier = test.identifier, sent = true) + notificationHelper.cancelCurrentNotification( + NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + } + isTestViewed -> { + Timber.tag(TAG).d("Canceling PCR test result notification.") + cancelTestResultAvailableNotification() + } } } .launchIn(appScope) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensions.kt index 523b6f2a00fedbfc9ca08d03d5fb3023cb30f43a..faedc26b6b290c1dd7a44881d9cbcee28366317a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensions.kt @@ -17,14 +17,14 @@ import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT.Test import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT.TestPending import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT.TestPositive import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT.TestResultReady -import de.rki.coronawarnapp.exception.http.CwaServerError +import de.rki.coronawarnapp.exception.http.BadRequestException import org.joda.time.Instant fun RACoronaTest?.toSubmissionState(nowUTC: Instant = Instant.now(), coronaTestConfig: CoronaTestConfig) = when { this == null -> NoTest isSubmitted -> SubmissionDone(testRegisteredAt = registeredAt) isProcessing -> FetchingResult - lastError != null -> if (lastError is CwaServerError) TestPending else TestInvalid + lastError is BadRequestException -> TestInvalid else -> when (getState(nowUTC, coronaTestConfig)) { INVALID -> TestError POSITIVE -> { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt index 9454ce480a3b245046b45d89fa89be69281cf4bd..2a9d412af608b844ebea4b85bbc209f10e14171d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt @@ -39,9 +39,14 @@ class RapidAntigenProcessor @Inject constructor( Timber.tag(TAG).d("create(data=%s)", request) request as CoronaTestQRCode.RapidAntigen - val registrationData = submissionService.asyncRegisterDeviceViaGUID(request.registrationIdentifier) + val registrationData = submissionService.asyncRegisterDeviceViaGUID(request.registrationIdentifier).also { + Timber.tag(TAG).d("Request %s gave us %s", request, it) + } - val testResult = registrationData.testResult.validOrThrow() + val testResult = registrationData.testResult.let { + Timber.tag(TAG).v("Raw test result was %s", it) + it.toValidatedResult() + } val now = timeStamper.nowUTC @@ -81,8 +86,10 @@ class RapidAntigenProcessor @Inject constructor( return test } - val newTestResult = submissionService.asyncRequestTestResult(test.registrationToken) - Timber.tag(TAG).d("Test result was %s", newTestResult) + val newTestResult = submissionService.asyncRequestTestResult(test.registrationToken).let { + Timber.tag(TAG).v("Raw test result was %s", it) + it.toValidatedResult() + } test.copy( testResult = check60PlusDays(test, newTestResult), @@ -157,11 +164,11 @@ class RapidAntigenProcessor @Inject constructor( companion object { private val FINAL_STATES = setOf(RAT_POSITIVE, RAT_NEGATIVE, RAT_REDEEMED) - private const val TAG = "RapidAntigenProcessor" + internal const val TAG = "RapidAntigenProcessor" } } -private fun CoronaTestResult.validOrThrow(): CoronaTestResult { +private fun CoronaTestResult.toValidatedResult(): CoronaTestResult { val isValid = when (this) { PCR_OR_RAT_PENDING, RAT_PENDING, @@ -176,6 +183,10 @@ private fun CoronaTestResult.validOrThrow(): CoronaTestResult { PCR_REDEEMED -> false } - if (!isValid) throw IllegalArgumentException("Invalid testResult $this") - return this + return if (isValid) { + this + } else { + Timber.tag(RapidAntigenProcessor.TAG).e("Server returned invalid RapidAntigen testresult $this") + RAT_INVALID + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATTestResultAvailableNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATTestResultAvailableNotificationService.kt index b7d5a0886b296ad610d713b5f1c5c798093e775a..49fd8b04f991b4596609052462ea70fa4e9bb9aa 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATTestResultAvailableNotificationService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATTestResultAvailableNotificationService.kt @@ -3,7 +3,6 @@ package de.rki.coronawarnapp.coronatest.type.rapidantigen.notification import android.content.Context import androidx.navigation.NavDeepLinkBuilder import de.rki.coronawarnapp.coronatest.CoronaTestRepository -import de.rki.coronawarnapp.coronatest.latestPCRT import de.rki.coronawarnapp.coronatest.latestRAT import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.coronatest.type.common.TestResultAvailableNotificationService @@ -38,30 +37,41 @@ class RATTestResultAvailableNotificationService @Inject constructor( notificationHelper, cwaSettings, NotificationConstants.RAT_TEST_RESULT_AVAILABLE_NOTIFICATION_ID, + logTag = TAG, ) { fun setup() { Timber.tag(TAG).d("setup() - RATTestResultAvailableNotificationService") - coronaTestRepository.latestPCRT + @Suppress("RedundantLambdaArrow") + coronaTestRepository.latestRAT .onEach { _ -> + // We want the flow to trigger us, but not work with outdated data due to queue processing val test = coronaTestRepository.latestRAT.first() + Timber.tag(TAG).v("RA test change: %s", test) + if (test == null) { cancelTestResultAvailableNotification() return@onEach } - val alreadySent = test.isResultAvailableNotificationSent + val notSentYet = !test.isResultAvailableNotificationSent val isInteresting = INTERESTING_STATES.contains(test.testResult) - Timber.tag(TAG).v("alreadySent=$alreadySent, isInteresting=$isInteresting") + val isTestViewed = test.isViewed + Timber.tag(TAG).v("notSentYet=$notSentYet, isInteresting=$isInteresting, isTestViewed=$isTestViewed") - if (!alreadySent && isInteresting) { - coronaTestRepository.updateResultNotification(identifier = test.identifier, sent = true) - showTestResultAvailableNotification(test) - notificationHelper.cancelCurrentNotification( - NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID - ) - } else { - cancelTestResultAvailableNotification() + when { + notSentYet && isInteresting -> { + Timber.tag(TAG).d("Showing RA test result notification.") + showTestResultAvailableNotification(test) + coronaTestRepository.updateResultNotification(identifier = test.identifier, sent = true) + notificationHelper.cancelCurrentNotification( + NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + } + isTestViewed -> { + Timber.tag(TAG).d("Canceling RA test result notification as it has already been viewed.") + cancelTestResultAvailableNotification() + } } } .launchIn(appScope) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationScheduler.kt index cdde640fa1b309a7905d53da77aca3bbfc415cae..1936efa408c3049819bf315f031f49611a249994 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationScheduler.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationScheduler.kt @@ -4,16 +4,55 @@ import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy import androidx.work.WorkManager import dagger.Reusable +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.storage.OnboardingSettings +import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.flow.combine +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject @Reusable class DeadmanNotificationScheduler @Inject constructor( + @AppScope val appScope: CoroutineScope, val timeCalculation: DeadmanNotificationTimeCalculation, val workManager: WorkManager, - val workBuilder: DeadmanNotificationWorkBuilder + val workBuilder: DeadmanNotificationWorkBuilder, + val onboardingSettings: OnboardingSettings, + val enfClient: ENFClient, + val coronaTestRepository: CoronaTestRepository ) { + fun setup() { + Timber.i("setup() DeadmanNotificationScheduler") + + combine( + onboardingSettings.isOnboardedFlow, + coronaTestRepository.coronaTests, + enfClient.isTracingEnabled + ) { isOnboarded, coronaTests, isTracingEnabled -> + val noPositiveTestRegistered = coronaTests.none { it.isPositive } + Timber.d( + "isOnboarded = $isOnboarded, " + + "noPositiveTestRegistered = $noPositiveTestRegistered, " + + "isTracingEnabled = $isTracingEnabled" + ) + isOnboarded && noPositiveTestRegistered && isTracingEnabled + } + .onEach { shouldSchedulePeriodic -> + Timber.d("shouldSchedulePeriodic: $shouldSchedulePeriodic") + if (shouldSchedulePeriodic) { + schedulePeriodic() + } else { + cancelScheduledWork() + } + } + .launchIn(appScope) + } + /** * Enqueue background deadman notification onetime work * Replace with new if older work exists. @@ -25,6 +64,7 @@ class DeadmanNotificationScheduler @Inject constructor( if (delay < 0) { return } else { + Timber.d("DeadmanNotification will be scheduled for $delay minutes in the future") // Create unique work and enqueue workManager.enqueueUniqueWork( ONE_TIME_WORK_NAME, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculation.kt index 48e6d3a8eba598fb4121e761684c7595230f544b..a7820554419bb452b385378f8df864c85bdd6c74 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationTimeCalculation.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.first import org.joda.time.DateTimeConstants import org.joda.time.Hours import org.joda.time.Instant +import timber.log.Timber import javax.inject.Inject @Reusable @@ -29,6 +30,7 @@ class DeadmanNotificationTimeCalculation @Inject constructor( */ suspend fun getDelay(): Long { val lastSuccess = enfClient.lastSuccessfulTrackedExposureDetection().first()?.finishedAt + Timber.d("enfClient.lastSuccessfulTrackedExposureDetection: $lastSuccess") return if (lastSuccess != null) { getHoursDiff(lastSuccess).toLong() } else { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/main/CWASettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/main/CWASettings.kt index 86a3b42668eb0f9d689b5b70307a3403d991a863..c433ce80d25024c1173dc5cf4d06fea1303a076a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/main/CWASettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/main/CWASettings.kt @@ -48,9 +48,13 @@ class CWASettings @Inject constructor( ).let { raw -> ConfigData.DeviceTimeState.values().single { it.key == raw } } set(value) = prefs.edit { putString(PKEY_DEVICE_TIME_LAST_STATE_CHANGE_STATE, value.key) } - var numberOfRemainingSharePositiveTestResultReminders: Int - get() = prefs.getInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT, Int.MIN_VALUE) - set(value) = prefs.edit { putInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT, value) } + var numberOfRemainingSharePositiveTestResultRemindersPcr: Int + get() = prefs.getInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT_PCR, Int.MIN_VALUE) + set(value) = prefs.edit { putInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT_PCR, value) } + + var numberOfRemainingSharePositiveTestResultRemindersRat: Int + get() = prefs.getInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT_RAT, Int.MIN_VALUE) + set(value) = prefs.edit { putInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT_RAT, value) } val isNotificationsRiskEnabled = prefs.createFlowPreference( key = PKEY_NOTIFICATIONS_RISK_ENABLED, @@ -80,7 +84,10 @@ class CWASettings @Inject constructor( private const val PKEY_DEVICE_TIME_LAST_STATE_CHANGE_STATE = "devicetime.laststatechange.state" private const val PKEY_NOTIFICATIONS_RISK_ENABLED = "notifications.risk.enabled" private const val PKEY_NOTIFICATIONS_TEST_ENABLED = "notifications.test.enabled" - private const val PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT = "testresults.count" + + private const val PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT_PCR = "testresults.count" + private const val PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT_RAT = "testresults.count.rat" + private const val LAST_CHANGELOG_VERSION = "update.changelog.lastversion" private const val DEFAULT_APP_VERSION = 1L } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt index e8c1e9f7d7ebfb442c6aa05bb584990db24ac6f6..025a3ff9f4fe24325066579a53e31b750ad18179 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -84,23 +85,34 @@ class DefaultTracingStatus @Inject constructor( .addOnFailureListener { cont.resumeWithException(it) } } + @Suppress("LoopWithTooManyJumpStatements") override val isTracingEnabled: Flow<Boolean> = flow { while (true) { try { emit(isEnabled()) delay(POLLING_DELAY_MS) } catch (e: CancellationException) { - Timber.d("isBackgroundRestricted was cancelled") + Timber.tag(TAG).d("isBackgroundRestricted was cancelled") break + } catch (e: ApiException) { + emit(false) + if (e.statusCode == 17) { + // No ENS installed, no need to keep polling. + Timber.tag(TAG).v("No ENS available, aborting polling, assuming permanent.") + break + } else { + Timber.tag(TAG).v("Polling failed, will retry with backoff.") + delay(POLLING_DELAY_MS * 5) + } } } } .distinctUntilChanged() - .onStart { Timber.v("isTracingEnabled FLOW start") } - .onEach { Timber.v("isTracingEnabled FLOW emission: %b", it) } - .onCompletion { if (it == null) Timber.v("isTracingEnabled FLOW completed.") } + .onStart { Timber.tag(TAG).v("isTracingEnabled FLOW start") } + .onEach { Timber.tag(TAG).v("isTracingEnabled FLOW emission: %b", it) } + .onCompletion { if (it == null) Timber.tag(TAG).v("isTracingEnabled FLOW completed.") } .catch { - Timber.w(it, "ENF isEnabled failed.") + Timber.tag(TAG).w(it, "ENF isEnabled failed.") it.report(ExceptionCategory.EXPOSURENOTIFICATION, TAG, null) emit(false) } @@ -109,10 +121,13 @@ class DefaultTracingStatus @Inject constructor( scope = scope ) - private suspend fun isEnabled(): Boolean = suspendCoroutine { cont -> - client.isEnabled - .addOnSuccessListener { cont.resume(it) } - .addOnFailureListener { cont.resumeWithException(it) } + private suspend fun isEnabled(): Boolean = try { + client.isEnabled.await().also { + Timber.tag(TAG).v("Tracing isEnabled=$it") + } + } catch (e: Throwable) { + Timber.tag(TAG).w(e, "Failed to determine tracing status.") + throw e } companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/GeneralNotifications.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/GeneralNotifications.kt index a96ab92983f448bb14c2200ca38bd183bb8507ae..9bffd29f9e43a7f8d9de165bb8793bee5067296a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/GeneralNotifications.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/GeneralNotifications.kt @@ -17,7 +17,9 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import dagger.Reusable import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.notification.NotificationConstants.NOTIFICATION_ID +import de.rki.coronawarnapp.notification.NotificationConstants.POSITIVE_RESULT_NOTIFICATION_TEST_TYPE import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.notifications.setContentTextExpandable @@ -63,8 +65,8 @@ class GeneralNotifications @Inject constructor( notificationManager.createNotificationChannel(channel) } - fun cancelFutureNotifications(notificationId: Int) { - val pendingIntent = createPendingIntentToScheduleNotification(notificationId) + fun cancelFutureNotifications(notificationId: Int, testType: CoronaTest.Type) { + val pendingIntent = createPendingIntentToScheduleNotification(notificationId, testType) val manager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager manager.cancel(pendingIntent) @@ -77,11 +79,12 @@ class GeneralNotifications @Inject constructor( } fun scheduleRepeatingNotification( + testType: CoronaTest.Type, initialTime: Instant, interval: Duration, notificationId: NotificationId ) { - val pendingIntent = createPendingIntentToScheduleNotification(notificationId) + val pendingIntent = createPendingIntentToScheduleNotification(notificationId, testType) val manager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager manager.setInexactRepeating(AlarmManager.RTC, initialTime.millis, interval.millis, pendingIntent) @@ -89,6 +92,7 @@ class GeneralNotifications @Inject constructor( private fun createPendingIntentToScheduleNotification( notificationId: NotificationId, + testType: CoronaTest.Type, flag: Int = FLAG_CANCEL_CURRENT ) = PendingIntent.getBroadcast( @@ -96,13 +100,14 @@ class GeneralNotifications @Inject constructor( notificationId, Intent(context, NotificationReceiver::class.java).apply { putExtra(NOTIFICATION_ID, notificationId) + putExtra(POSITIVE_RESULT_NOTIFICATION_TEST_TYPE, testType.raw) }, flag ) fun newBaseBuilder(): NotificationCompat.Builder { val common = NotificationCompat.Builder(context, MAIN_CHANNEL_ID).apply { - setSmallIcon(R.drawable.ic_splash_logo) + setSmallIcon(R.drawable.ic_notification_icon_default_small) priority = NotificationCompat.PRIORITY_MAX val defaultIntent = PendingIntent.getActivity( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationConstants.kt index 5e89ce916308b96724c92ce00219e9c5806a1813..ca7cfe8ddbbb4cfbb32fe86ccf1e629cdcf9f046 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationConstants.kt @@ -11,6 +11,7 @@ object NotificationConstants { const val NOTIFICATION_ID = "NOTIFICATION_ID" + const val POSITIVE_RESULT_NOTIFICATION_TEST_TYPE = "NOTIFICATION_TEST_TYPE" const val POSITIVE_RESULT_NOTIFICATION_ID = 100 const val POSITIVE_RESULT_NOTIFICATION_TOTAL_COUNT = 2 val POSITIVE_RESULT_NOTIFICATION_INITIAL_OFFSET: Duration = Duration.standardHours(2) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationReceiver.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationReceiver.kt index b11c004a67e76b40da477cd2382af14a64750e8b..33834f3004bcd7ad3a2b4686eabbbeb46745003d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationReceiver.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationReceiver.kt @@ -5,8 +5,10 @@ import android.content.Context import android.content.Intent import dagger.android.AndroidInjection import de.rki.coronawarnapp.coronatest.notification.ShareTestResultNotificationService +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.notification.NotificationConstants.NOTIFICATION_ID import de.rki.coronawarnapp.notification.NotificationConstants.POSITIVE_RESULT_NOTIFICATION_ID +import de.rki.coronawarnapp.notification.NotificationConstants.POSITIVE_RESULT_NOTIFICATION_TEST_TYPE import timber.log.Timber import javax.inject.Inject @@ -20,8 +22,16 @@ class NotificationReceiver : BroadcastReceiver() { AndroidInjection.inject(this, context) when (val notificationId = intent.getIntExtra(NOTIFICATION_ID, Int.MIN_VALUE)) { POSITIVE_RESULT_NOTIFICATION_ID -> { - Timber.tag(TAG).v("NotificationReceiver received intent to show a positive test result notification") - shareTestResultNotificationService.maybeShowSharePositiveTestResultNotification(notificationId) + val testTypeRaw = intent.getStringExtra(POSITIVE_RESULT_NOTIFICATION_TEST_TYPE) + val testType = CoronaTest.Type.values().first { it.raw == testTypeRaw } + Timber.tag(TAG).v( + "NotificationReceiver received intent to show a positive test result notification for test type %s", + testType + ) + shareTestResultNotificationService.maybeShowSharePositiveTestResultNotification( + notificationId, + testType + ) } else -> Timber.tag(TAG).d("NotificationReceiver received an undefined notificationId: %s", notificationId) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt index 02168291fba66cac033257b02fd37433e3107210..65da46f62d6fa7135edb275a028d27ee2a6fede8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt @@ -50,7 +50,7 @@ class PresenceTracingNotifications @Inject constructor( fun newBaseBuilder(): NotificationCompat.Builder { val common = NotificationCompat.Builder(context, channelId).apply { - setSmallIcon(R.drawable.ic_splash_logo) + setSmallIcon(R.drawable.ic_notification_icon_default_small) priority = NotificationCompat.PRIORITY_DEFAULT val pendingIntent = NavDeepLinkBuilder(context) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt index cd5d3699d94a1b0e95360c62733c4d504f17edd4..e329e38b84e00c40302926dd85da8186677d2318 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt @@ -1,6 +1,8 @@ package de.rki.coronawarnapp.risk +import androidx.annotation.VisibleForTesting import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import org.joda.time.Instant @@ -13,7 +15,8 @@ data class CombinedEwPtDayRisk( data class CombinedEwPtRiskLevelResult( private val ptRiskLevelResult: PtRiskLevelResult, - private val ewRiskLevelResult: EwRiskLevelResult + private val ewRiskLevelResult: EwRiskLevelResult, + private val exposureWindowDayRisks: List<ExposureWindowDayRisk>? = null ) { val riskState: RiskState by lazy { @@ -31,12 +34,12 @@ data class CombinedEwPtRiskLevelResult( val daysWithEncounters: Int by lazy { when (riskState) { RiskState.INCREASED_RISK -> { - ewRiskLevelResult.daysWithHighRisk + ewDaysWithHighRisk .plus(ptRiskLevelResult.daysWithHighRisk) .distinct().count() } RiskState.LOW_RISK -> { - ewRiskLevelResult.daysWithLowRisk + ewDaysWithLowRisk .plus(ptRiskLevelResult.daysWithLowRisk) .distinct().count() } @@ -66,6 +69,18 @@ data class CombinedEwPtRiskLevelResult( val matchedRiskCount: Int by lazy { ewRiskLevelResult.matchedKeyCount + ptRiskLevelResult.checkInOverlapCount } + + @VisibleForTesting + internal val ewDaysWithHighRisk: List<LocalDate> + get() = exposureWindowDayRisks?.filter { + it.riskLevel.mapToRiskState() == RiskState.INCREASED_RISK + }?.map { it.localDateUtc } ?: emptyList() + + @VisibleForTesting + internal val ewDaysWithLowRisk: List<LocalDate> + get() = exposureWindowDayRisks?.filter { + it.riskLevel.mapToRiskState() == RiskState.LOW_RISK + }?.map { it.localDateUtc } ?: emptyList() } data class LastCombinedRiskResults( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/EwRiskLevelResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/EwRiskLevelResult.kt index daa440e5a5d375f5c0b1ded9a8885c07f8a74aee..397f47d17acf7cbd541445b8244890c3a601e03d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/EwRiskLevelResult.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/EwRiskLevelResult.kt @@ -3,7 +3,6 @@ package de.rki.coronawarnapp.risk import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import org.joda.time.Instant -import org.joda.time.LocalDate interface EwRiskLevelResult { val calculatedAt: Instant @@ -43,16 +42,6 @@ interface EwRiskLevelResult { ewAggregatedRiskResult?.mostRecentDateWithLowRisk } - val daysWithHighRisk: List<LocalDate> - get() = ewAggregatedRiskResult?.exposureWindowDayRisks?.filter { - it.riskLevel.mapToRiskState() == RiskState.INCREASED_RISK - }?.map { it.localDateUtc } ?: emptyList() - - val daysWithLowRisk: List<LocalDate> - get() = ewAggregatedRiskResult?.exposureWindowDayRisks?.filter { - it.riskLevel.mapToRiskState() == RiskState.LOW_RISK - }?.map { it.localDateUtc } ?: emptyList() - enum class FailureReason(val failureCode: String) { UNKNOWN("unknown"), TRACING_OFF("tracingOff"), diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt index e7891623cc31e9c57bb4b7368d0dc529185655d8..a483530c3c8c1c567fa7b91a69f593074c4b5cea 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt @@ -16,12 +16,16 @@ import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevel import de.rki.coronawarnapp.risk.storage.internal.riskresults.toPersistedAggregatedRiskPerDateResult import de.rki.coronawarnapp.risk.storage.internal.riskresults.toPersistedRiskResult import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDaoWrapper +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.flow.shareLatest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import org.joda.time.Days +import org.joda.time.Instant import timber.log.Timber import de.rki.coronawarnapp.util.flow.combine as flowCombine @@ -30,6 +34,7 @@ abstract class BaseRiskLevelStorage constructor( private val presenceTracingRiskRepository: PresenceTracingRiskRepository, scope: CoroutineScope, private val riskCombinator: RiskCombinator, + private val timeStamper: TimeStamper, ) : RiskLevelStorage { private val database by lazy { riskResultDatabaseFactory.create() } @@ -37,6 +42,9 @@ abstract class BaseRiskLevelStorage constructor( internal val exposureWindowsTables by lazy { database.exposureWindows() } internal val aggregatedRiskPerDateResultTables by lazy { database.aggregatedRiskPerDate() } + private val fifteenDaysAgo: Instant + get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) + abstract val storedResultLimit: Int private suspend fun List<PersistedRiskLevelResultDao>.combineWithWindows( @@ -131,7 +139,7 @@ abstract class BaseRiskLevelStorage constructor( aggregatedRiskPerDateResultTables.allEntries() .map { it.map { persistedAggregatedRiskPerDateResult -> - persistedAggregatedRiskPerDateResult.toAggregatedRiskPerDateResult() + persistedAggregatedRiskPerDateResult.toExposureWindowDayRisk() } } .shareLatest(tag = TAG, scope = scope) @@ -187,15 +195,21 @@ abstract class BaseRiskLevelStorage constructor( override val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<LastCombinedRiskResults> get() = combine( allEwRiskLevelResults, - presenceTracingRiskRepository.allEntries() - ) { ewRiskLevelResults, ptRiskLevelResults -> + presenceTracingRiskRepository.allEntries(), + ewDayRiskStates + ) { ewRiskLevelResults, ptRiskLevelResults, ewDayRiskStates -> val combinedResults = riskCombinator .combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) .sortedByDescending { it.calculatedAt } LastCombinedRiskResults( - lastCalculated = combinedResults.firstOrNull() ?: riskCombinator.latestCombinedResult, + lastCalculated = combinedResults.firstOrNull()?.copy( + // need to provide the data here as they are null in EwAggregatedRiskResult + exposureWindowDayRisks = ewDayRiskStates.filter { ewDayRisk -> + ewDayRisk.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) + } + ) ?: riskCombinator.latestCombinedResult, lastSuccessfullyCalculated = combinedResults.find { it.wasSuccessfullyCalculated } ?: riskCombinator.initialCombinedResult diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedAggregatedRiskPerDateResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedAggregatedRiskPerDateResult.kt index dcd8a3bcae175261ad7248518243fbf16c6209f6..c12514e22e0fc3e8f35a5da22ca54b41d2a79aa2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedAggregatedRiskPerDateResult.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedAggregatedRiskPerDateResult.kt @@ -14,7 +14,7 @@ data class PersistedAggregatedRiskPerDateResult( @ColumnInfo(name = "minimumDistinctEncountersWithLowRisk") val minimumDistinctEncountersWithLowRisk: Int, @ColumnInfo(name = "minimumDistinctEncountersWithHighRisk") val minimumDistinctEncountersWithHighRisk: Int ) { - fun toAggregatedRiskPerDateResult(): ExposureWindowDayRisk = + fun toExposureWindowDayRisk(): ExposureWindowDayRisk = ExposureWindowDayRisk( dateMillisSinceEpoch = dateMillisSinceEpoch, riskLevel = riskLevel, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt index 7e621ecff18388ef2a5f100003def572229cb6eb..70549aee1c17eb4acd80e6bdc2b7004a6e08b1ad 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.submission import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.coronatest.TestRegistrationRequest +import de.rki.coronawarnapp.coronatest.errors.CoronaTestNotFoundException import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest @@ -101,9 +102,15 @@ class SubmissionRepository @Inject constructor( scope.launch { val test = coronaTestRepository.coronaTests.first().singleOrNull { it.type == type } - ?: throw IllegalStateException("No test of type $type available") - - coronaTestRepository.removeTest(identifier = test.identifier) + if (test == null) { + Timber.tag(TAG).w("There is no test of type=$type to remove.") + return@launch + } + try { + coronaTestRepository.removeTest(identifier = test.identifier) + } catch (e: CoronaTestNotFoundException) { + Timber.tag(TAG).e(e, "Test not found (type=$type), already removed?") + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt index 8e0551005b4c26f57eebcd962e47aee5ab00883e..bf8c50d1dfadb1311526b939be3deaaede65c962 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt @@ -4,7 +4,6 @@ import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.bugreporting.reportProblem import de.rki.coronawarnapp.coronatest.CoronaTestRepository -import de.rki.coronawarnapp.coronatest.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService @@ -41,7 +40,6 @@ class SubmissionTask @Inject constructor( private val submissionSettings: SubmissionSettings, private val autoSubmission: AutoSubmission, private val timeStamper: TimeStamper, - private val shareTestResultNotificationService: ShareTestResultNotificationService, private val testResultAvailableNotificationService: PCRTestResultAvailableNotificationService, private val checkInsRepository: CheckInRepository, private val checkInsTransformer: CheckInsTransformer, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestSubmissionDoneCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestSubmissionDoneCard.kt index d4ed6af6b785854ccaa27f593a538961a89bc8bb..7ddb6919692812dd506361473ff8b5ce6bed0964 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestSubmissionDoneCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestSubmissionDoneCard.kt @@ -24,7 +24,7 @@ class PcrTestSubmissionDoneCard( payloads: List<Any> ) -> Unit = { item, payloads -> val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item - + itemView.setOnClickListener { curItem.onClickAction(item) } val userDate = curItem.state.getFormattedRegistrationDate() date.text = resources.getString(R.string.ag_homescreen_card_pcr_body_result_date, userDate) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestSubmissionDoneCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestSubmissionDoneCard.kt index c41df314763cbfb4c4262019318a6b22232249b0..22717b0201abdb410c92a1a8dc8187704ee74971 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestSubmissionDoneCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestSubmissionDoneCard.kt @@ -24,7 +24,7 @@ class RapidTestSubmissionDoneCard( payloads: List<Any> ) -> Unit = { item, payloads -> val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item - + itemView.setOnClickListener { curItem.onClickAction(item) } val userDate = curItem.state.getFormattedRegistrationDate() date.text = resources.getString(R.string.ag_homescreen_card_rapid_body_result_date, userDate) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragment.kt index a3d333a237db33eea300ae776f2449a2de4e8da6..bc08e9d60a4b95f3d7a5ae90c1c3e1a7dcaca571 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder import de.rki.coronawarnapp.databinding.FragmentSettingsTracingBinding import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.tracing.ui.TracingConsentDialog @@ -75,6 +76,10 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), Au binding.settingsTracingSwitchRow.settingsSwitchRowSwitch.isChecked = checked } + vm.ensErrorEvents.observe2(this) { error -> + error.toErrorDialogBuilder(requireContext()).show() + } + setButtonOnClickListener() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt index 420e2179b7763130d0c82a0386d2c31fb0ea7be6..d8eedf1c334362173e74a94fbc781f09acff7a8b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt @@ -65,6 +65,8 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor( } } + val ensErrorEvents = SingleLiveEvent<Throwable>() + private val tracingPermissionHelper = tracingPermissionHelperFactory.create( object : TracingPermissionHelper.Callback { @@ -96,7 +98,8 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor( } override fun onError(error: Throwable) { - Timber.w(error, "Failed to start tracing") + Timber.w(error, "Failed to start tracing from settings screen.") + ensErrorEvents.postValue(error) } } ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt index 1b669bb181d70987acf682fa200fcc0a62a2d311..09f6ce0b1374dcdcbbfcbc16691ef79d723ee091 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt @@ -195,7 +195,6 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { vm.doBackgroundNoiseCheck() contactDiaryWorkScheduler.schedulePeriodic() dataDonationAnalyticsScheduler.schedulePeriodic() - vm.checkDeadMan() } private fun showEnergyOptimizedEnabledForBackground() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt index 1949db08144a868cd025f9a46b64e90fbab4a9d4..f8fcb04d1a3005d290195b5ccd8db41580c0ef30 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt @@ -21,7 +21,6 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import timber.log.Timber @Suppress("LongParameterList") class MainActivityViewModel @AssistedInject constructor( @@ -97,16 +96,6 @@ class MainActivityViewModel @AssistedInject constructor( } } - fun checkDeadMan() { - launch { - val isAllowedToSubmitKeys = coronaTestRepository.coronaTests.first().any { it.isSubmissionAllowed } - if (!isAllowedToSubmitKeys) { - Timber.v("We are not allowed to submit keys, scheduling deadman.") - deadmanScheduler.schedulePeriodic() - } - } - } - @AssistedFactory interface Factory : SimpleCWAViewModelFactory<MainActivityViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt index e63fe280af03b70f15000d95984468a17b21a4e9..0921d2f093d0da987c76535badef71cc4db242f2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt @@ -10,6 +10,8 @@ import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.LinearLayoutManager import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.databinding.HomeFragmentLayoutBinding import de.rki.coronawarnapp.tracing.ui.TracingExplanationDialog import de.rki.coronawarnapp.ui.main.home.popups.DeviceTimeIncorrectDialog @@ -101,7 +103,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { onPositive = { viewModel.errorResetDialogDismissed() } ) } - HomeFragmentEvents.ShowDeleteTestDialog -> showRemoveTestDialog() + is HomeFragmentEvents.ShowDeleteTestDialog -> showRemoveTestDialog(event.type) HomeFragmentEvents.GoToStatisticsExplanation -> doNavigate( HomeFragmentDirections.actionMainFragmentToStatisticsExplanationFragment() ) @@ -122,6 +124,18 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { if (!showDialog) return@observe2 deviceTimeIncorrectDialog.show { viewModel.userHasAcknowledgedIncorrectDeviceTime() } } + + viewModel.coronaTestErrors.observe2(this) { tests -> + tests.forEach { test -> + test.lastError?.toErrorDialogBuilder(requireContext())?.apply { + val testName = when (test.type) { + CoronaTest.Type.PCR -> R.string.ag_homescreen_card_pcr_title + CoronaTest.Type.RAPID_ANTIGEN -> R.string.ag_homescreen_card_rapidtest_title + } + setTitle(getString(testName) + " " + getString(R.string.errors_generic_headline_short)) + }?.show() + } + } } override fun onResume() { @@ -131,7 +145,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { binding.container.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) } - private fun showRemoveTestDialog() { + private fun showRemoveTestDialog(type: CoronaTest.Type) { val removeTestDialog = DialogHelper.DialogInstance( requireActivity(), R.string.submission_test_result_dialog_remove_test_title, @@ -139,7 +153,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject { R.string.submission_test_result_dialog_remove_test_button_positive, R.string.submission_test_result_dialog_remove_test_button_negative, positiveButtonFunction = { - viewModel.deregisterWarningAccepted() + viewModel.deregisterWarningAccepted(type) } ) DialogHelper.showDialog(removeTestDialog).apply { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentEvents.kt index b219536e6ad616d1f0e99d5e9b10584a2336c35c..4573361022e31b8bc36f27279c2aff8ad07573f9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentEvents.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentEvents.kt @@ -1,12 +1,14 @@ package de.rki.coronawarnapp.ui.main.home +import de.rki.coronawarnapp.coronatest.type.CoronaTest + sealed class HomeFragmentEvents { object ShowTracingExplanation : HomeFragmentEvents() object ShowErrorResetDialog : HomeFragmentEvents() - object ShowDeleteTestDialog : HomeFragmentEvents() + data class ShowDeleteTestDialog(val type: CoronaTest.Type) : HomeFragmentEvents() object GoToStatisticsExplanation : HomeFragmentEvents() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt index 75c8f0e2268acc84087f85cafdf5f7bcf42a6939..922423eaaf0b071377d7af7e80cbc67caa0e69ad 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.appconfig.CoronaTestConfig import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.coronatest.latestPCRT import de.rki.coronawarnapp.coronatest.latestRAT +import de.rki.coronawarnapp.coronatest.testErrorsSingleEvent import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR @@ -17,7 +18,6 @@ import de.rki.coronawarnapp.coronatest.type.pcr.toSubmissionState import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT import de.rki.coronawarnapp.coronatest.type.rapidantigen.toSubmissionState -import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.statistics.source.StatisticsProvider import de.rki.coronawarnapp.statistics.ui.homecards.StatisticsHomeCard @@ -87,7 +87,6 @@ class HomeFragmentViewModel @AssistedInject constructor( private val cwaSettings: CWASettings, private val appConfigProvider: AppConfigProvider, statisticsProvider: StatisticsProvider, - private val deadmanNotificationScheduler: DeadmanNotificationScheduler, private val appShortcutsHelper: AppShortcutsHelper, private val tracingSettings: TracingSettings, private val traceLocationOrganizerSettings: TraceLocationOrganizerSettings, @@ -108,6 +107,9 @@ class HomeFragmentViewModel @AssistedInject constructor( val popupEvents = SingleLiveEvent<HomeFragmentEvents>() + val coronaTestErrors = coronaTestRepository.testErrorsSingleEvent + .asLiveData(context = dispatcherProvider.Default) + fun showPopUps() { launch { if (errorResetTool.isResetNoticeToBeShown) { @@ -197,7 +199,7 @@ class HomeFragmentViewModel @AssistedInject constructor( ) } is SubmissionStatePCR.TestInvalid -> PcrTestInvalidCard.Item(state) { - popupEvents.postValue(HomeFragmentEvents.ShowDeleteTestDialog) + popupEvents.postValue(HomeFragmentEvents.ShowDeleteTestDialog(CoronaTest.Type.PCR)) } is SubmissionStatePCR.TestError -> PcrTestErrorCard.Item(state) { routeToScreen.postValue( @@ -212,7 +214,10 @@ class HomeFragmentViewModel @AssistedInject constructor( ) } is SubmissionStatePCR.SubmissionDone -> PcrTestSubmissionDoneCard.Item(state) { - // TODO + routeToScreen.postValue( + HomeFragmentDirections + .actionMainFragmentToSubmissionTestResultKeysSharedFragment(CoronaTest.Type.PCR) + ) } } @@ -243,7 +248,7 @@ class HomeFragmentViewModel @AssistedInject constructor( ) } is SubmissionStateRAT.TestInvalid -> RapidTestInvalidCard.Item(state) { - popupEvents.postValue(HomeFragmentEvents.ShowDeleteTestDialog) + popupEvents.postValue(HomeFragmentEvents.ShowDeleteTestDialog(CoronaTest.Type.RAPID_ANTIGEN)) } is SubmissionStateRAT.TestError -> RapidTestErrorCard.Item(state) { routeToScreen.postValue( @@ -265,7 +270,10 @@ class HomeFragmentViewModel @AssistedInject constructor( submissionRepository.removeTestFromDevice(type = CoronaTest.Type.RAPID_ANTIGEN) } is SubmissionStateRAT.SubmissionDone -> RapidTestSubmissionDoneCard.Item(state) { - // TODO + routeToScreen.postValue( + HomeFragmentDirections + .actionMainFragmentToSubmissionTestResultKeysSharedFragment(CoronaTest.Type.RAPID_ANTIGEN) + ) } } @@ -381,8 +389,8 @@ class HomeFragmentViewModel @AssistedInject constructor( tracingRepository.refreshRiskResult() } - fun deregisterWarningAccepted() { - submissionRepository.removeTestFromDevice(type = CoronaTest.Type.PCR) + fun deregisterWarningAccepted(type: CoronaTest.Type) { + submissionRepository.removeTestFromDevice(type) } fun userHasAcknowledgedTheLoweredRiskLevel() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt index 55776f95bba45e2d9c3490521e8b672928cf75ad..9c657fa366ef04b8eaed2885a48c48635736ad43 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragment.kt @@ -7,6 +7,7 @@ import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder import de.rki.coronawarnapp.databinding.FragmentOnboardingTracingBinding import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.util.DialogHelper @@ -62,6 +63,9 @@ class OnboardingTracingFragment : Fragment(R.layout.fragment_onboarding_tracing) vm.permissionRequestEvent.observe2(this) { permissionRequest -> permissionRequest.invoke(requireActivity()) } + vm.ensErrorEvents.observe2(this) { error -> + error.toErrorDialogBuilder(requireContext()).show() + } } override fun onResume() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt index dca5b110f9d065b6af5a29728cbeb3ae83dfa58b..94f0dadf744b212591c86373f78601061d79f012 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt @@ -28,6 +28,7 @@ class OnboardingTracingFragmentViewModel @AssistedInject constructor( .asLiveData(context = dispatcherProvider.Default) val routeToScreen: SingleLiveEvent<OnboardingNavigationEvents> = SingleLiveEvent() val permissionRequestEvent = SingleLiveEvent<(Activity) -> Unit>() + val ensErrorEvents = SingleLiveEvent<Throwable>() private val tracingPermissionHelper = tracingPermissionHelperFactory.create( @@ -49,6 +50,7 @@ class OnboardingTracingFragmentViewModel @AssistedInject constructor( override fun onError(error: Throwable) { Timber.e(error, "Failed to activate tracing during onboarding.") + ensErrorEvents.postValue(error) } } ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt index 38b737b09b30de54bf34e26500b4c128e51842fb..03e2b7cd6e30e82ead765ca2331cc576518a4480 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt @@ -60,7 +60,6 @@ class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_ } continueButton.setOnClickListener { - viewModel.deleteExistingAndRegisterNewTest() } @@ -84,6 +83,7 @@ class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_ viewModel.registrationState.observe2(this) { state -> binding.submissionQrCodeScanSpinner.isVisible = state.apiRequestState == ApiRequestState.STARTED + binding.continueButton.isVisible = state.apiRequestState != ApiRequestState.STARTED if (ApiRequestState.SUCCESS == state.apiRequestState) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultKeysSharedFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultKeysSharedFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..d7de6021db80d802d076d64914d89865f00b9a7e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultKeysSharedFragment.kt @@ -0,0 +1,94 @@ +package de.rki.coronawarnapp.ui.submission.testresult.positive + +import android.app.AlertDialog +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityEvent +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.databinding.FragmentSubmissionTestResultPositiveKeysSharedBinding +import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat +import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.observe2 +import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted +import javax.inject.Inject + +/** + * [SubmissionTestResultKeysSharedFragment], the test result screen that is shown to the user if they have provided + * consent. + */ +class SubmissionTestResultKeysSharedFragment : + Fragment(R.layout.fragment_submission_test_result_positive_keys_shared), + AutoInject { + + private val navArgs by navArgs<SubmissionTestResultKeysSharedFragmentArgs>() + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: SubmissionTestResultKeysSharedViewModel by cwaViewModelsAssisted( + factoryProducer = { viewModelFactory }, + constructorCall = { factory, _ -> + factory as SubmissionTestResultKeysSharedViewModel.Factory + factory.create(navArgs.testType) + } + ) + + private val binding: FragmentSubmissionTestResultPositiveKeysSharedBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.onTestOpened() + + binding.submissionDonePcrValidation.root.isVisible = viewModel.testType == CoronaTest.Type.RAPID_ANTIGEN + + binding.toolbar.setNavigationOnClickListener { + popBackStack() + } + + binding.deleteTest.setOnClickListener { + viewModel.onShowDeleteTestDialog() + } + + viewModel.uiState.observe2(this) { + binding.apply { + submissionTestResultSection.setTestResultSection(it.coronaTest) + } + } + + viewModel.showDeleteTestDialog.observe2(this) { showDeleteTestDialog() } + } + + override fun onResume() { + super.onResume() + binding.submissionTestResultContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) + } + + private fun navigateBack() { + popBackStack() + } + + private fun showDeleteTestDialog() { + val removeTestDialog = DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_test_result_dialog_remove_test_title, + R.string.submission_test_result_dialog_remove_test_message, + R.string.submission_test_result_dialog_remove_test_button_positive, + R.string.submission_test_result_dialog_remove_test_button_negative, + positiveButtonFunction = { + viewModel.onDeleteTestConfirmed() + navigateBack() + } + ) + DialogHelper.showDialog(removeTestDialog).apply { + getButton(AlertDialog.BUTTON_POSITIVE) + .setTextColor(context.getColorCompat(R.color.colorTextSemanticRed)) + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultKeysSharedModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultKeysSharedModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..65ed0e6ecfa2c1aefe6596570e40ab769a38c565 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultKeysSharedModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.submission.testresult.positive + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class SubmissionTestResultKeysSharedModule { + @Binds + @IntoMap + @CWAViewModelKey(SubmissionTestResultKeysSharedViewModel::class) + abstract fun submissionTestResultKeysSharedFragment( + factory: SubmissionTestResultKeysSharedViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultKeysSharedViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultKeysSharedViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..8e85122c6b61dc7938ecd1abd9754dfc02f84fbf --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultKeysSharedViewModel.kt @@ -0,0 +1,57 @@ +package de.rki.coronawarnapp.ui.submission.testresult.positive + +import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type +import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState +import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import timber.log.Timber + +class SubmissionTestResultKeysSharedViewModel @AssistedInject constructor( + private val submissionRepository: SubmissionRepository, + @Assisted val testType: Type, + dispatcherProvider: DispatcherProvider +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + init { + Timber.v("init() coronaTestType=%s", testType) + } + + val uiState: LiveData<TestResultUIState> = submissionRepository.testForType(type = testType) + .filterNotNull() + .map { test -> + TestResultUIState(coronaTest = test) + }.asLiveData(context = Dispatchers.Default) + + val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() + + val showDeleteTestDialog = SingleLiveEvent<Unit>() + + fun onTestOpened() = launch { + submissionRepository.setViewedTestResult(type = testType) + } + + fun onShowDeleteTestDialog() { + showDeleteTestDialog.postValue(Unit) + } + + fun onDeleteTestConfirmed() { + submissionRepository.removeTestFromDevice(type = testType) + } + + @AssistedFactory + interface Factory : CWAViewModelFactory<SubmissionTestResultKeysSharedViewModel> { + fun create(testType: Type): SubmissionTestResultKeysSharedViewModel + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt index 40b436ea56a0bf12eb637bcd3797854b2aa9d07e..2c573ff236da8691de9a51a61c6689b147ffa87c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt @@ -32,6 +32,8 @@ import de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResul import de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingModule import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultConsentGivenFragment import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultConsentGivenModule +import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultKeysSharedFragment +import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultKeysSharedModule import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultNoConsentFragment import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultNoConsentModule import de.rki.coronawarnapp.ui.submission.warnothers.SubmissionResultPositiveOtherWarningNoConsentFragment @@ -39,7 +41,7 @@ import de.rki.coronawarnapp.ui.submission.warnothers.SubmissionResultPositiveOth import de.rki.coronawarnapp.ui.submission.yourconsent.SubmissionYourConsentFragment import de.rki.coronawarnapp.ui.submission.yourconsent.SubmissionYourConsentModule -@Suppress("FunctionNaming", "MaxLineLength") +@Suppress("FunctionNaming", "MaxLineLength", "TooManyFunctions") @Module internal abstract class SubmissionFragmentModule { @@ -105,4 +107,7 @@ internal abstract class SubmissionFragmentModule { @ContributesAndroidInjector(modules = [RATResultNegativeModule::class]) abstract fun submissionNegativeRATResultScreen(): RATResultNegativeFragment + + @ContributesAndroidInjector(modules = [SubmissionTestResultKeysSharedModule::class]) + abstract fun submissionTestResultKeysSharedScreen(): SubmissionTestResultKeysSharedFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/HasHumanReadableError.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/HasHumanReadableError.kt index 37eebfda6663da7d3587233005b48d36899a5d7e..87de8310852e8ffb588ef7376fc37f356349f977 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/HasHumanReadableError.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/HasHumanReadableError.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.util import android.content.Context +import de.rki.coronawarnapp.bugreporting.exceptions.findKnownError import de.rki.coronawarnapp.util.ui.LazyString interface HasHumanReadableError { @@ -15,7 +16,7 @@ data class HumanReadableError( fun Throwable.tryHumanReadableError(context: Context): HumanReadableError = when (this) { is HasHumanReadableError -> this.toHumanReadableError(context) else -> { - HumanReadableError( + findKnownError(context) ?: HumanReadableError( description = (localizedMessage ?: this.message) ?: this.toString() ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt index 9479266cd2b464059f98a58a8afd340ee37e902b..6cd22bcbd6e0869c25a0452a44bdd062a429ddcc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt @@ -54,7 +54,7 @@ class EncryptedPreferencesMigration @Inject constructor( cwaSettings.wasTracingExplanationDialogShown = wasTracingExplanationDialogShown() cwaSettings.isNotificationsRiskEnabled.update { isNotificationsRiskEnabled() } cwaSettings.isNotificationsTestEnabled.update { isNotificationsTestEnabled() } - cwaSettings.numberOfRemainingSharePositiveTestResultReminders = + cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr = numberOfRemainingSharePositiveTestResultReminders() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/errors/ExceptionExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/errors/ExceptionExtensions.kt index f2d986507e3a6bd8f88acbdfc5ef125fe8835ee1..50407b262a3a4d7bdaef4d264f4cb90d431800b1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/errors/ExceptionExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/errors/ExceptionExtensions.kt @@ -9,3 +9,9 @@ fun Throwable.causes(): Sequence<Throwable> = sequence { error = error.cause ?: break } } + +fun Throwable.stackTraceToStringList(): List<String> { + return stackTrace.toList().map { traceElement -> + traceElement.toString() + } +} diff --git a/Corona-Warn-App/src/main/res/drawable-hdpi/ic_notification_icon_default_small.png b/Corona-Warn-App/src/main/res/drawable-hdpi/ic_notification_icon_default_small.png new file mode 100644 index 0000000000000000000000000000000000000000..121fd8f2f022ccfb5fea9fd95dff1a361f79fdb2 Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-hdpi/ic_notification_icon_default_small.png differ diff --git a/Corona-Warn-App/src/main/res/drawable-mdpi/ic_notification_icon_default_small.png b/Corona-Warn-App/src/main/res/drawable-mdpi/ic_notification_icon_default_small.png new file mode 100644 index 0000000000000000000000000000000000000000..a6fd4cde3ac7714d82d0392485c6084464e80d81 Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-mdpi/ic_notification_icon_default_small.png differ diff --git a/Corona-Warn-App/src/main/res/drawable-xhdpi/ic_notification_icon_default_small.png b/Corona-Warn-App/src/main/res/drawable-xhdpi/ic_notification_icon_default_small.png new file mode 100644 index 0000000000000000000000000000000000000000..0f242d9ee09f9d3f3be4806bbabf08db7b020528 Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-xhdpi/ic_notification_icon_default_small.png differ diff --git a/Corona-Warn-App/src/main/res/drawable-xxhdpi/ic_notification_icon_default_small.png b/Corona-Warn-App/src/main/res/drawable-xxhdpi/ic_notification_icon_default_small.png new file mode 100644 index 0000000000000000000000000000000000000000..6edec43d5fa76e9f3c261193393ceab522739fe6 Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-xxhdpi/ic_notification_icon_default_small.png differ diff --git a/Corona-Warn-App/src/main/res/drawable-xxxhdpi/ic_notification_icon_default_small.png b/Corona-Warn-App/src/main/res/drawable-xxxhdpi/ic_notification_icon_default_small.png new file mode 100644 index 0000000000000000000000000000000000000000..07b562d0da9e1cb25a2b1a39fa93ba371ff55468 Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-xxxhdpi/ic_notification_icon_default_small.png differ diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_fragment.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_fragment.xml index 1a236c4b811a56108ddc2af3207966dbaaf3bcec..497d4491be0c86e6c0b32196ba1d18c9b0817be6 100644 --- a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_fragment.xml @@ -22,9 +22,8 @@ android:id="@+id/scrollview" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_marginBottom="12dp" android:clipToPadding="false" - android:paddingBottom="8dp" + android:paddingBottom="@dimen/spacing_normal" app:layout_constraintBottom_toTopOf="@id/log_control_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_edit_locations_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_edit_locations_fragment.xml index f849d27ac5a7955ca4a7a4c46931b9aad2382134..72bfa17005cd72bc5645155e10d221a8b56c88c9 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_edit_locations_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_edit_locations_fragment.xml @@ -58,7 +58,11 @@ android:id="@+id/locations_recycler_view" android:layout_width="0dp" android:layout_height="0dp" - android:layout_margin="@dimen/spacing_normal" + android:layout_marginBottom="@dimen/spacing_normal" + android:paddingTop="@dimen/spacing_normal" + android:paddingHorizontal="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" android:importantForAccessibility="no" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_edit_persons_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_edit_persons_fragment.xml index b49af42225c281bb502c6cd21433f7fcebc1f02f..e58bf18b26d95cd7af8ceffafc57a420435a63f2 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_edit_persons_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_edit_persons_fragment.xml @@ -57,7 +57,11 @@ android:id="@+id/persons_recycler_view" android:layout_width="0dp" android:layout_height="0dp" - android:layout_margin="@dimen/spacing_normal" + android:layout_marginBottom="@dimen/spacing_normal" + android:paddingTop="@dimen/spacing_normal" + android:paddingHorizontal="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" android:importantForAccessibility="no" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_overview_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_overview_fragment.xml index 140a4c3982e094a187e809252263df3147963cd3..503bc98d761c9f55006ca2d5e430f0e8d5e602c9 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_overview_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_overview_fragment.xml @@ -6,6 +6,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:contentDescription="@string/contact_diary_overview_title" + android:background="@color/colorBackground" android:orientation="vertical"> <androidx.appcompat.widget.Toolbar diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information.xml b/Corona-Warn-App/src/main/res/layout/fragment_information.xml index 25dfed9ca1d242afaa0acc5111e66759c8d63cd1..1fd3284d9b98cf9861372a0ffa1b6aecc9e50e96 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information.xml @@ -26,8 +26,10 @@ android:layout_width="match_parent" android:layout_height="0dp" android:fillViewport="true" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginBottom="@dimen/spacing_normal" + android:paddingTop="@dimen/spacing_normal" + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml index 5d5c89cfd12d23814ea839799d974a5d7c4c860d..cbc670c87dafce6a194073d268eed6e9512d67d2 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_about.xml @@ -24,7 +24,10 @@ android:layout_width="0dp" android:layout_height="0dp" android:fillViewport="true" - app:layout_constraintBottom_toBottomOf="@+id/guideline_bottom" + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/information_about_header"> @@ -103,13 +106,6 @@ </ScrollView> - <androidx.constraintlayout.widget.Guideline - android:id="@+id/guideline_bottom" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/guideline_bottom" /> - </androidx.constraintlayout.widget.ConstraintLayout> </layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml index 75163b421f29afa948d220349347a3c74e1ab7d4..968aa88b3461620ee41cdaf20ddb7ced87803f4a 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml @@ -24,7 +24,10 @@ android:layout_width="0dp" android:layout_height="0dp" android:fillViewport="true" - app:layout_constraintBottom_toBottomOf="@+id/guideline_bottom" + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/information_contact_header"> @@ -152,13 +155,6 @@ </ScrollView> - <androidx.constraintlayout.widget.Guideline - android:id="@+id/guideline_bottom" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/guideline_bottom" /> - </androidx.constraintlayout.widget.ConstraintLayout> </layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml index acc309aed1edf4c6a7debc6f0a885e67fd848d3d..620ef558a39f452ac61f38f5fba2ae668925b9a0 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_legal.xml @@ -31,9 +31,11 @@ <ScrollView android:layout_width="0dp" android:layout_height="0dp" - android:fillViewport="true" android:focusable="true" - app:layout_constraintBottom_toBottomOf="@+id/guideline_bottom" + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/information_legal_header"> @@ -155,13 +157,6 @@ </ScrollView> - <androidx.constraintlayout.widget.Guideline - android:id="@+id/guideline_bottom" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/guideline_bottom" /> - </androidx.constraintlayout.widget.ConstraintLayout> </layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml index dd56b2ae00893e505474de3ae6ce8fdf17adda51..3fa2679625865a4a7d89de12d06782308a86315a 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_privacy.xml @@ -30,7 +30,9 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" - android:paddingBottom="@dimen/guideline_bottom"> + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay"> <include android:id="@+id/information_privacy_header_details" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml index 2cbf8db7f0ea002a329a34d864e31f44ac549590..2d648ab3d6bedc5e45b1281e73caa299ca072adc 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_technical.xml @@ -30,7 +30,9 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" - android:paddingBottom="@dimen/guideline_bottom"> + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay"> <include android:id="@+id/information_technical_header_details" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_ppa_more_info.xml b/Corona-Warn-App/src/main/res/layout/fragment_ppa_more_info.xml index 3d6dfdd6af518ff567e5872880a4ec18965da228..4acd20f2b086f3d68190b79faa099565a4b4f012 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_ppa_more_info.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_ppa_more_info.xml @@ -3,19 +3,17 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - <androidx.constraintlayout.widget.ConstraintLayout + <LinearLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" + android:orientation="vertical" tools:context=".datadonation.analytics.ui.PpaMoreInfoFragment"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/header" android:layout_width="match_parent" - android:layout_height="@dimen/header" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + android:layout_height="@dimen/header"> <include android:id="@+id/button_back" @@ -39,44 +37,35 @@ </androidx.constraintlayout.widget.ConstraintLayout> <ScrollView - android:layout_width="0dp" - android:layout_height="0dp" - app:layout_constraintBottom_toBottomOf="@id/guideline_bottom" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="1.0" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/header" - app:layout_constraintVertical_bias="1.0"> - - <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="@dimen/spacing_medium" + android:paddingHorizontal="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay"> + + <LinearLayout android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:orientation="vertical"> <TextView android:id="@+id/onboarding_headline" style="@style/headline4" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/guideline_start" android:layout_marginTop="12dp" - android:layout_marginEnd="@dimen/guideline_end" android:contentDescription="@string/onboarding_ppa_more_info_headline" android:focusable="true" - android:text="@string/onboarding_ppa_more_info_headline" - app:layout_constraintEnd_toEndOf="@id/body_end" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="parent" /> + android:text="@string/onboarding_ppa_more_info_headline" /> - <androidx.constraintlayout.widget.ConstraintLayout + <LinearLayout android:id="@+id/legal_layout" style="@style/cardTracing" - android:layout_width="0dp" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="45dp" - android:orientation="vertical" - app:layout_constraintEnd_toStartOf="@+id/guideline_card_end" - app:layout_constraintStart_toStartOf="@+id/guideline_card_start" - app:layout_constraintTop_toBottomOf="@+id/onboarding_headline"> + android:layout_marginTop="@dimen/spacing_large" + android:orientation="vertical"> <TextView android:id="@+id/legal_title" @@ -86,686 +75,602 @@ android:layout_marginEnd="@dimen/spacing_small" android:contentDescription="@string/ppa_onboarding_more_info_title" android:focusable="true" - android:text="@string/ppa_onboarding_more_info_title" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + android:text="@string/ppa_onboarding_more_info_title" /> <TextView android:id="@+id/legal_body" style="@style/body1" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="27dp" + android:layout_marginTop="@dimen/spacing_normal" android:focusable="true" - android:text="@string/ppa_onboarding_more_info_body" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/legal_title" /> + android:text="@string/ppa_onboarding_more_info_body" /> - </androidx.constraintlayout.widget.ConstraintLayout> + </LinearLayout> <TextView android:id="@+id/data_processing_title" style="@style/subtitleBoldSixteen" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/guideline_start" - android:layout_marginTop="41dp" - android:layout_marginEnd="@dimen/guideline_end" + android:layout_marginTop="@dimen/spacing_large" android:contentDescription="@string/onboarding_ppa_more_info_data_processing_title" android:focusable="true" - android:text="@string/onboarding_ppa_more_info_data_processing_title" - app:layout_constraintEnd_toEndOf="@id/body_end" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/legal_layout" /> + android:text="@string/onboarding_ppa_more_info_data_processing_title" /> <TextView android:id="@+id/data_processing_body" style="@style/body1" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/guideline_start" - android:layout_marginTop="16dp" - android:layout_marginEnd="@dimen/guideline_end" + android:layout_marginTop="@dimen/spacing_small" android:contentDescription="@string/onboarding_ppa_more_info_data_processing_body" android:focusable="true" - android:text="@string/onboarding_ppa_more_info_data_processing_body" - app:layout_constraintEnd_toEndOf="@id/body_end" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/data_processing_title" /> - - <TextView - android:id="@+id/data_processing_point_1_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="35dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_data_processing_point_1_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/data_processing_body" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/data_processing_point_1" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/data_processing_point_1_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/data_processing_point_2_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_data_processing_point_2_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/data_processing_point_1_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/data_processing_point_2" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/data_processing_point_2_text" - app:srcCompat="@drawable/bullet_point" /> + android:text="@string/onboarding_ppa_more_info_data_processing_body" /> - <TextView - android:id="@+id/data_processing_point_3_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_data_processing_point_3_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/data_processing_point_2_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/data_processing_point_3" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/data_processing_point_3_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/data_processing_point_4_text" - style="@style/subtitle" - android:layout_width="0dp" + <TableLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_data_processing_point_4_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/data_processing_point_3_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/data_processing_point_4" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/data_processing_point_4_text" - app:srcCompat="@drawable/bullet_point" /> + android:layout_marginTop="@dimen/spacing_small"> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/data_processing_point_1" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/data_processing_point_1_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_data_processing_point_1_text" /> + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + <ImageView + android:id="@+id/data_processing_point_2" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/data_processing_point_2_text" + style="@style/subtitle" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_data_processing_point_2_text" + android:layout_weight="1" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/data_processing_point_3" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/data_processing_point_3_text" + style="@style/subtitle" + android:layout_marginStart="13dp" + android:layout_weight="1" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_data_processing_point_3_text" /> + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/data_processing_point_4" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/data_processing_point_4_text" + style="@style/subtitle" + android:layout_marginStart="13dp" + android:layout_weight="1" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_data_processing_point_4_text" /> + + </TableRow> + </TableLayout> <TextView android:id="@+id/rki_data_title" style="@style/subtitleBoldSixteen" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/guideline_start" - android:layout_marginTop="41dp" - android:layout_marginEnd="@dimen/guideline_end" + android:layout_marginTop="@dimen/spacing_large" android:contentDescription="@string/onboarding_ppa_more_info_rki_data_title" android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_title" - app:layout_constraintEnd_toEndOf="@id/body_end" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/data_processing_point_4_text" /> - - <TextView - android:id="@+id/rki_data_point_1_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="23dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_1_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_title" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_1" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_1_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/rki_data_point_2_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_2_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_1_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_2" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_2_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/rki_data_point_3_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_3_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_2_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_3" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_3_text" - app:srcCompat="@drawable/bullet_point" /> + android:text="@string/onboarding_ppa_more_info_rki_data_title" /> - <TextView - android:id="@+id/rki_data_point_4_text" - style="@style/subtitle" - android:layout_width="0dp" + <TableLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_4_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_3_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_4" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_4_text" - app:srcCompat="@drawable/bullet_point" /> + android:layout_marginTop="@dimen/spacing_small"> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_1" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_1_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_1_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_2" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_2_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_2_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_3" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_3_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_3_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_4" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_4_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_4_text" /> + + </TableRow> + </TableLayout> <TextView android:id="@+id/rki_data_body_1" style="@style/body1" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/guideline_start" - android:layout_marginTop="23dp" - android:layout_marginEnd="@dimen/guideline_end" + android:layout_marginTop="@dimen/spacing_normal" android:contentDescription="@string/onboarding_ppa_more_info_rki_data_body_1" android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_body_1" - app:layout_constraintEnd_toEndOf="@id/body_end" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_4_text" /> - - <TextView - android:id="@+id/rki_data_point_5_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="23dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_5_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_body_1" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_5" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_5_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/rki_data_point_6_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_6_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_5_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_6" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_6_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/rki_data_point_7_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_7_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_6_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_7" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_7_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/rki_data_point_8_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_8_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_7_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_8" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_8_text" - app:srcCompat="@drawable/bullet_point" /> + android:text="@string/onboarding_ppa_more_info_rki_data_body_1" /> - <TextView - android:id="@+id/rki_data_point_9_text" - style="@style/subtitle" - android:layout_width="0dp" + <TableLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_9_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_8_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_9" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_9_text" - app:srcCompat="@drawable/bullet_point" /> + android:layout_marginTop="@dimen/spacing_small"> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_5" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_5_text" + style="@style/subtitle" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_5_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_6" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_6_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_6_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_7" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_7_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_7_text" /> + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_8" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_8_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_8_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_9" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_9_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_9_text" /> + + </TableRow> + + </TableLayout> <TextView android:id="@+id/rki_data_body_2" style="@style/body1" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/guideline_start" - android:layout_marginTop="23dp" - android:layout_marginEnd="@dimen/guideline_end" + android:layout_marginTop="@dimen/spacing_normal" android:contentDescription="@string/onboarding_ppa_more_info_rki_data_body_2" android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_body_2" - app:layout_constraintEnd_toEndOf="@id/body_end" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_9_text" /> - - <TextView - android:id="@+id/rki_data_point_10_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="23dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_10_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_body_2" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_10" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_10_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/rki_data_point_11_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_11_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_10_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_11" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_11_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/rki_data_point_12_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_12_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_11_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_12" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_12_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/rki_data_point_13_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_13_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_12_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_13" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_13_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/rki_data_point_14_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_14_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_13_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_14" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_14_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/rki_data_point_15_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_15_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_14_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_15" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_15_text" - app:srcCompat="@drawable/bullet_point" /> + android:text="@string/onboarding_ppa_more_info_rki_data_body_2" /> - <TextView - android:id="@+id/rki_data_point_16_text" - style="@style/subtitle" - android:layout_width="0dp" + <TableLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_rki_data_point_16_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_15_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/rki_data_point_16" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/rki_data_point_16_text" - app:srcCompat="@drawable/bullet_point" /> + android:layout_marginTop="@dimen/spacing_small"> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_10" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_10_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_10_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_11" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_11_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_11_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_12" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_12_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_12_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_13" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_13_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_13_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_14" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_14_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_14_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_15" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_15_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_15_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/rki_data_point_16" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/rki_data_point_16_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_rki_data_point_16_text" /> + + </TableRow> + + </TableLayout> <TextView android:id="@+id/other_info_body" style="@style/body1" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/guideline_start" - android:layout_marginTop="23dp" - android:layout_marginEnd="@dimen/guideline_end" + android:layout_marginTop="@dimen/spacing_normal" android:contentDescription="@string/onboarding_ppa_more_info_other_info_body" android:focusable="true" - android:text="@string/onboarding_ppa_more_info_other_info_body" - app:layout_constraintEnd_toEndOf="@id/body_end" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/rki_data_point_16_text" /> - - <TextView - android:id="@+id/other_info_point_1_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="23dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_other_info_point_1_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/other_info_body" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/other_info_point_1" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/other_info_point_1_text" - app:srcCompat="@drawable/bullet_point" /> + android:text="@string/onboarding_ppa_more_info_other_info_body" /> - <TextView - android:id="@+id/other_info_point_3_text" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_other_info_point_2_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/other_info_point_1_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/other_info_point_3" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/other_info_point_3_text" - app:srcCompat="@drawable/bullet_point" /> - - <TextView - android:id="@+id/other_info_point_4_text" - style="@style/subtitle" - android:layout_width="0dp" + <TableLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="13dp" - android:layout_marginTop="15dp" - android:focusable="true" - android:text="@string/onboarding_ppa_more_info_other_info_point_3_text" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/other_info_point_3_text" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/other_info_point_4" - android:layout_width="@dimen/bullet_point_size" - android:layout_height="@dimen/bullet_point_size" - android:layout_marginTop="@dimen/spacing_tiny" - android:importantForAccessibility="no" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toTopOf="@id/other_info_point_4_text" - app:srcCompat="@drawable/bullet_point" /> + android:layout_marginTop="@dimen/spacing_small"> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/other_info_point_1" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/other_info_point_1_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_other_info_point_1_text" /> + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/other_info_point_3" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/other_info_point_3_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_other_info_point_2_text" /> + + </TableRow> + + <TableRow + android:layout_marginTop="@dimen/spacing_small"> + + <ImageView + android:id="@+id/other_info_point_4" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:layout_marginTop="@dimen/spacing_tiny" + android:importantForAccessibility="no" + app:srcCompat="@drawable/bullet_point" /> + + <TextView + android:id="@+id/other_info_point_4_text" + style="@style/subtitle" + android:layout_weight="1" + android:layout_marginStart="13dp" + android:focusable="true" + android:text="@string/onboarding_ppa_more_info_other_info_point_3_text" /> + + </TableRow> + + </TableLayout> <TextView android:id="@+id/much_privacy_body" style="@style/body1" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/guideline_start" - android:layout_marginTop="35dp" - android:layout_marginEnd="@dimen/guideline_end" + android:layout_marginTop="@dimen/spacing_medium" android:contentDescription="@string/onboarding_ppa_more_info_much_privacy_body" android:focusable="true" - android:paddingBottom="51dp" - android:text="@string/onboarding_ppa_more_info_much_privacy_body" - app:layout_constraintEnd_toEndOf="@id/body_end" - app:layout_constraintStart_toStartOf="@id/body_start" - app:layout_constraintTop_toBottomOf="@+id/other_info_point_4_text" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@+id/body_start" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_begin="@dimen/guideline_start" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@+id/body_end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_end="@dimen/guideline_end" /> - - <include layout="@layout/merge_guidelines_card" /> + android:text="@string/onboarding_ppa_more_info_much_privacy_body" /> - </androidx.constraintlayout.widget.ConstraintLayout> + </LinearLayout> </ScrollView> - <androidx.constraintlayout.widget.Guideline - android:id="@+id/guideline_bottom" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/guideline_bottom" /> - - </androidx.constraintlayout.widget.ConstraintLayout> + </LinearLayout> </layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_settings.xml b/Corona-Warn-App/src/main/res/layout/fragment_settings.xml index e80f31e3e0f6ca848e59ba75f3ca403dfa2c933e..48388cf2745a58fb247bc93d6ac7184f87decde3 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_settings.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_settings.xml @@ -43,7 +43,10 @@ android:layout_width="0dp" android:layout_height="0dp" android:fillViewport="true" - app:layout_constraintBottom_toBottomOf="@+id/guideline_bottom" + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/settings_header"> @@ -158,13 +161,6 @@ </ScrollView> - <androidx.constraintlayout.widget.Guideline - android:id="@+id/guideline_bottom" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/guideline_bottom" /> - </androidx.constraintlayout.widget.ConstraintLayout> </layout> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_settings_background_priority.xml b/Corona-Warn-App/src/main/res/layout/fragment_settings_background_priority.xml index cb4c410114df6878889a7a376f6a81b06d299e19..b3a12bb191abaa9325bacb7c94c9a710e90ed00a 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_settings_background_priority.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_settings_background_priority.xml @@ -31,7 +31,10 @@ android:layout_width="0dp" android:layout_height="0dp" android:fillViewport="true" - app:layout_constraintBottom_toBottomOf="@+id/guideline_bottom" + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/settings_background_priority_header"> @@ -120,13 +123,6 @@ </ScrollView> - <androidx.constraintlayout.widget.Guideline - android:id="@+id/guideline_bottom" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/guideline_bottom" /> - </androidx.constraintlayout.widget.ConstraintLayout> </layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_settings_notifications.xml b/Corona-Warn-App/src/main/res/layout/fragment_settings_notifications.xml index f2a5f0f15f992c625cf64c554ba0a700fe76cc24..f97962270f1d8305cd082be0e9b76bbf5ce632c0 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_settings_notifications.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_settings_notifications.xml @@ -32,7 +32,10 @@ android:layout_width="0dp" android:layout_height="0dp" android:fillViewport="true" - app:layout_constraintBottom_toBottomOf="@+id/guideline_bottom" + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/settings_notifications_header"> @@ -120,13 +123,6 @@ </ScrollView> - <androidx.constraintlayout.widget.Guideline - android:id="@+id/guideline_bottom" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/guideline_bottom" /> - </androidx.constraintlayout.widget.ConstraintLayout> </layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_settings_privacy_preserving_analytics.xml b/Corona-Warn-App/src/main/res/layout/fragment_settings_privacy_preserving_analytics.xml index 21731d367ee1d3f4849102e6fe107334e371ba31..b158a7869eea4a58699eae92cbd43eb8e416905d 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_settings_privacy_preserving_analytics.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_settings_privacy_preserving_analytics.xml @@ -27,7 +27,10 @@ android:id="@+id/scrollview" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintBottom_toTopOf="@+id/guideline_bottom" + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/settings_ppa_header"> @@ -377,33 +380,5 @@ </ScrollView> - <androidx.constraintlayout.widget.Guideline - android:id="@id/guideline_top" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_begin="@dimen/guideline_top" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@id/guideline_bottom" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/spacing_small" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@id/guideline_start" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_begin="@dimen/guideline_start" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@id/guideline_end" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - app:layout_constraintGuide_end="@dimen/guideline_end" /> - </androidx.constraintlayout.widget.ConstraintLayout> </layout> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_statistics_explanation.xml b/Corona-Warn-App/src/main/res/layout/fragment_statistics_explanation.xml index 3a77158151f358e1e8e26e462e67c80f7fdaef76..082aa18e15213023e746986a0d2735bad6b20ecd 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_statistics_explanation.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_statistics_explanation.xml @@ -93,7 +93,10 @@ <ScrollView android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintBottom_toBottomOf="@id/guideline_bottom" + android:paddingBottom="@dimen/spacing_normal" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="1.0" app:layout_constraintStart_toStartOf="parent" @@ -426,9 +429,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="@dimen/guideline_start" - android:layout_marginTop="24dp" + android:layout_marginTop="@dimen/spacing_normal" android:layout_marginEnd="@dimen/guideline_end" - android:layout_marginBottom="23dp" android:contentDescription="@string/statistics_explanation_trend_description" android:focusable="true" android:text="@string/statistics_explanation_trend_description" /> @@ -437,13 +439,6 @@ </ScrollView> - <androidx.constraintlayout.widget.Guideline - android:id="@+id/guideline_bottom" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/guideline_bottom" /> - <include layout="@layout/merge_guidelines_side" /> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_deletion_warning.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_deletion_warning.xml index 27fdf7bca4367c65b6311fa1732c57cf836dd3b2..5b89dec0d5222de7328ab7f2a25781a5dc5c3c18 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_deletion_warning.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_deletion_warning.xml @@ -18,19 +18,6 @@ app:navigationIcon="@drawable/ic_close" app:title="@string/submission_deletion_warning_title" /> - <ProgressBar - android:id="@+id/submission_qr_code_scan_spinner" - style="?android:attr/progressBarStyle" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - android:visibility="gone" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:visibility="visible" /> - <ScrollView android:layout_width="0dp" android:layout_height="0dp" @@ -114,5 +101,19 @@ app:layout_constraintStart_toStartOf="parent" tools:text="@string/submission_deletion_warning_continue_button" /> + <ProgressBar + android:id="@+id/submission_qr_code_scan_spinner" + style="?android:attr/progressBarStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:indeterminateTint="@color/colorAccentTintIcon" + android:indeterminateTintMode="src_in" + android:layout_marginBottom="@dimen/spacing_normal" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/continue_button" + app:layout_constraintEnd_toEndOf="@id/continue_button" + app:layout_constraintStart_toStartOf="@id/continue_button" + tools:visibility="visible" /> + </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_positive_keys_shared.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_positive_keys_shared.xml new file mode 100644 index 0000000000000000000000000000000000000000..7f26366943bd40fbdf4faf1306c7d6efcd6be2bc --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_positive_keys_shared.xml @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/submission_test_result_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:accessibilityLiveRegion="assertive" + android:contentDescription="@string/submission_test_result_headline"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar.Close" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:title="@string/submission_test_result_consent_given_heading" /> + + <ScrollView + android:id="@+id/scroll_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_marginBottom="12dp" + android:fillViewport="true" + app:layout_constraintBottom_toTopOf="@id/delete_test" + app:layout_constraintTop_toBottomOf="@id/toolbar"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="true"> + + <de.rki.coronawarnapp.ui.view.TestResultSectionView + android:id="@+id/submission_test_result_section" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="12dp" + android:focusable="true" + android:importantForAccessibility="yes" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/submission_done_text" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:focusable="true" + android:text="@string/submission_done_body" + android:textStyle="bold" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/submission_test_result_section" /> + + <TextView + android:id="@+id/submission_done_subtitle" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" + android:focusable="true" + android:text="@string/submission_done_subtitle" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/submission_done_text" /> + + <include + android:id="@+id/submission_done_pcr_validation" + layout="@layout/include_submission_behaviour_row" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:body="@{@string/submission_done_pcr_validation}" + app:icon="@{@drawable/ic_faq_information}" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/submission_done_subtitle" /> + + <include + android:id="@+id/submission_done_contagious" + layout="@layout/include_submission_behaviour_row" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:body="@{@string/submission_done_contagious}" + app:icon="@{@drawable/ic_phone_white}" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/submission_done_pcr_validation" /> + + <include + android:id="@+id/submission_done_isolate" + layout="@layout/include_submission_behaviour_row" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:body="@{@string/submission_done_isolate}" + app:icon="@{@drawable/ic_risk_details_home}" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/submission_done_contagious" /> + + <include + layout="@layout/include_submission_done_further_info" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/submission_done_isolate" /> + + <include layout="@layout/merge_guidelines_side" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + </ScrollView> + + <Button + android:id="@+id/delete_test" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginVertical="16dp" + android:text="@string/submission_test_result_pending_remove_test_button" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/guideline_action" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline_action" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="@dimen/guideline_action" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline_action_large" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="@dimen/guideline_action_large" /> + + <include layout="@layout/merge_guidelines_side" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml b/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml index fd7e77dd2b97a4de774bf6831549a61990609b76..d86a5a7de4b057adcb9d0cd8756c793e75343d38 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml @@ -20,9 +20,10 @@ style="@style/buttonPrimary" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginHorizontal="24dp" - android:layout_marginBottom="24dp" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginVertical="@dimen/spacing_small" android:text="@string/trace_location_onboarding_body_confirm" + app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_scroll_view" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> @@ -31,7 +32,9 @@ android:id="@+id/check_in_onboarding_scroll_view" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_marginBottom="20dp" + android:paddingBottom="@dimen/spacing_small" + android:clipToPadding="false" + android:scrollbarStyle="outsideOverlay" app:layout_constraintBottom_toTopOf="@+id/check_in_onboarding_acknowledge" app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_toolbar"> diff --git a/Corona-Warn-App/src/main/res/layout/view_diary_circumstances_textview.xml b/Corona-Warn-App/src/main/res/layout/view_diary_circumstances_textview.xml index 17a22866f6708fd479dc41c16d4257a5908205ce..8c410cd33f534b2a1076b68d25282496c2c2ccf2 100644 --- a/Corona-Warn-App/src/main/res/layout/view_diary_circumstances_textview.xml +++ b/Corona-Warn-App/src/main/res/layout/view_diary_circumstances_textview.xml @@ -13,7 +13,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" app:hintEnabled="false" - app:layout_constraintEnd_toStartOf="@id/info_button" + app:layout_constraintEnd_toStartOf="@id/spacer_info_start" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/environment_group"> @@ -29,6 +29,12 @@ </com.google.android.material.textfield.TextInputLayout> + <Space + android:id="@+id/spacer_info_start" + android:layout_width="5dp" + android:layout_height="0dp" + app:layout_constraintEnd_toStartOf="@id/info_button" /> + <ImageButton android:id="@+id/info_button" style="@style/ContactDiaryInfoButton" diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml index b40d39ed43bae4eee20080f87ccb6de01a19bb59..d48804ccba58ce2abd52de0b7ad0e7a1e526a0ee 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -70,6 +70,9 @@ <action android:id="@+id/action_mainFragment_to_submissionNegativeAntigenTestResultFragment" app:destination="@id/submissionNegativeAntigenTestResultFragment" /> + <action + android:id="@+id/action_mainFragment_to_submissionTestResultKeysSharedFragment" + app:destination="@id/submissionTestResultKeysSharedFragment" /> </fragment> <fragment @@ -794,4 +797,13 @@ android:name="testType" app:argType="de.rki.coronawarnapp.coronatest.type.CoronaTest$Type"/> </fragment> + <fragment + android:id="@+id/submissionTestResultKeysSharedFragment" + android:name="de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultKeysSharedFragment" + android:label="SubmissionTestResultKeysSharedFragment" + tools:layout="@layout/fragment_submission_test_result_positive_keys_shared"> + <argument + android:name="testType" + app:argType="de.rki.coronawarnapp.coronatest.type.CoronaTest$Type" /> + </fragment> </navigation> diff --git a/Corona-Warn-App/src/main/res/values-bg/strings.xml b/Corona-Warn-App/src/main/res/values-bg/strings.xml index c45a642f087a824de2e3ba8d106090c4fc677387..d6e4047261ad0a665bda816dac8a429dfdd65589 100644 --- a/Corona-Warn-App/src/main/res/values-bg/strings.xml +++ b/Corona-Warn-App/src/main/res/values-bg/strings.xml @@ -1377,24 +1377,6 @@ <!-- YTXT: text for share result card--> <string name="submission_status_card_positive_result_share">"Споделете Ñвоите Ñлучайни ИД кодове, за да предупредите оÑтаналите потребители."</string> - <!-- Reenable risk card --> - <!-- XHED: Card title for re-enable risk card --> - <string name="reenable_risk_card_title">"Проверката за излагане на риÑк е ÑпрÑна"</string> - <!-- YTXT: Body text for test registration date --> - <string name="reenable_risk_card_test_registration_string">"Дата на региÑтриране на теÑта: %s"</string> - <!-- YTXT: Description text for re-enable risk card --> - <string name="reenable_risk_card_description_text">"Ðвтоматичната проверка за излагане на риÑк е ÑпрÑна, защото Ви е била поÑтавена диагноза „коронавируÑ“ и понаÑтоÑщем Ñте в изолациÑ. Можете да активирате проверката отново по вÑÑко време, като в този Ñлучай региÑтрираниÑÑ‚ от Ð’Ð°Ñ Ñ‚ÐµÑÑ‚ ще бъде изтрит автоматично."</string> - <!-- XBUT: Button for re-enabling risk calculation --> - <string name="reenable_risk_card_button_text">"Включване на проверката за излагане на риÑк "</string> - <!-- XHED: Dialog title for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_title">"Ðктивиране на проверката за излагане на риÑк"</string> - <!-- YTXT: Dialog text for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_message">"Ð¢ÐµÐºÑƒÑ‰Ð¸Ñ Ð’Ð¸ теÑÑ‚ ще бъде изтрит от приложението. Ðко желаете, може да региÑтрирате нов теÑÑ‚."</string> - <!-- XBUT: Positive button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_positive">"Ðктивиране"</string> - <!-- XBUT: Negative button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_negative">"Отказ"</string> - <!-- Test Result Card --> <string name="test_result_card_headline">"Вашата диагноза %s:"</string> <!-- YTXT: virus name text --> diff --git a/Corona-Warn-App/src/main/res/values-en/contact_diary_strings.xml b/Corona-Warn-App/src/main/res/values-en/contact_diary_strings.xml index 97bab3bbeda1f7ab22d67db2b520de5abd020266..6d2df0cbdfe9c54d7f90bea7d579bbc439c72712 100644 --- a/Corona-Warn-App/src/main/res/values-en/contact_diary_strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/contact_diary_strings.xml @@ -13,7 +13,7 @@ <string name="contact_diary_add_person_title">"Person"</string> <string name="contact_diary_add_person_text_input_name_hint">"Name"</string> <string name="contact_diary_add_text_input_phone_hint">"Phone"</string> - <string name="contact_diary_add_text_input_email_hint">"e-mail"</string> + <string name="contact_diary_add_text_input_email_hint">"E-mail"</string> <string name="contact_diary_add_person_save_button">"Save"</string> <string name="contact_diary_add_location_title">"Place"</string> <string name="contact_diary_add_location_text_input_hint">"Description"</string> diff --git a/Corona-Warn-App/src/main/res/values-en/strings.xml b/Corona-Warn-App/src/main/res/values-en/strings.xml index 0d164ebd5a791e4ca0c76a4f6c4469b5eaf2b1a4..190cd0d62a392b7ba3bb2f013052aa2acc7ebcd8 100644 --- a/Corona-Warn-App/src/main/res/values-en/strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/strings.xml @@ -1377,24 +1377,6 @@ <!-- YTXT: text for share result card--> <string name="submission_status_card_positive_result_share">"Share your random IDs so that others can be warned."</string> - <!-- Reenable risk card --> - <!-- XHED: Card title for re-enable risk card --> - <string name="reenable_risk_card_title">"Exposure Check Ended"</string> - <!-- YTXT: Body text for test registration date --> - <string name="reenable_risk_card_test_registration_string">"Test registered on %s"</string> - <!-- YTXT: Description text for re-enable risk card --> - <string name="reenable_risk_card_description_text">"The automatic exposure check has ended because you have been diagnosed with coronavirus and are currently in isolation. You can reactivate the exposure test at any time. If you do, your registered test will be deleted automatically."</string> - <!-- XBUT: Button for re-enabling risk calculation --> - <string name="reenable_risk_card_button_text">"Turn On Exposure Check "</string> - <!-- XHED: Dialog title for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_title">"Activate exposure check."</string> - <!-- YTXT: Dialog text for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_message">"Your current test will be deleted from the app, so you can register a new test if necessary."</string> - <!-- XBUT: Positive button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_positive">"Activate"</string> - <!-- XBUT: Negative button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_negative">"Cancel"</string> - <!-- Test Result Card --> <string name="test_result_card_headline">"Your %s diagnosis:"</string> <!-- YTXT: virus name text --> diff --git a/Corona-Warn-App/src/main/res/values-pl/strings.xml b/Corona-Warn-App/src/main/res/values-pl/strings.xml index 5787cb0fbd4b8e97f54944aed4ce0a45f24288bd..dfa08080011ccd3433acf3ead09ff32fb70900c4 100644 --- a/Corona-Warn-App/src/main/res/values-pl/strings.xml +++ b/Corona-Warn-App/src/main/res/values-pl/strings.xml @@ -1377,24 +1377,6 @@ <!-- YTXT: text for share result card--> <string name="submission_status_card_positive_result_share">"UdostÄ™pnij swoje losowe identyfikatory, aby umożliwić ostrzeganie innych osób."</string> - <!-- Reenable risk card --> - <!-- XHED: Card title for re-enable risk card --> - <string name="reenable_risk_card_title">"ZakoÅ„czono sprawdzanie narażeÅ„"</string> - <!-- YTXT: Body text for test registration date --> - <string name="reenable_risk_card_test_registration_string">"Test zarejestrowany dnia %s"</string> - <!-- YTXT: Description text for re-enable risk card --> - <string name="reenable_risk_card_description_text">"Automatyczne sprawdzanie narażeÅ„ zakoÅ„czyÅ‚o siÄ™, ponieważ zdiagnozowano u Ciebie koronawirusa i przebywasz obecnie na izolacji. W każdej chwili możesz ponownie aktywować test narażenia. JeÅ›li to zrobisz, zarejestrowany test zostanie automatycznie usuniÄ™ty."</string> - <!-- XBUT: Button for re-enabling risk calculation --> - <string name="reenable_risk_card_button_text">"WÅ‚Ä…cz sprawdzanie narażeÅ„ "</string> - <!-- XHED: Dialog title for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_title">"Aktywuj sprawdzanie narażeÅ„."</string> - <!-- YTXT: Dialog text for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_message">"Twój bieżący test zostanie usuniÄ™ty z aplikacji, wiÄ™c w razie potrzeby możesz zarejestrować nowy test."</string> - <!-- XBUT: Positive button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_positive">"Aktywuj"</string> - <!-- XBUT: Negative button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_negative">"Anuluj"</string> - <!-- Test Result Card --> <string name="test_result_card_headline">"Twoja %s diagnoza:"</string> <!-- YTXT: virus name text --> diff --git a/Corona-Warn-App/src/main/res/values-ro/contact_diary_strings.xml b/Corona-Warn-App/src/main/res/values-ro/contact_diary_strings.xml index 3811f313e6f41eb4828478d4690cedf83dd0bde4..7ea5c62fb7d3b08de3f8d36025c9ae153e599d0c 100644 --- a/Corona-Warn-App/src/main/res/values-ro/contact_diary_strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/contact_diary_strings.xml @@ -13,7 +13,7 @@ <string name="contact_diary_add_person_title">"Persoană"</string> <string name="contact_diary_add_person_text_input_name_hint">"Nume"</string> <string name="contact_diary_add_text_input_phone_hint">"Telefon"</string> - <string name="contact_diary_add_text_input_email_hint">"e-mail"</string> + <string name="contact_diary_add_text_input_email_hint">"E-mail"</string> <string name="contact_diary_add_person_save_button">"Salvare"</string> <string name="contact_diary_add_location_title">"Loc"</string> <string name="contact_diary_add_location_text_input_hint">"Descriere"</string> diff --git a/Corona-Warn-App/src/main/res/values-ro/strings.xml b/Corona-Warn-App/src/main/res/values-ro/strings.xml index 3e92702f112ff5ae91283f499a070d7d5b7eeeb6..bc5edb78491b079299ea5ec7b3c83c35e93a9864 100644 --- a/Corona-Warn-App/src/main/res/values-ro/strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/strings.xml @@ -1377,24 +1377,6 @@ <!-- YTXT: text for share result card--> <string name="submission_status_card_positive_result_share">"TrimiteÈ›i ID-ul dvs. aleatoriu pentru a-i avertiza È™i pe alÈ›ii."</string> - <!-- Reenable risk card --> - <!-- XHED: Card title for re-enable risk card --> - <string name="reenable_risk_card_title">"Verificarea expunerii s-a încheiat"</string> - <!-- YTXT: Body text for test registration date --> - <string name="reenable_risk_card_test_registration_string">"Test înregistrat pe %s"</string> - <!-- YTXT: Description text for re-enable risk card --> - <string name="reenable_risk_card_description_text">"Verificarea automată a expunerii s-a încheiat deoarece aÈ›i fost diagnosticat cu coronavirus È™i în prezent sunteÈ›i în izolare. PuteÈ›i reactiva oricând testarea expunerii. Dacă faceÈ›i acest lucru, testul dvs. înregistrat va fi È™ters automat."</string> - <!-- XBUT: Button for re-enabling risk calculation --> - <string name="reenable_risk_card_button_text">"Pornire verificarea expunerii "</string> - <!-- XHED: Dialog title for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_title">"ActivaÈ›i verificarea expunerii."</string> - <!-- YTXT: Dialog text for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_message">"Testul dvs. curent va fi È™ters din aplicaÈ›ie pentru a putea înregistra un test nou dacă este necesar."</string> - <!-- XBUT: Positive button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_positive">"Activare"</string> - <!-- XBUT: Negative button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_negative">"Anulare"</string> - <!-- Test Result Card --> <string name="test_result_card_headline">"Diagnosticul dvs. pentru %s:"</string> <!-- YTXT: virus name text --> diff --git a/Corona-Warn-App/src/main/res/values-tr/contact_diary_strings.xml b/Corona-Warn-App/src/main/res/values-tr/contact_diary_strings.xml index ba14375c44e5d0603fb96eed435c86613589e86c..9424c7dacbbf20fa402c46bbec22c80da392f3de 100644 --- a/Corona-Warn-App/src/main/res/values-tr/contact_diary_strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/contact_diary_strings.xml @@ -13,7 +13,7 @@ <string name="contact_diary_add_person_title">"KiÅŸi"</string> <string name="contact_diary_add_person_text_input_name_hint">"Ad"</string> <string name="contact_diary_add_text_input_phone_hint">"Telefon"</string> - <string name="contact_diary_add_text_input_email_hint">"e-posta"</string> + <string name="contact_diary_add_text_input_email_hint">"E-posta"</string> <string name="contact_diary_add_person_save_button">"Kaydet"</string> <string name="contact_diary_add_location_title">"Yer"</string> <string name="contact_diary_add_location_text_input_hint">"Tanım"</string> diff --git a/Corona-Warn-App/src/main/res/values-tr/strings.xml b/Corona-Warn-App/src/main/res/values-tr/strings.xml index f283c8543139f0c1598b4c385fb32069a98ce4be..ea1fe7e70a82edfff130055bf9c6215115837f45 100644 --- a/Corona-Warn-App/src/main/res/values-tr/strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/strings.xml @@ -1377,24 +1377,6 @@ <!-- YTXT: text for share result card--> <string name="submission_status_card_positive_result_share">"DiÄŸer kullanıcıların uyarılabilmesi için rastgele kimliklerinizi paylaşın."</string> - <!-- Reenable risk card --> - <!-- XHED: Card title for re-enable risk card --> - <string name="reenable_risk_card_title">"Maruz Kalma Denetimi Sona Erdi"</string> - <!-- YTXT: Body text for test registration date --> - <string name="reenable_risk_card_test_registration_string">"Test %s tarihinde kaydedildi"</string> - <!-- YTXT: Description text for re-enable risk card --> - <string name="reenable_risk_card_description_text">"Koronavirüs tanısı aldığınız ve ÅŸu anda izolasyonda olduÄŸunuz için otomatik maruz kalma denetimi sona erdi. Maruz kalma testini dilediÄŸiniz zaman yeniden etkinleÅŸtirebilirsiniz. EtkinleÅŸtirirseniz kayıtlı testiniz otomatik olarak silinir."</string> - <!-- XBUT: Button for re-enabling risk calculation --> - <string name="reenable_risk_card_button_text">"Maruz Kalma Denetimini Aç "</string> - <!-- XHED: Dialog title for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_title">"Maruz kalma denetimini etkinleÅŸtirin."</string> - <!-- YTXT: Dialog text for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_message">"Gerekli olması halinde yeni bir test kaydedebilmeniz için mevcut testiniz uygulamadan silinecek."</string> - <!-- XBUT: Positive button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_positive">"EtkinleÅŸtir"</string> - <!-- XBUT: Negative button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_negative">"Ä°ptal"</string> - <!-- Test Result Card --> <string name="test_result_card_headline">"%s tanınız:"</string> <!-- YTXT: virus name text --> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index 0520c47e8391cbd5911885ee01fdbee4269704b6..fb1e73aba05fe6b365a26adef014ed754427669e 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -1386,24 +1386,6 @@ <!-- YTXT: text for share result card--> <string name="submission_status_card_positive_result_share">"Share your random IDs so that others can be warned."</string> - <!-- Reenable risk card --> - <!-- XHED: Card title for re-enable risk card --> - <string name="reenable_risk_card_title">"Exposure Check Ended"</string> - <!-- YTXT: Body text for test registration date --> - <string name="reenable_risk_card_test_registration_string">"Test registered on %s"</string> - <!-- YTXT: Description text for re-enable risk card --> - <string name="reenable_risk_card_description_text">"The automatic exposure check has ended because you have been diagnosed with coronavirus and are currently in isolation. You can reactivate the exposure test at any time. If you do, your registered test will be deleted automatically."</string> - <!-- XBUT: Button for re-enabling risk calculation --> - <string name="reenable_risk_card_button_text">"Turn On Exposure Check "</string> - <!-- XHED: Dialog title for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_title">"Activate exposure check."</string> - <!-- YTXT: Dialog text for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_message">"Your current test will be deleted from the app, so you can register a new test if necessary."</string> - <!-- XBUT: Positive button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_positive">"Activate"</string> - <!-- XBUT: Negative button for reactivate risk calculation --> - <string name="dialog_reactivate_risk_calculation_button_negative">"Cancel"</string> - <!-- Test Result Card --> <string name="test_result_card_headline">"Your %s diagnosis:"</string> <!-- YTXT: virus name text --> diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index f8c051894a0b6af393e988d086a2dde72b5382dd..be7743ea4251a325806bf29993af49ddbaaeba5d 100644 --- a/Corona-Warn-App/src/main/res/values/styles.xml +++ b/Corona-Warn-App/src/main/res/values/styles.xml @@ -9,6 +9,8 @@ <item name="materialCalendarTheme">@style/ThemeOverlay.App.DatePicker</item> <item name="materialTimePickerTheme">@style/ThemeOverlay.App.TimePicker</item> + + <item name="buttonStyle">@style/Widget.AppCompat.Button.Borderless.Colored</item> </style> <style name="ThemeOverlay.App.DatePicker" parent="ThemeOverlay.MaterialComponents.MaterialCalendar"> @@ -490,8 +492,8 @@ <item name="android:height">@dimen/button_icon</item> <item name="android:width">@dimen/button_icon</item> <item name="android:src">@drawable/ic_info</item> - <item name="android:paddingStart">12dp</item> - <item name="android:paddingEnd">12dp</item> + <item name="android:paddingStart">7dp</item> + <item name="android:paddingEnd">7dp</item> <item name="android:paddingTop">7dp</item> <item name="android:paddingBottom">7dp</item> <item name="android:contentDescription">@string/statistics_info_button</item> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CoronaTestParametersDistinctTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CoronaTestParametersDistinctTest.kt index 1f71e9b78d2c72f0df2989e4a752b29bfdc5147e..772016b4be0eda0504f68f311bafd41f9eceb6a7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CoronaTestParametersDistinctTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CoronaTestParametersDistinctTest.kt @@ -7,19 +7,22 @@ import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Duration import org.junit.Test import testhelpers.BaseTest class CoronaTestParametersDistinctTest : BaseTest() { + private val durationOf48H: Duration = Duration.standardHours(48) + private val durationOf12H: Duration = Duration.standardHours(12) @Test fun `can we use distinctUntilChanged on CoronaTestParameters`() = runBlockingTest { val flow = flow { - emit(CoronaRapidAntigenTestParametersContainer(48)) - emit(CoronaRapidAntigenTestParametersContainer(48)) - emit(CoronaRapidAntigenTestParametersContainer(12)) + emit(CoronaRapidAntigenTestParametersContainer(durationOf48H)) + emit(CoronaRapidAntigenTestParametersContainer(Duration.standardHours(48))) + emit(CoronaRapidAntigenTestParametersContainer(durationOf12H)) } - flow.distinctUntilChanged().drop(1).first().hoursToDeemTestOutdated shouldBe 12 + flow.distinctUntilChanged().drop(1).first().hoursToDeemTestOutdated shouldBe durationOf12H } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt index e5fffdcd31f6232ee060e0dd184e6e448bfb5621..c622c01cdbee90db12d47b2ba57257972322327d 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt @@ -229,6 +229,35 @@ class AppConfigServerTest : BaseIOTest() { ) } + @Test + fun `cache control with max-age=0 defaults to 300 seconds`() = runBlockingTest { + coEvery { api.getApplicationConfiguration() } returns Response.success( + APPCONFIG_BUNDLE.toResponseBody(), + Headers.headersOf( + "Date", + "Tue, 03 Nov 2020 08:46:03 GMT", + "ETag", + "I am an ETag :)!", + "Cache-Control", + "max-age=0, no-cache, no-store" + ) + ) + + val downloadServer = createInstance() + + val configDownload = downloadServer.downloadAppConfig() + configDownload shouldBe InternalConfigData( + rawData = APPCONFIG_RAW, + serverTime = Instant.parse("2020-11-03T08:46:03.000Z"), + localOffset = Duration( + Instant.parse("2020-11-03T08:46:03.000Z"), + Instant.ofEpochMilli(123456789) + ), + etag = "I am an ETag :)!", + cacheValidity = Duration.standardSeconds(300) + ) + } + companion object { private val APPCONFIG_BUNDLE = ( diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotificationServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotificationServiceTest.kt index 7e4f8ecd181e644e15a99cf419fbe757ae7711b0..9bcf463acf45f9ebbc60c1a16fdc6da2336355de 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotificationServiceTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/notification/ShareTestResultNotificationServiceTest.kt @@ -2,6 +2,8 @@ package de.rki.coronawarnapp.coronatest.notification import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR +import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.RAPID_ANTIGEN import de.rki.coronawarnapp.main.CWASettings import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -26,22 +28,29 @@ class ShareTestResultNotificationServiceTest : BaseTest() { private val coronaTestFlow = MutableStateFlow( emptySet<CoronaTest>() ) - private var numberOfRemainingSharePositiveTestResultReminders: Int = Int.MIN_VALUE + private var numberOfRemainingSharePositiveTestResultRemindersPcr: Int = Int.MIN_VALUE + private var numberOfRemainingSharePositiveTestResultRemindersRat: Int = Int.MIN_VALUE @BeforeEach fun setup() { MockKAnnotations.init(this) every { coronaTestRepository.coronaTests } returns coronaTestFlow - every { cwaSettings.numberOfRemainingSharePositiveTestResultReminders = any() } answers { - numberOfRemainingSharePositiveTestResultReminders = arg(0) + every { cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr = any() } answers { + numberOfRemainingSharePositiveTestResultRemindersPcr = arg(0) } - every { cwaSettings.numberOfRemainingSharePositiveTestResultReminders } answers { - numberOfRemainingSharePositiveTestResultReminders + every { cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr } answers { + numberOfRemainingSharePositiveTestResultRemindersPcr + } + every { cwaSettings.numberOfRemainingSharePositiveTestResultRemindersRat = any() } answers { + numberOfRemainingSharePositiveTestResultRemindersRat = arg(0) + } + every { cwaSettings.numberOfRemainingSharePositiveTestResultRemindersRat } answers { + numberOfRemainingSharePositiveTestResultRemindersRat } - every { shareTestResultNotification.showSharePositiveTestResultNotification(any()) } just Runs - every { shareTestResultNotification.cancelSharePositiveTestResultNotification() } just Runs - every { shareTestResultNotification.scheduleSharePositiveTestResultReminder() } just Runs + every { shareTestResultNotification.showSharePositiveTestResultNotification(any(), any()) } just Runs + every { shareTestResultNotification.cancelSharePositiveTestResultNotification(any()) } just Runs + every { shareTestResultNotification.scheduleSharePositiveTestResultReminder(any()) } just Runs } private fun createInstance(scope: CoroutineScope) = ShareTestResultNotificationService( @@ -57,6 +66,12 @@ class ShareTestResultNotificationServiceTest : BaseTest() { coronaTestFlow.value = setOf( mockk<CoronaTest>().apply { + every { type } returns PCR + every { isSubmissionAllowed } returns true + every { isSubmitted } returns false + }, + mockk<CoronaTest>().apply { + every { type } returns RAPID_ANTIGEN every { isSubmissionAllowed } returns true every { isSubmitted } returns false } @@ -64,54 +79,49 @@ class ShareTestResultNotificationServiceTest : BaseTest() { instance.setup() - verify { shareTestResultNotification.scheduleSharePositiveTestResultReminder() } - verify { cwaSettings.numberOfRemainingSharePositiveTestResultReminders = 2 } + verify(exactly = 1) { shareTestResultNotification.scheduleSharePositiveTestResultReminder(PCR) } + verify(exactly = 1) { shareTestResultNotification.scheduleSharePositiveTestResultReminder(RAPID_ANTIGEN) } + + verify { cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr = 2 } + verify { cwaSettings.numberOfRemainingSharePositiveTestResultRemindersRat = 2 } } @Test fun `showing a notification consumes a token`() = runBlockingTest2(ignoreActive = true) { val instance = createInstance(this) - numberOfRemainingSharePositiveTestResultReminders = 2 + numberOfRemainingSharePositiveTestResultRemindersPcr = 2 + numberOfRemainingSharePositiveTestResultRemindersRat = 2 - instance.maybeShowSharePositiveTestResultNotification(1) + instance.maybeShowSharePositiveTestResultNotification(1, PCR) + instance.maybeShowSharePositiveTestResultNotification(1, RAPID_ANTIGEN) - numberOfRemainingSharePositiveTestResultReminders shouldBe 1 + numberOfRemainingSharePositiveTestResultRemindersPcr shouldBe 1 + numberOfRemainingSharePositiveTestResultRemindersRat shouldBe 1 - verify { shareTestResultNotification.showSharePositiveTestResultNotification(1) } + verify { shareTestResultNotification.showSharePositiveTestResultNotification(1, PCR) } + verify { shareTestResultNotification.showSharePositiveTestResultNotification(1, RAPID_ANTIGEN) } } @Test fun `if there are no tokens left to show a notification, cancel the current one`() = runBlockingTest2(ignoreActive = true) { val instance = createInstance(this) - numberOfRemainingSharePositiveTestResultReminders = 0 - instance.maybeShowSharePositiveTestResultNotification(1) - - numberOfRemainingSharePositiveTestResultReminders shouldBe 0 - - verify { shareTestResultNotification.cancelSharePositiveTestResultNotification() } + // PCR + numberOfRemainingSharePositiveTestResultRemindersPcr = 0 + instance.maybeShowSharePositiveTestResultNotification(1, PCR) + numberOfRemainingSharePositiveTestResultRemindersPcr shouldBe 0 + verify { shareTestResultNotification.cancelSharePositiveTestResultNotification(PCR) } + + // RAT + numberOfRemainingSharePositiveTestResultRemindersRat = 0 + instance.maybeShowSharePositiveTestResultNotification(1, RAPID_ANTIGEN) + numberOfRemainingSharePositiveTestResultRemindersRat shouldBe 0 + verify { shareTestResultNotification.cancelSharePositiveTestResultNotification(PCR) } } @Test - fun `any test which allowes submission triggers scheduling`() = runBlockingTest2(ignoreActive = true) { - val instance = createInstance(this) - - coronaTestFlow.value = setOf( - mockk<CoronaTest>().apply { - every { isSubmissionAllowed } returns true - every { isSubmitted } returns false - } - ) - - instance.setup() - - verify { shareTestResultNotification.scheduleSharePositiveTestResultReminder() } - verify { cwaSettings.numberOfRemainingSharePositiveTestResultReminders = 2 } - } - - @Test - fun `if there are no tests, we reset scheduling`() = runBlockingTest2(ignoreActive = true) { + fun `reset notification if no test is stored or test was deleted`() = runBlockingTest2(ignoreActive = true) { val instance = createInstance(this) coronaTestFlow.value = emptySet() @@ -120,7 +130,9 @@ class ShareTestResultNotificationServiceTest : BaseTest() { advanceUntilIdle() - verify { shareTestResultNotification.cancelSharePositiveTestResultNotification() } - verify { cwaSettings.numberOfRemainingSharePositiveTestResultReminders = Int.MIN_VALUE } + verify { shareTestResultNotification.cancelSharePositiveTestResultNotification(PCR) } + verify { shareTestResultNotification.cancelSharePositiveTestResultNotification(RAPID_ANTIGEN) } + verify { cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr = Int.MIN_VALUE } + verify { cwaSettings.numberOfRemainingSharePositiveTestResultRemindersRat = Int.MIN_VALUE } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt index 43e945ad847cf0fee318e804e26371eaa091752b..e78658b69175283df92b1e3ebb58182c62749a61 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt @@ -37,11 +37,11 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() { fun `personal data is extracted`() { val data = instance.extract(raQrCode3) data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN - data.hash shouldBe "7b1c063e883063f8c33ffaa256aded506afd907f7446143b3da0f938a21967a9" - data.createdAt shouldBe Instant.ofEpochMilli(1618563782000) - data.dateOfBirth shouldBe LocalDate.parse("1962-01-08") - data.lastName shouldBe "Hayes" - data.firstName shouldBe "Alma" + data.hash shouldBe "7dce08db0d4abd5ac1d2498b571afb221ca947c75c847d05466b4cfe9d95dc66" + data.createdAt shouldBe Instant.ofEpochMilli(1619618352000) + data.dateOfBirth shouldBe LocalDate.parse("1963-03-17") + data.lastName shouldBe "Tyler" + data.firstName shouldBe "Jacob" } @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt index c439951382bf5a228ab7b2a4f89b3c96611aff52..b29edd24deddfb62ba7a51a0d66a7c59a7376477 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt @@ -5,21 +5,21 @@ internal val pcrQrCode2 = "https://localhost/?123456-12345678-1234-4DA7-B166-B86 internal val pcrQrCode3 = "https://LOCALHOST/?123456-12345678-1234-4DA7-B166-B86D85475064" internal val raQrCode1 = - "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjA5NjQsInNhbHQiOiIwQ0ZEMUJCQzI2Q0FCODVCNkZFNDE5MTJFQjFBQUU1QUNEN0QyNjA0RTQwNTQyRUVEQjZEQUYyQkRBMDQ5QzRGIiwidGVzdElkIjoiYjM2YzUzN2ItZWQ5NC00Njc3LTkzZmQtODUwMTY4NjlkYjEwIiwiaGFzaCI6IjJiNTc0NjhlN2Q4MTkyMWQzOGM4OGI1NjExOWE0Y2ViMzYyNmI1MDM4ZWI5Njk3ZjkxOTQ4NmJjMzg0Y2U2M2UifQ" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzMTEsInNhbHQiOiJBODczOTVDRjYyMjc1QzRCQjczMjAxOERFRTRDQzhCRSIsInRlc3RpZCI6IjQwNDBkNTRlLWIzNmYtNGQ1Yi05MThiLTExODZjM2E0OTZhNSIsImhhc2giOiI4MzFhNzNmNGZhODZkMDdjMjViOTdjNzdiZjg5MzNhN2Q5MzAzODIxZDRjNzdiZDc5YzlkNzJlMmU0ZTI1MWYyIiwiZm4iOiJEeWxhbiIsImxuIjoiR2FyZGluZXIiLCJkb2IiOiIxOTY3LTEyLTIyIn0=" internal val raQrCode2 = - "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjA5NjQsInNhbHQiOiIwQ0ZEMUJCQzI2Q0FCODVCNkZFNDE5MTJFQjFBQUU1QUNEN0QyNjA0RTQwNTQyRUVEQjZEQUYyQkRBMDQ5QzRGIiwidGVzdElkIjoiYjM2YzUzN2ItZWQ5NC00Njc3LTkzZmQtODUwMTY4NjlkYjEwIiwiaGFzaCI6IjJiNTc0NjhlN2Q4MTkyMWQzOGM4OGI1NjExOWE0Y2ViMzYyNmI1MDM4ZWI5Njk3ZjkxOTQ4NmJjMzg0Y2U2M2UifQ" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzMzMsInNhbHQiOiI5Nzk5OUY4QTBFMjBBMDgxMDMyMDdGQzkxOEQzRTVFRiIsInRlc3RpZCI6IjE3ZjFlOGMxLTBiMWMtNDE1Ni1iMTZkLTlmMmQwNzEzMDJmNSIsImhhc2giOiIzMDcxNmQzM2FkNDFhZjQwNTk1Y2IyOThkMDcwMDllM2QwZjIxZDk5Njg4ZWZkOTIyNGQ4OWQ0OTQ3YjRkZDU3IiwiZm4iOiJIYXJyaWV0IiwibG4iOiJCbGFuYyIsImRvYiI6IjE5OTUtMDItMTUifQ==" internal val raQrCode3 = - "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM3ODIsInNhbHQiOiI1QTI3M0REREJCQTFEMkFDQUEzN0ExMDg4NjhGNkIwMjM3NjQzRjhBNjdCQTNENkQ3RUE3RkREQ0M0RDJGMjBEIiwidGVzdElkIjoiMGQ5ZTg0MzItZWI5MS00YzhmLTgyYWYtNWEwMWZiMWI2NzYwIiwiaGFzaCI6IjdiMWMwNjNlODgzMDYzZjhjMzNmZmFhMjU2YWRlZDUwNmFmZDkwN2Y3NDQ2MTQzYjNkYTBmOTM4YTIxOTY3YTkiLCJmbiI6IkFsbWEiLCJsbiI6IkhheWVzIiwiZG9iIjoiMTk2Mi0wMS0wOCJ9" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzNTIsInNhbHQiOiJDRjBDNTQ3RkY2RDMwOTlBMkIwNkMxQzRFNEYwOEFGOSIsInRlc3RpZCI6ImY0N2VmODA0LTZmMGMtNDhiMy1hODY5LWUyZjg4NmIxMjU0ZiIsImhhc2giOiI3ZGNlMDhkYjBkNGFiZDVhYzFkMjQ5OGI1NzFhZmIyMjFjYTk0N2M3NWM4NDdkMDU0NjZiNGNmZTlkOTVkYzY2IiwiZm4iOiJKYWNvYiIsImxuIjoiVHlsZXIiLCJkb2IiOiIxOTYzLTAzLTE3In0=" internal val raQrCode4 = - "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM3ODIsInNhbHQiOiI1QTI3M0REREJCQTFEMkFDQUEzN0ExMDg4NjhGNkIwMjM3NjQzRjhBNjdCQTNENkQ3RUE3RkREQ0M0RDJGMjBEIiwidGVzdElkIjoiMGQ5ZTg0MzItZWI5MS00YzhmLTgyYWYtNWEwMWZiMWI2NzYwIiwiaGFzaCI6IjdiMWMwNjNlODgzMDYzZjhjMzNmZmFhMjU2YWRlZDUwNmFmZDkwN2Y3NDQ2MTQzYjNkYTBmOTM4YTIxOTY3YTkiLCJmbiI6IkFsbWEiLCJsbiI6IkhheWVzIiwiZG9iIjoiMTk2Mi0wMS0wOCJ9" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzNjksInNhbHQiOiJEQTg3M0YwOUJCQzZCQzVFMEQ0QTdBMzc2MjZERkMwNSIsInRlc3RpZCI6IjlmYjYzNWE2LThhZTQtNDVjZS1iZTZkLTg5MjdmMjM1ZmIzNiIsImhhc2giOiJmNGYzMDU0NTMwODI1MjkxYjhmMDQ3MWZkZTRiY2EzNTljOGVjZDI0ZTBmNTkxNTA5NTQyY2ZmNGJhNDBkNmY1IiwiZm4iOiJFZGRpZSIsImxuIjoiQm91Y2hlciIsImRvYiI6IjE5NzktMDEtMjMifQ==" internal val raQrCode5 = - "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM4NDMsInNhbHQiOiJBNkVFRkZERDE2Qzk5RDFCODEyREE2NTc1NzgwMDM1QTQ0REI0OUM5NTBGODdCQkY0NDJBRkIwMDE5NkZCNDQ4IiwidGVzdElkIjoiNDhjOTc2ODgtY2U2ZC00MDFjLWEwMmMtMjU5MTE2YTRmODhmIiwiaGFzaCI6IjIyMTA4Y2FmNTQwNWM0MzI5Y2I3ZTEzNzg3MTMxMDJhMGNkNjY4OWM3YWFmMWJjOGViYzk3MDdiMjNjNTZhN2UifQ==" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzODIsInNhbHQiOiI2RUJCMUM4NTc0QUYxQzcwQkY2MTNGQjMzNDM3MkM3MiIsInRlc3RpZCI6Ijg2MzkzMTE1LWVkYjAtNGE3Zi1iZTg1LWEwYjViMjY5M2Q3MSIsImhhc2giOiIzMmQxYjk4MTRjNWU0ZjI3Mjc5NWU0NjNhMmViZjI5Y2Y1ZDZkYzdiZmRhODNhYzViZWY5Y2Q5M2E3YjMxMjYwIiwiZm4iOiJBZGVsYWlkZSIsImxuIjoiSHVpc21hbiIsImRvYiI6IjE5NTktMDgtMDIifQ==" internal val raQrCode6 = - "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM4NDMsInNhbHQiOiJBNkVFRkZERDE2Qzk5RDFCODEyREE2NTc1NzgwMDM1QTQ0REI0OUM5NTBGODdCQkY0NDJBRkIwMDE5NkZCNDQ4IiwidGVzdElkIjoiNDhjOTc2ODgtY2U2ZC00MDFjLWEwMmMtMjU5MTE2YTRmODhmIiwiaGFzaCI6IjIyMTA4Y2FmNTQwNWM0MzI5Y2I3ZTEzNzg3MTMxMDJhMGNkNjY4OWM3YWFmMWJjOGViYzk3MDdiMjNjNTZhN2UifQ==" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTgzOTcsInNhbHQiOiI3MDcyQzAyMTkyM0ZFOEMzMDVDNTAxQkU5MjQyMjNBQyIsInRlc3RpZCI6IjhmMzA0OTE4LWI2YmMtNDQyOS1iYzhlLWMzMzRkMjdhNTdiNCIsImhhc2giOiI2ZjVhMjJhYzY2ZTc1Y2JiYTE3MTBlN2IxZWMwZTllMDk4NjUyMjY0MWE3NTYyNGY0MGZhMDc4YTZkZjY0ZTVjIiwiZm4iOiJBbmRyZSIsImxuIjoiQmFyZ2VsbGluaSIsImRvYiI6IjE5OTItMTEtMDcifQ==" internal val raQrCode7 = - "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM5MDIsInNhbHQiOiJGRkVERDZCMEM5MzZGRDVBQTg1M0NBNjgzOTY1QzYzMDI1NDRBMTIyNTY5MDAyQjg5OEI1NkM5OTY1NjRENTNGIiwidGVzdElkIjoiODYyNDcwYzItMzk3MS00M2Y0LWFjY2UtMjIzMzRlMjZiZTg3IiwiaGFzaCI6Ijc1ZmU5M2MyOWI1ZDI4NTU3NmJjZmM5NTZmYWIxMzllNTgxMWNhZWY1MTNiN2Y4MzhmNjY3NWJhYTU4MGM5YWUiLCJmbiI6IkJyeWFuIiwibG4iOiJNYXJzaCIsImRvYiI6IjE5OTAtMDQtMjgifQ==" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MTg0MjMsInNhbHQiOiJEM0Y1MzcyODU2NkMxMDFENjE1MkVCQ0I0OEMxMkFCOCIsInRlc3RpZCI6Ijc5NWIxY2MwLWU2NjQtNGFmZi05NTk3LWU3MTk2ODE4ZGVmYiIsImhhc2giOiI3NmMxMjJiOTlmZWVmZmM5Mjc3MTE2YjUwZGIwZGM1NjI0ZjY5OWFiMzliMDAwOWMwYzg5YmRlMWNjZjM4YmQxIiwiZm4iOiJWaWN0b3JpYSIsImxuIjoiTWFubmVsbGkiLCJkb2IiOiIxOTc4LTA0LTIxIn0=" internal val raQrCode8 = - "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM5MDIsInNhbHQiOiJGRkVERDZCMEM5MzZGRDVBQTg1M0NBNjgzOTY1QzYzMDI1NDRBMTIyNTY5MDAyQjg5OEI1NkM5OTY1NjRENTNGIiwidGVzdElkIjoiODYyNDcwYzItMzk3MS00M2Y0LWFjY2UtMjIzMzRlMjZiZTg3IiwiaGFzaCI6Ijc1ZmU5M2MyOWI1ZDI4NTU3NmJjZmM5NTZmYWIxMzllNTgxMWNhZWY1MTNiN2Y4MzhmNjY3NWJhYTU4MGM5YWUiLCJmbiI6IkJyeWFuIiwibG4iOiJNYXJzaCIsImRvYiI6IjE5OTAtMDQtMjgifQ==" + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTk2MjEzMzEsInNhbHQiOiI5NERDMkUyRkNCNTE2NDFFQzA5RkZENjVEMkJEMDg4QiIsInRlc3RpZCI6ImMyMGI5OTgxLWZmNzAtNGRmOC1hYTAyLTUwOTdmMmJkN2YzYyIsImhhc2giOiI1MmVlZjA3YjQwYzU1ODM0OTg3MGQ3YTA2Yzc0OGIwOTAxNGMxMTBlYTkzYjZhNmRhNDhkOWI4NTU0NTU2MzY0In0=" // { // "fn":"", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensionsTest.kt index f1ea759196b4ee7f44df7006f8f1825cc45a730f..8b3f39956b7d425d8000bd5d5e36e03f43fe929e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensionsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensionsTest.kt @@ -1,9 +1,14 @@ package de.rki.coronawarnapp.coronatest.type.pcr +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.exception.http.BadRequestException import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.instanceOf import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant import org.junit.jupiter.api.Test import testhelpers.BaseTest +import java.net.SocketException class PCRCoronaTestExtensionsTest : BaseTest() { @@ -13,200 +18,30 @@ class PCRCoronaTestExtensionsTest : BaseTest() { test.toSubmissionState() shouldBe SubmissionStatePCR.NoTest } -// @Test -// fun removeTestFromDeviceSucceeds() = runBlockingTest { -// val submissionRepository = createInstance(scope = this) -// -// val initialPollingForTestResultTimeStampSlot = slot<Long>() -// val isTestResultAvailableNotificationSent = slot<Boolean>() -// every { -// tracingSettings.initialPollingForTestResultTimeStamp = capture(initialPollingForTestResultTimeStampSlot) -// } answers {} -// every { -// tracingSettings.isTestResultAvailableNotificationSent = capture(isTestResultAvailableNotificationSent) -// } answers {} -// -// every { submissionSettings.isAllowedToSubmitKeys = any() } just Runs -// every { submissionSettings.isSubmissionSuccessful = any() } just Runs -// -// submissionRepository.removeTestFromDevice() -// -// verify(exactly = 1) { -// testResultDataCollector.clear() -// registrationTokenPreference.update(any()) -// submissionSettings.devicePairingSuccessfulAt = null -// submissionSettings.initialTestResultReceivedAt = null -// submissionSettings.isAllowedToSubmitKeys = false -// submissionSettings.isSubmissionSuccessful = false -// } -// -// initialPollingForTestResultTimeStampSlot.captured shouldBe 0L -// isTestResultAvailableNotificationSent.captured shouldBe false -// } -// -// @Test -// fun registrationWithGUIDSucceeds() = runBlockingTest { -// coEvery { submissionService.asyncRegisterDeviceViaGUID(guid) } returns registrationData -// coEvery { analyticsKeySubmissionCollector.reportTestRegistered() } just Runs -// every { analyticsKeySubmissionCollector.reset() } just Runs -// -// val submissionRepository = createInstance(scope = this) -// -// submissionRepository.asyncRegisterDeviceViaGUID(guid) -// -// registrationTokenPreference.value shouldBe registrationToken -// submissionRepository.testResultReceivedDateFlow.first() shouldBe resultReceivedTimeStamp.toDate() -// -// verify(exactly = 1) { -// registrationTokenPreference.update(any()) -// submissionSettings.devicePairingSuccessfulAt = any() -// backgroundNoise.scheduleDummyPattern() -// } -// -// coVerify { testResultDataCollector.saveTestResultAnalyticsSettings(any()) } -// } -// -// @Test -// fun registrationWithTeleTANSucceeds() = runBlockingTest { -// coEvery { submissionService.asyncRegisterDeviceViaTAN(tan) } returns registrationData -// coEvery { analyticsKeySubmissionCollector.reportTestRegistered() } just Runs -// every { analyticsKeySubmissionCollector.reportRegisteredWithTeleTAN() } just Runs -// every { analyticsKeySubmissionCollector.reset() } just Runs -// -// val submissionRepository = createInstance(scope = this) -// -// submissionRepository.asyncRegisterDeviceViaTAN(tan) -// -// registrationTokenPreference.value shouldBe registrationToken -// submissionRepository.testResultReceivedDateFlow.first() shouldBe resultReceivedTimeStamp.toDate() -// -// verify(exactly = 1) { -// registrationTokenPreference.update(any()) -// submissionSettings.devicePairingSuccessfulAt = any() -// backgroundNoise.scheduleDummyPattern() -// } -// -// coVerify(exactly = 0) { -// testResultDataCollector.saveTestResultAnalyticsSettings(any()) -// } -// } -// -// @Test -// fun `reset clears tek history and settings`() = runBlockingTest { -// val instance = createInstance(this) -// instance.reset() -// -// instance.deviceUIStateFlow.first() shouldBe NetworkRequestWrapper.RequestIdle -// -// coVerifyOrder { -// tekHistoryStorage.clear() -// submissionSettings.clear() -// } -// } -// -// @Test -// fun `ui state is SUBMITTED_FINAL when submission was done`() = runBlockingTest { -// every { submissionSettings.isSubmissionSuccessful } returns true -// -// val submissionRepository = createInstance(scope = this) -// -// submissionRepository.refreshTest() -// submissionRepository.deviceUIStateFlow.first() shouldBe -// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL) -// } -// -// @Test -// fun `ui state is UNPAIRED when no token is present`() = runBlockingTest { -// every { submissionSettings.isSubmissionSuccessful } returns false -// every { submissionSettings.registrationToken } returns mockFlowPreference(null) -// -// val submissionRepository = createInstance(scope = this) -// -// submissionRepository.refreshTest() -// submissionRepository.deviceUIStateFlow.first() shouldBe -// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED) -// } -// -// @Test -// fun `ui state is PAIRED_POSITIVE when allowed to submit`() = runBlockingTest { -// every { submissionSettings.isSubmissionSuccessful } returns false -// every { submissionSettings.registrationToken } returns mockFlowPreference("token") -// coEvery { submissionSettings.isAllowedToSubmitKeys } returns true -// -// val submissionRepository = createInstance(scope = this) -// -// submissionRepository.refreshTest() -// submissionRepository.deviceUIStateFlow.first() shouldBe -// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE) -// } -// -// @Test -// fun `refresh when state is PAIRED_NO_RESULT`() = runBlockingTest { -// every { submissionSettings.isSubmissionSuccessful } returns false -// every { submissionSettings.registrationToken } returns mockFlowPreference("token") -// coEvery { submissionSettings.isAllowedToSubmitKeys } returns false -// coEvery { submissionService.asyncRequestTestResult(any()) } returns CoronaTestResult.PCR_OR_RAT_PENDING -// -// val submissionRepository = createInstance(scope = this) -// -// submissionRepository.refreshTest() -// submissionRepository.deviceUIStateFlow.first() shouldBe -// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT) -// -// coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } -// } -// -// @Test -// fun `refresh when state is UNPAIRED`() = runBlockingTest { -// every { submissionSettings.isSubmissionSuccessful } returns false -// every { submissionSettings.registrationToken } returns mockFlowPreference(null) -// coEvery { submissionSettings.isAllowedToSubmitKeys } returns false -// coEvery { submissionService.asyncRequestTestResult(any()) } returns CoronaTestResult.PCR_OR_RAT_PENDING -// -// val submissionRepository = createInstance(scope = this) -// -// submissionRepository.refreshTest() -// submissionRepository.deviceUIStateFlow.first() shouldBe -// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED) -// -// every { submissionSettings.registrationToken } returns mockFlowPreference("token") -// -// submissionRepository.refreshTest() -// -// coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } -// } -// -// @Test -// fun `no refresh when state is SUBMITTED_FINAL`() = runBlockingTest { -// every { submissionSettings.isSubmissionSuccessful } returns true -// -// val submissionRepository = createInstance(scope = this) -// -// submissionRepository.refreshTest() -// -// submissionRepository.deviceUIStateFlow.first() shouldBe -// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL) -// -// submissionRepository.refreshTest() -// -// coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } -// } -// @Test -// fun `updateTestResult updates test result donor data`() = runBlockingTest { -// val submissionRepository = createInstance(scope = this) -// submissionRepository.updateTestResult(CoronaTestResult.PCR_NEGATIVE) -// -// verify { testResultDataCollector.updatePendingTestResultReceivedTime(any()) } -// } + // EXPOSUREAPP-6784 / https://github.com/corona-warn-app/cwa-app-android/issues/2953 + @Test + fun `non http 400 errors do not affect result state`() = runBlockingTest { + val test = PCRCoronaTest( + identifier = "identifier", + registeredAt = Instant.ofEpochMilli(123), + registrationToken = "regtoken", + testResult = CoronaTestResult.PCR_POSITIVE, + lastUpdatedAt = Instant.EPOCH, + lastError = SocketException("Connection reset") + ) + test.toSubmissionState() shouldBe instanceOf(SubmissionStatePCR.TestResultReady::class) + } -// @Test -// fun `doDeviceRegistration calls TestResultDataCollector`() { -// val viewModel = createViewModel() -// val mockResult = mockk<QRScanResult>().apply { -// every { guid } returns "guid" -// } -// -// coEvery { submissionRepository.asyncRegisterDeviceViaGUID(any()) } returns CoronaTestResult.PCR_POSITIVE -// viewModel.doDeviceRegistration(mockResult) -// } + @Test + fun `client HTTP400 errors result in invalid test state`() = runBlockingTest { + val test = PCRCoronaTest( + identifier = "identifier", + registeredAt = Instant.ofEpochMilli(123), + registrationToken = "regtoken", + testResult = CoronaTestResult.PCR_POSITIVE, + lastUpdatedAt = Instant.EPOCH, + lastError = BadRequestException("") + ) + test.toSubmissionState() shouldBe instanceOf(SubmissionStatePCR.TestInvalid::class) + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt index 4692cc48ae93f8f0c5ee647c20eb905ded541760..7a2c51d7dc38bb10a0b02aa99f2ee7b29e9a0060 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt @@ -1,10 +1,22 @@ package de.rki.coronawarnapp.coronatest.type.pcr +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_INVALID +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_NEGATIVE +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_OR_RAT_PENDING +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_POSITIVE +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_REDEEMED +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_INVALID +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_NEGATIVE +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_PENDING +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_POSITIVE +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_REDEEMED +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.values +import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN import de.rki.coronawarnapp.coronatest.type.CoronaTestService import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest.TestResultDataCollector -import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -19,13 +31,13 @@ import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest +import timber.log.Timber class PCRProcessorTest : BaseTest() { @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var submissionService: CoronaTestService @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector @MockK lateinit var testResultDataCollector: TestResultDataCollector - @MockK lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler private val nowUTC = Instant.parse("2021-03-15T05:45:00.000Z") @@ -36,11 +48,26 @@ class PCRProcessorTest : BaseTest() { every { timeStamper.nowUTC } returns nowUTC submissionService.apply { - coEvery { asyncRequestTestResult(any()) } answers { CoronaTestResult.PCR_OR_RAT_PENDING } + coEvery { asyncRequestTestResult(any()) } returns PCR_OR_RAT_PENDING + coEvery { asyncRegisterDeviceViaGUID(any()) } returns CoronaTestService.RegistrationData( + registrationToken = "regtoken-qr", + testResult = PCR_OR_RAT_PENDING, + ) + coEvery { asyncRegisterDeviceViaTAN(any()) } returns CoronaTestService.RegistrationData( + registrationToken = "regtoken-tan", + testResult = PCR_OR_RAT_PENDING, + ) } + analyticsKeySubmissionCollector.apply { + coEvery { reportRegisteredWithTeleTAN() } just Runs + coEvery { reset() } just Runs + coEvery { reportPositiveTestResultReceived() } just Runs + coEvery { reportTestRegistered() } just Runs + } testResultDataCollector.apply { coEvery { updatePendingTestResultReceivedTime(any()) } just Runs + coEvery { saveTestResultAnalyticsSettings(any()) } just Runs } } @@ -48,8 +75,7 @@ class PCRProcessorTest : BaseTest() { timeStamper = timeStamper, submissionService = submissionService, analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, - testResultDataCollector = testResultDataCollector, - deadmanNotificationScheduler = deadmanNotificationScheduler, + testResultDataCollector = testResultDataCollector ) @Test @@ -61,15 +87,98 @@ class PCRProcessorTest : BaseTest() { lastUpdatedAt = Instant.EPOCH, registeredAt = nowUTC, registrationToken = "regtoken", - testResult = CoronaTestResult.PCR_POSITIVE + testResult = PCR_POSITIVE ) - instance.pollServer(pcrTest).testResult shouldBe CoronaTestResult.PCR_OR_RAT_PENDING + instance.pollServer(pcrTest).testResult shouldBe PCR_OR_RAT_PENDING val past60DaysTest = pcrTest.copy( registeredAt = nowUTC.minus(Duration.standardDays(21)) ) - instance.pollServer(past60DaysTest).testResult shouldBe CoronaTestResult.PCR_REDEEMED + instance.pollServer(past60DaysTest).testResult shouldBe PCR_REDEEMED + } + + @Test + fun `registering a new test maps invalid results to INVALID state`() = runBlockingTest { + var registrationData = CoronaTestService.RegistrationData( + registrationToken = "regtoken", + testResult = PCR_OR_RAT_PENDING, + ) + coEvery { submissionService.asyncRegisterDeviceViaGUID(any()) } answers { registrationData } + + val instance = createInstance() + + val request = CoronaTestQRCode.PCR(qrCodeGUID = "guid") + + values().forEach { + registrationData = registrationData.copy(testResult = it) + when (it) { + PCR_OR_RAT_PENDING, + PCR_NEGATIVE, + PCR_POSITIVE, + PCR_INVALID, + PCR_REDEEMED -> instance.create(request).testResult shouldBe it + + RAT_PENDING, + RAT_NEGATIVE, + RAT_POSITIVE, + RAT_INVALID, + RAT_REDEEMED -> + instance.create(request).testResult shouldBe PCR_INVALID + } + } + } + + @Test + fun `polling maps invalid results to INVALID state`() = runBlockingTest { + var pollResult: CoronaTestResult = PCR_OR_RAT_PENDING + coEvery { submissionService.asyncRequestTestResult(any()) } answers { pollResult } + + val instance = createInstance() + + val pcrTest = PCRCoronaTest( + identifier = "identifier", + lastUpdatedAt = Instant.EPOCH, + registeredAt = nowUTC, + registrationToken = "regtoken", + testResult = PCR_POSITIVE + ) + + values().forEach { + pollResult = it + when (it) { + PCR_OR_RAT_PENDING, + PCR_NEGATIVE, + PCR_POSITIVE, + PCR_INVALID, + PCR_REDEEMED -> { + Timber.v("Should NOT throw for $it") + instance.pollServer(pcrTest).testResult shouldBe it + } + RAT_PENDING, + RAT_NEGATIVE, + RAT_POSITIVE, + RAT_INVALID, + RAT_REDEEMED -> { + Timber.v("Should throw for $it") + instance.pollServer(pcrTest).testResult shouldBe PCR_INVALID + } + } + } + } + + // TANs are automatically positive, there is no test result available screen that should be reached + @Test + fun `registering a TAN test automatically consumes the notification flag`() = runBlockingTest { + val instance = createInstance() + + instance.create(CoronaTestTAN.PCR(tan = "thisIsATan")).apply { + isResultAvailableNotificationSent shouldBe true + } + + instance.create(CoronaTestQRCode.PCR(qrCodeGUID = "thisIsAQRCodeGUID")).apply { + isResultAvailableNotificationSent shouldBe false + } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensionsTest.kt index 3998d73348ed29e81a1ed8c900b39d10529c7ea2..e4ce30b845de6c8a467b5dd7675a60a99175b716 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensionsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensionsTest.kt @@ -2,16 +2,20 @@ package de.rki.coronawarnapp.coronatest.type.rapidantigen import de.rki.coronawarnapp.appconfig.CoronaTestConfig import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.exception.http.BadRequestException import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.instanceOf import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Duration import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest +import java.net.SocketException class RapidAntigenCoronaTestExtensionsTest : BaseTest() { @MockK lateinit var coronaTestConfig: CoronaTestConfig @@ -22,7 +26,8 @@ class RapidAntigenCoronaTestExtensionsTest : BaseTest() { MockKAnnotations.init(this) every { timeStamper.nowUTC } returns Instant.ofEpochMilli(1010010101) - every { coronaTestConfig.coronaRapidAntigenTestParameters.hoursToDeemTestOutdated } returns 48 + every { coronaTestConfig.coronaRapidAntigenTestParameters.hoursToDeemTestOutdated } returns + Duration.standardHours(48) } @Test @@ -49,4 +54,45 @@ class RapidAntigenCoronaTestExtensionsTest : BaseTest() { testRegisteredAt = Instant.ofEpochMilli(123) ) } + + // EXPOSUREAPP-6784 / https://github.com/corona-warn-app/cwa-app-android/issues/2953 + @Test + fun `errors that are not http 400 do not affect result state`() = runBlockingTest { + val test = RACoronaTest( + identifier = "identifier", + registeredAt = Instant.ofEpochMilli(123), + registrationToken = "regtoken", + testResult = CoronaTestResult.RAT_POSITIVE, + testedAt = Instant.EPOCH, + dateOfBirth = null, + firstName = null, + lastName = null, + lastUpdatedAt = Instant.EPOCH, + lastError = SocketException("Connection reset") + ) + test.toSubmissionState( + timeStamper.nowUTC, + coronaTestConfig + ) shouldBe instanceOf(SubmissionStateRAT.TestResultReady::class) + } + + @Test + fun `client http 400 errors result in invalid test state`() = runBlockingTest { + val test = RACoronaTest( + identifier = "identifier", + registeredAt = Instant.ofEpochMilli(123), + registrationToken = "regtoken", + testResult = CoronaTestResult.RAT_POSITIVE, + testedAt = Instant.EPOCH, + dateOfBirth = null, + firstName = null, + lastName = null, + lastUpdatedAt = Instant.EPOCH, + lastError = BadRequestException("") + ) + test.toSubmissionState( + timeStamper.nowUTC, + coronaTestConfig + ) shouldBe instanceOf(SubmissionStateRAT.TestInvalid::class) + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt index b2b3693be4be5f6d4bca10a5c0ed201df0116d87..76573d88bc5c0dbfe14a727593fa4b37f1e2e55a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt @@ -1,6 +1,18 @@ package de.rki.coronawarnapp.coronatest.type.rapidantigen +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_INVALID +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_NEGATIVE +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_OR_RAT_PENDING +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_POSITIVE +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_REDEEMED +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_INVALID +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_NEGATIVE +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_PENDING +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_POSITIVE +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_REDEEMED +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.values import de.rki.coronawarnapp.coronatest.type.CoronaTestService import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe @@ -29,7 +41,15 @@ class RapidAntigenProcessorTest : BaseTest() { every { timeStamper.nowUTC } returns nowUTC submissionService.apply { - coEvery { asyncRequestTestResult(any()) } answers { CoronaTestResult.PCR_OR_RAT_PENDING } + coEvery { asyncRequestTestResult(any()) } returns PCR_OR_RAT_PENDING + coEvery { asyncRegisterDeviceViaGUID(any()) } returns CoronaTestService.RegistrationData( + registrationToken = "regtoken-qr", + testResult = PCR_OR_RAT_PENDING, + ) + coEvery { asyncRegisterDeviceViaTAN(any()) } returns CoronaTestService.RegistrationData( + registrationToken = "regtoken-tan", + testResult = PCR_OR_RAT_PENDING, + ) } } @@ -42,21 +62,88 @@ class RapidAntigenProcessorTest : BaseTest() { fun `if we receive a pending result 60 days after registration, we map to REDEEMED`() = runBlockingTest { val instance = createInstance() - val pcrTest = RACoronaTest( + val raTest = RACoronaTest( identifier = "identifier", lastUpdatedAt = Instant.EPOCH, registeredAt = nowUTC, registrationToken = "regtoken", - testResult = CoronaTestResult.RAT_POSITIVE, + testResult = RAT_POSITIVE, testedAt = Instant.EPOCH, ) - instance.pollServer(pcrTest).testResult shouldBe CoronaTestResult.PCR_OR_RAT_PENDING + instance.pollServer(raTest).testResult shouldBe PCR_OR_RAT_PENDING - val past60DaysTest = pcrTest.copy( + val past60DaysTest = raTest.copy( registeredAt = nowUTC.minus(Duration.standardDays(21)) ) - instance.pollServer(past60DaysTest).testResult shouldBe CoronaTestResult.RAT_REDEEMED + instance.pollServer(past60DaysTest).testResult shouldBe RAT_REDEEMED + } + + @Test + fun `registering a new test maps invalid results to INVALID state`() = runBlockingTest { + var registrationData = CoronaTestService.RegistrationData( + registrationToken = "regtoken", + testResult = PCR_OR_RAT_PENDING, + ) + coEvery { submissionService.asyncRegisterDeviceViaGUID(any()) } answers { registrationData } + + val instance = createInstance() + + val request = CoronaTestQRCode.RapidAntigen( + hash = "hash", + createdAt = Instant.EPOCH, + ) + + values().forEach { + registrationData = registrationData.copy(testResult = it) + when (it) { + PCR_NEGATIVE, + PCR_POSITIVE, + PCR_INVALID, + PCR_REDEEMED -> instance.create(request).testResult shouldBe RAT_INVALID + + PCR_OR_RAT_PENDING, + RAT_PENDING, + RAT_NEGATIVE, + RAT_POSITIVE, + RAT_INVALID, + RAT_REDEEMED -> instance.create(request).testResult shouldBe it + } + } + } + + @Test + fun `polling filters out invalid test result values`() = runBlockingTest { + var pollResult: CoronaTestResult = PCR_OR_RAT_PENDING + coEvery { submissionService.asyncRequestTestResult(any()) } answers { pollResult } + + val instance = createInstance() + + val raTest = RACoronaTest( + identifier = "identifier", + lastUpdatedAt = Instant.EPOCH, + registeredAt = nowUTC, + registrationToken = "regtoken", + testResult = RAT_POSITIVE, + testedAt = Instant.EPOCH, + ) + + values().forEach { + pollResult = it + when (it) { + PCR_NEGATIVE, + PCR_POSITIVE, + PCR_INVALID, + PCR_REDEEMED -> instance.pollServer(raTest).testResult shouldBe RAT_INVALID + + PCR_OR_RAT_PENDING, + RAT_PENDING, + RAT_NEGATIVE, + RAT_POSITIVE, + RAT_INVALID, + RAT_REDEEMED -> instance.pollServer(raTest).testResult shouldBe it + } + } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/deadman/DeadmanNotificationSchedulerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/deadman/DeadmanNotificationSchedulerTest.kt index 27cb96a8fa562b02fb40c6561e53bdfe3397c7bf..e207eee48763c3edfaf20a85d1db815b9be04914 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/deadman/DeadmanNotificationSchedulerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/deadman/DeadmanNotificationSchedulerTest.kt @@ -6,12 +6,19 @@ import androidx.work.OneTimeWorkRequest import androidx.work.Operation import androidx.work.PeriodicWorkRequest import androidx.work.WorkManager +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.storage.OnboardingSettings import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import io.mockk.verify import io.mockk.verifySequence +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -25,12 +32,17 @@ class DeadmanNotificationSchedulerTest : BaseTest() { @MockK lateinit var workBuilder: DeadmanNotificationWorkBuilder @MockK lateinit var periodicWorkRequest: PeriodicWorkRequest @MockK lateinit var oneTimeWorkRequest: OneTimeWorkRequest + @MockK lateinit var onboardingSettings: OnboardingSettings + @MockK lateinit var enfClient: ENFClient + @MockK lateinit var coronaTestRepository: CoronaTestRepository @BeforeEach fun setup() { MockKAnnotations.init(this) every { workBuilder.buildPeriodicWork() } returns periodicWorkRequest every { workBuilder.buildOneTimeWork(any()) } returns oneTimeWorkRequest + every { workManager.cancelUniqueWork(DeadmanNotificationScheduler.PERIODIC_WORK_NAME) } returns operation + every { workManager.cancelUniqueWork(DeadmanNotificationScheduler.ONE_TIME_WORK_NAME) } returns operation every { workManager.enqueueUniquePeriodicWork( DeadmanNotificationScheduler.PERIODIC_WORK_NAME, @@ -46,19 +58,27 @@ class DeadmanNotificationSchedulerTest : BaseTest() { oneTimeWorkRequest ) } returns operation + + every { onboardingSettings.isOnboardedFlow } returns flowOf(true) + every { coronaTestRepository.coronaTests } returns flowOf(emptySet()) + every { enfClient.isTracingEnabled } returns flowOf(true) } - private fun createScheduler() = DeadmanNotificationScheduler( + private fun createScheduler(scope: CoroutineScope) = DeadmanNotificationScheduler( + appScope = scope, timeCalculation = timeCalculation, workManager = workManager, - workBuilder = workBuilder + workBuilder = workBuilder, + onboardingSettings = onboardingSettings, + enfClient = enfClient, + coronaTestRepository = coronaTestRepository ) @Test fun `one time work was scheduled`() = runBlockingTest { coEvery { timeCalculation.getDelay() } returns 10L - createScheduler().scheduleOneTime() + createScheduler(this).scheduleOneTime() verifySequence { workManager.enqueueUniqueWork( @@ -73,7 +93,7 @@ class DeadmanNotificationSchedulerTest : BaseTest() { fun `one time work was not scheduled`() = runBlockingTest { coEvery { timeCalculation.getDelay() } returns -10L - createScheduler().scheduleOneTime() + createScheduler(this).scheduleOneTime() verify(exactly = 0) { workManager.enqueueUniqueWork( @@ -93,10 +113,76 @@ class DeadmanNotificationSchedulerTest : BaseTest() { } @Test - fun `test periodic work was scheduled`() { - createScheduler().schedulePeriodic() + fun `test periodic work was scheduled`() = runBlockingTest { + createScheduler(this).schedulePeriodic() - verifySequence { + verifyPeriodicWorkScheduled() + } + + @Test + fun `scheduled work should be cancelled if onboarding wasn't yet done `() = runBlockingTest { + every { onboardingSettings.isOnboardedFlow } returns flowOf(false) + + createScheduler(this).apply { setup() } + + verifyCancelScheduledWork(exactly = 1) + verifyPeriodicWorkScheduled(exactly = 0) + } + + @Test + fun `work should be scheduled if no test is registered`() = runBlockingTest { + every { coronaTestRepository.coronaTests } returns flowOf(emptySet()) + + createScheduler(this).apply { setup() } + + verifyCancelScheduledWork(exactly = 0) + verifyPeriodicWorkScheduled(exactly = 1) + } + + @Test + fun `work should be scheduled if only negative test is registered`() = runBlockingTest { + every { coronaTestRepository.coronaTests } returns flowOf( + setOf( + mockk<CoronaTest>().apply { + every { isPositive } returns false + } + ) + ) + + createScheduler(this).apply { setup() } + + verifyCancelScheduledWork(exactly = 0) + verifyPeriodicWorkScheduled(exactly = 1) + } + + @Test + fun `scheduled work should be cancelled if positive test is registered`() = runBlockingTest { + every { coronaTestRepository.coronaTests } returns flowOf( + setOf( + mockk<CoronaTest>().apply { + every { isPositive } returns true + } + ) + ) + + createScheduler(this).apply { setup() } + + verifyCancelScheduledWork(exactly = 1) + verifyPeriodicWorkScheduled(exactly = 0) + } + + @Test + fun `scheduled work should be cancelled if tracing is disabled`() = runBlockingTest { + every { enfClient.isTracingEnabled } returns flowOf(false) + + createScheduler(this).apply { setup() } + + verifyCancelScheduledWork(exactly = 1) + verifyPeriodicWorkScheduled(exactly = 0) + } + + private fun verifyPeriodicWorkScheduled(exactly: Int = 1) { + verify(exactly = exactly) { workManager.enqueueUniquePeriodicWork( DeadmanNotificationScheduler.PERIODIC_WORK_NAME, ExistingPeriodicWorkPolicy.KEEP, @@ -104,4 +190,13 @@ class DeadmanNotificationSchedulerTest : BaseTest() { ) } } + + private fun verifyCancelScheduledWork(exactly: Int = 1) { + verify(exactly = exactly) { + workManager.cancelUniqueWork(DeadmanNotificationScheduler.PERIODIC_WORK_NAME) + } + verify(exactly = exactly) { + workManager.cancelUniqueWork(DeadmanNotificationScheduler.ONE_TIME_WORK_NAME) + } + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt index 9171db1449f3deeca8589cd8da53e28b0381e8a1..3e45f0f9e0e9dd99e70cab18024572854c0acc9a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt @@ -3,7 +3,6 @@ package de.rki.coronawarnapp.main.home import android.content.Context import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.coronatest.CoronaTestRepository -import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.statistics.source.StatisticsProvider @@ -58,7 +57,6 @@ class HomeFragmentViewModelTest : BaseTest() { @MockK lateinit var cwaSettings: CWASettings @MockK lateinit var appConfigProvider: AppConfigProvider @MockK lateinit var statisticsProvider: StatisticsProvider - @MockK lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler @MockK lateinit var appShortcutsHelper: AppShortcutsHelper @MockK lateinit var tracingSettings: TracingSettings @MockK lateinit var traceLocationOrganizerSettings: TraceLocationOrganizerSettings @@ -98,7 +96,6 @@ class HomeFragmentViewModelTest : BaseTest() { cwaSettings = cwaSettings, appConfigProvider = appConfigProvider, statisticsProvider = statisticsProvider, - deadmanNotificationScheduler = deadmanNotificationScheduler, appShortcutsHelper = appShortcutsHelper, tracingSettings = tracingSettings, traceLocationOrganizerSettings = traceLocationOrganizerSettings, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt index a9a402b39d5b9c41d76cc54c30b46daea87d7e18..b8d6def71692b477eaceed3955e68d459d4a02e1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt @@ -1,5 +1,7 @@ package de.rki.coronawarnapp.nearby.modules.tracing +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.Status import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient import de.rki.coronawarnapp.storage.TracingSettings import io.kotest.matchers.shouldBe @@ -100,4 +102,31 @@ class DefaultTracingStatusTest : BaseTest() { every { client.isEnabled } answers { MockGMSTask.forValue(false) } instance.isTracingEnabled.first() shouldBe false } + + @Test + fun `api errors during state polling are mapped to false`() = runBlockingTest2(ignoreActive = true) { + every { client.isEnabled } answers { MockGMSTask.forError(ApiException(Status.RESULT_INTERNAL_ERROR)) } + + val instance = createInstance(scope = this) + + instance.isTracingEnabled.first() shouldBe false + } + + @Test + fun `api errors during state setting are rethrown`() = runBlockingTest2(ignoreActive = true) { + val ourError = ApiException(Status.RESULT_INTERNAL_ERROR) + every { client.isEnabled } answers { MockGMSTask.forError(ourError) } + + val instance = createInstance(scope = this) + + var thrownError: Throwable? = null + instance.setTracing( + enable = true, + onSuccess = {}, + onError = { thrownError = it }, + onPermissionRequired = {} + ) + + thrownError shouldBe ourError + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/CombinedEwPtRiskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/CombinedEwPtRiskTest.kt index 18ec9e4c29a3c2051546b6fd2c4b2b7118660159..e53e0c16f2affbcb8817c6805e2c728ba25a07f6 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/CombinedEwPtRiskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/CombinedEwPtRiskTest.kt @@ -90,8 +90,6 @@ class CombinedEwPtRiskTest : BaseTest() { minimumDistinctEncountersWithHighRisk = 0 ) - every { ewAggregatedRiskResult.exposureWindowDayRisks } returns listOf(ewDayRisk, ewDayRisk2) - val ptDayRisk = PresenceTracingDayRisk( riskState = RiskState.LOW_RISK, localDateUtc = Instant.ofEpochMilli(1000).toLocalDateUtc() @@ -114,10 +112,49 @@ class CombinedEwPtRiskTest : BaseTest() { ewRiskLevelResult = createEwRiskLevel( calculatedAt = Instant.ofEpochMilli(1000 + 2 * MILLIS_DAY), ewAggregatedRiskResult - ) + ), + exposureWindowDayRisks = listOf(ewDayRisk, ewDayRisk2) ).daysWithEncounters shouldBe 3 } + @Test + fun `counts days correctly`() { + val dayRisk = ExposureWindowDayRisk( + dateMillisSinceEpoch = 1000, + riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + minimumDistinctEncountersWithLowRisk = 0, + minimumDistinctEncountersWithHighRisk = 1 + ) + val dayRisk2 = ExposureWindowDayRisk( + dateMillisSinceEpoch = 1000 + MILLIS_DAY, + riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + minimumDistinctEncountersWithLowRisk = 1, + minimumDistinctEncountersWithHighRisk = 0 + ) + val dayRisk3 = ExposureWindowDayRisk( + dateMillisSinceEpoch = 1000 + 2 * MILLIS_DAY, + riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + minimumDistinctEncountersWithLowRisk = 1, + minimumDistinctEncountersWithHighRisk = 2 + ) + + val result = CombinedEwPtRiskLevelResult( + ptRiskLevelResult = createPtRiskLevelResult( + calculatedAt = Instant.ofEpochMilli(1000 + 2 * MILLIS_DAY), + riskState = RiskState.LOW_RISK, + presenceTracingDayRisk = listOf() + ), + ewRiskLevelResult = createEwRiskLevel( + calculatedAt = Instant.ofEpochMilli(1000 + 2 * MILLIS_DAY), + ewAggregatedRiskResult + ), + exposureWindowDayRisks = listOf(dayRisk, dayRisk2, dayRisk3) + ) + + result.ewDaysWithHighRisk.size shouldBe 2 + result.ewDaysWithLowRisk.size shouldBe 1 + } + private fun createPtRiskLevelResult( calculatedAt: Instant, riskState: RiskState, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultTest.kt index c2bb7d54632201dc77070fac12757dd674cdf4c5..0a6a80870edd786fa6c38c828921d8edc5aa916c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultTest.kt @@ -2,12 +2,8 @@ package de.rki.coronawarnapp.risk import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult -import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk -import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK import io.mockk.mockk import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach @@ -16,8 +12,6 @@ import testhelpers.BaseTest class EwRiskLevelResultTest : BaseTest() { - @MockK lateinit var ewAggregatedRiskResult1: EwAggregatedRiskResult - @BeforeEach fun setup() { MockKAnnotations.init(this) @@ -36,35 +30,6 @@ class EwRiskLevelResultTest : BaseTest() { ).wasSuccessfullyCalculated shouldBe true } - @Test - fun `counts days correctly`() { - val dayRisk = ExposureWindowDayRisk( - dateMillisSinceEpoch = 1000, - riskLevel = RiskLevel.HIGH, - minimumDistinctEncountersWithLowRisk = 0, - minimumDistinctEncountersWithHighRisk = 1 - ) - val dayRisk2 = ExposureWindowDayRisk( - dateMillisSinceEpoch = 1000 + MILLIS_DAY, - riskLevel = RiskLevel.LOW, - minimumDistinctEncountersWithLowRisk = 1, - minimumDistinctEncountersWithHighRisk = 0 - ) - val dayRisk3 = ExposureWindowDayRisk( - dateMillisSinceEpoch = 1000 + 2 * MILLIS_DAY, - riskLevel = RiskLevel.HIGH, - minimumDistinctEncountersWithLowRisk = 1, - minimumDistinctEncountersWithHighRisk = 2 - ) - every { ewAggregatedRiskResult1.exposureWindowDayRisks } returns listOf(dayRisk, dayRisk2, dayRisk3) - val riskLevel = createRiskLevel( - ewAggregatedRiskResult = ewAggregatedRiskResult1, - failureReason = null - ) - riskLevel.daysWithHighRisk.size shouldBe 2 - riskLevel.daysWithLowRisk.size shouldBe 1 - } - private fun createRiskLevel( ewAggregatedRiskResult: EwAggregatedRiskResult?, failureReason: EwRiskLevelResult.FailureReason? @@ -76,5 +41,3 @@ class EwRiskLevelResultTest : BaseTest() { override val matchedKeyCount: Int = 0 } } - -private const val MILLIS_DAY = (1000 * 60 * 60 * 24).toLong() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt index 4ad2bbd28da2b03920fd7d3a785ae6229519c7fd..ead1de9a95156d5baee367764ebaea9d805ae225 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt @@ -103,6 +103,7 @@ class BaseRiskLevelStorageTest : BaseTest() { riskResultDatabaseFactory = databaseFactory, presenceTracingRiskRepository = presenceTracingRiskRepository, riskCombinator = riskCombinator, + timeStamper = timeStamper, ) { override val storedResultLimit: Int = storedResultLimit @@ -124,7 +125,7 @@ class BaseRiskLevelStorageTest : BaseTest() { val instance = createInstance() val allEntries = instance.aggregatedRiskPerDateResultTables.allEntries() allEntries shouldBe testPersistedAggregatedRiskPerDateResultFlow - allEntries.first().map { it.toAggregatedRiskPerDateResult() } shouldBe listOf( + allEntries.first().map { it.toExposureWindowDayRisk() } shouldBe listOf( testAggregatedRiskPerDateResult ) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt index de0bc5cb26df3bdcdd47b355d3c875cdc2386c93..0e899746ec7e05152f44c1161fd6aaeeaa39d83c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt @@ -4,7 +4,6 @@ import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.coronatest.CoronaTestRepository -import de.rki.coronawarnapp.coronatest.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.RAPID_ANTIGEN @@ -54,7 +53,6 @@ class SubmissionTaskTest : BaseTest() { @MockK lateinit var tekHistoryCalculations: ExposureKeyHistoryCalculations @MockK lateinit var tekHistoryStorage: TEKHistoryStorage @MockK lateinit var submissionSettings: SubmissionSettings - @MockK lateinit var shareTestResultNotificationService: ShareTestResultNotificationService @MockK lateinit var testResultAvailableNotificationService: PCRTestResultAvailableNotificationService @MockK lateinit var autoSubmission: AutoSubmission @@ -84,7 +82,7 @@ class SubmissionTaskTest : BaseTest() { every { isSubmitted } returns false every { registrationToken } returns "regtoken" every { identifier } returns "coronatest-identifier" - every { type } returns CoronaTest.Type.PCR + every { type } returns PCR } ) ) @@ -171,7 +169,6 @@ class SubmissionTaskTest : BaseTest() { tekHistoryCalculations = tekHistoryCalculations, tekHistoryStorage = tekHistoryStorage, submissionSettings = submissionSettings, - shareTestResultNotificationService = shareTestResultNotificationService, timeStamper = timeStamper, autoSubmission = autoSubmission, testResultAvailableNotificationService = testResultAvailableNotificationService, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt index f10be3cd849ecac441f0fedf6bb05f5b9f70e353..5ecc2cc19dc5952a3e05fc2298e2c17a55842e55 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt @@ -106,7 +106,7 @@ class EncryptedPreferencesMigrationTest : BaseIOTest() { every { cwaSettings.isNotificationsRiskEnabled } returns mockRiskPreference val mockTestPreference = mockFlowPreference(true) every { cwaSettings.isNotificationsTestEnabled } returns mockTestPreference - every { cwaSettings.numberOfRemainingSharePositiveTestResultReminders = Int.MAX_VALUE } just Runs + every { cwaSettings.numberOfRemainingSharePositiveTestResultRemindersPcr = Int.MAX_VALUE } just Runs // OnboardingLocalData val mockOnboardingCompletedTimestamp = mockFlowPreference(Instant.ofEpochMilli(10101010L)) diff --git a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 673ac7b96c843f1c2ed9909a24b0a68e7d383ff9..da4cf340d0a4052cba4455c09485cc118b733c9c 100644 --- a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -35,6 +35,7 @@ class DefaultRiskLevelStorageTest : BaseTest() { @MockK lateinit var riskResultTables: RiskResultDatabase.RiskResultsDao @MockK lateinit var exposureWindowTables: RiskResultDatabase.ExposureWindowsDao @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository + @MockK lateinit var timeStamper: TimeStamper private val testRiskLevelResultDao = PersistedRiskLevelResultDao( id = "riskresult-id", @@ -101,6 +102,7 @@ class DefaultRiskLevelStorageTest : BaseTest() { riskResultDatabaseFactory = databaseFactory, presenceTracingRiskRepository = presenceTracingRiskRepository, riskCombinator = RiskCombinator(TimeStamper()), + timeStamper = timeStamper, ) @Test diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 5502ead593df03320642893e077e6a6201216d88..75420cce334e89720646c7eca7a028ab6d71d3a8 100644 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -97,7 +97,8 @@ class DefaultRiskLevelStorageTest : BaseTestInstrumentation() { scope = TestCoroutineScope(), riskResultDatabaseFactory = databaseFactory, presenceTracingRiskRepository = presenceTracingRiskRepository, - riskCombinator = RiskCombinator(TimeStamper()) + riskCombinator = RiskCombinator(TimeStamper()), + timeStamper = TimeStamper(), ) @Test diff --git a/translation_v2.json b/translation_v2.json index e6f6b8365062ad46c3389847b753cb4f964f6be1..e6b00ad77b2710265307def547d68c8165a88ec0 100644 --- a/translation_v2.json +++ b/translation_v2.json @@ -9,7 +9,8 @@ "strings.xml", "contact_diary_strings.xml", "release_info_strings.xml", - "event_registration_strings.xml" + "event_registration_strings.xml", + "antigen_strings.xml" ], "targetFolderPath": "../values-[langCode]" }