diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt index 75fe2040043c79c6bf15c8924f645f6b8bc96263..09353815926329df5bc8708202254f4a1786fd4a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt @@ -8,6 +8,7 @@ import de.rki.coronawarnapp.appconfig.mapping.CWAConfigMapper import de.rki.coronawarnapp.appconfig.mapping.ExposureDetectionConfigMapper import de.rki.coronawarnapp.appconfig.mapping.ExposureWindowRiskCalculationConfigMapper import de.rki.coronawarnapp.appconfig.mapping.KeyDownloadParametersMapper +import de.rki.coronawarnapp.appconfig.mapping.LogUploadConfigMapper import de.rki.coronawarnapp.appconfig.mapping.SurveyConfigMapper import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl @@ -73,6 +74,10 @@ class AppConfigModule { fun analyticsMapper(mapper: AnalyticsConfigMapper): AnalyticsConfig.Mapper = mapper + @Provides + fun logUploadMapper(mapper: LogUploadConfigMapper): + LogUploadConfig.Mapper = mapper + companion object { private val HTTP_TIMEOUT_APPCONFIG = Duration.standardSeconds(10) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/LogUploadConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/LogUploadConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d2b9a41f0c815703b3b15ee2d1bab3ea603b314 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/LogUploadConfig.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.appconfig + +import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper + +interface LogUploadConfig { + + val safetyNetRequirements: SafetyNetRequirements + + interface Mapper : ConfigMapper<LogUploadConfig> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt index a55c67fc24a41d1c6d884ce5298a325ce5ee02e5..92a3d6849f65f8f840adc2c7da261670658721b1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig +import de.rki.coronawarnapp.appconfig.LogUploadConfig import de.rki.coronawarnapp.appconfig.SurveyConfig import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid @@ -20,4 +21,6 @@ interface ConfigMapping : val survey: SurveyConfig val analytics: AnalyticsConfig + + val logUpload: LogUploadConfig } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt index 6c50b4cb724b4ef47b321eda5bdf788b3c186ff2..e9bf15f00f367c91d89bc5fca9e0d7c38cc16507 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt @@ -6,6 +6,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig +import de.rki.coronawarnapp.appconfig.LogUploadConfig import de.rki.coronawarnapp.appconfig.SurveyConfig import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid import timber.log.Timber @@ -18,7 +19,8 @@ class ConfigParser @Inject constructor( private val exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper, private val exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper, private val surveyConfigMapper: SurveyConfig.Mapper, - private val analyticsConfigMapper: AnalyticsConfig.Mapper + private val analyticsConfigMapper: AnalyticsConfig.Mapper, + private val logUploadConfigMapper: LogUploadConfig.Mapper, ) { fun parse(configBytes: ByteArray): ConfigMapping = try { @@ -30,7 +32,8 @@ class ConfigParser @Inject constructor( exposureDetectionConfig = exposureDetectionConfigMapper.map(it), exposureWindowRiskCalculationConfig = exposureWindowRiskCalculationConfigMapper.map(it), survey = surveyConfigMapper.map(it), - analytics = analyticsConfigMapper.map(it) + analytics = analyticsConfigMapper.map(it), + logUpload = logUploadConfigMapper.map(it) ) } } catch (e: Exception) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt index f7063cbff7fd53434f584136dfbdf603b0524ae0..9ebbe211688cb22f5222f1b29e9d2d2b64577004 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig +import de.rki.coronawarnapp.appconfig.LogUploadConfig import de.rki.coronawarnapp.appconfig.SurveyConfig import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid @@ -15,7 +16,8 @@ data class DefaultConfigMapping( val exposureDetectionConfig: ExposureDetectionConfig, val exposureWindowRiskCalculationConfig: ExposureWindowRiskCalculationConfig, override val survey: SurveyConfig, - override val analytics: AnalyticsConfig + override val analytics: AnalyticsConfig, + override val logUpload: LogUploadConfig ) : ConfigMapping, CWAConfig by cwaConfig, KeyDownloadConfig by keyDownloadConfig, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/LogUploadConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/LogUploadConfigMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d30be7e16d471111f4bf239190da15c30d3f325 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/LogUploadConfigMapper.kt @@ -0,0 +1,38 @@ +package de.rki.coronawarnapp.appconfig.mapping + +import dagger.Reusable +import de.rki.coronawarnapp.appconfig.LogUploadConfig +import de.rki.coronawarnapp.appconfig.SafetyNetRequirements +import de.rki.coronawarnapp.appconfig.SafetyNetRequirementsContainer +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class LogUploadConfigMapper @Inject constructor() : LogUploadConfig.Mapper { + + override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): LogUploadConfig { + if (!rawConfig.hasErrorLogSharingParameters()) { + Timber.w("No error log sharing parameters found, returning defaults.") + return LogUploadConfigContainer() + } + return LogUploadConfigContainer( + safetyNetRequirements = rawConfig.mapSafetyNet() + ) + } + + private fun AppConfigAndroid.ApplicationConfigurationAndroid.mapSafetyNet(): SafetyNetRequirementsContainer { + return privacyPreservingAnalyticsParameters.ppac.let { + SafetyNetRequirementsContainer( + requireBasicIntegrity = it.requireBasicIntegrity, + requireCTSProfileMatch = it.requireCTSProfileMatch, + requireEvaluationTypeBasic = it.requireEvaluationTypeBasic, + requireEvaluationTypeHardwareBacked = it.requireEvaluationTypeHardwareBacked + ) + } + } + + data class LogUploadConfigContainer( + override val safetyNetRequirements: SafetyNetRequirements = SafetyNetRequirementsContainer() + ) : LogUploadConfig +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt new file mode 100644 index 0000000000000000000000000000000000000000..c8a451d8003001bc7b7e40e6c82e299642b45d22 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSettings.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.bugreporting + +import android.content.Context +import com.google.gson.Gson +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.UploadHistory +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.preferences.FlowPreference +import de.rki.coronawarnapp.util.preferences.clearAndNotify +import de.rki.coronawarnapp.util.preferences.createFlowPreference +import de.rki.coronawarnapp.util.serialization.BaseGson +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BugReportingSettings @Inject constructor( + @AppContext private val context: Context, + @BaseGson private val gson: Gson +) { + + private val prefs by lazy { + context.getSharedPreferences("bugreporting_localdata", Context.MODE_PRIVATE) + } + + val uploadHistory: FlowPreference<UploadHistory> = prefs.createFlowPreference( + key = "upload.history", + reader = FlowPreference.gsonReader(gson, UploadHistory()), + writer = FlowPreference.gsonWriter(gson) + ) + + fun clear() = prefs.clearAndNotify() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt index cfb6696c5fc9f3fd965e79a27e7f01d311fe0420..70cf264d396708e97412548538bd50aafa167d42 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt @@ -2,24 +2,65 @@ package de.rki.coronawarnapp.bugreporting import dagger.Module import dagger.Provides +import dagger.Reusable import de.rki.coronawarnapp.bugreporting.censors.BugCensor import de.rki.coronawarnapp.bugreporting.censors.DiaryLocationCensor import de.rki.coronawarnapp.bugreporting.censors.DiaryPersonCensor import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor import de.rki.coronawarnapp.bugreporting.censors.RegistrationTokenCensor -import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger -import de.rki.coronawarnapp.bugreporting.debuglog.DebugLoggerScope -import de.rki.coronawarnapp.bugreporting.debuglog.DebuggerScope +import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebugLoggerScope +import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadApiV1 +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadAuthApiV1 +import de.rki.coronawarnapp.environment.bugreporting.LogUploadHttpClient +import de.rki.coronawarnapp.environment.bugreporting.LogUploadServerUrl +import de.rki.coronawarnapp.environment.datadonation.DataDonationCDNHttpClient +import de.rki.coronawarnapp.environment.datadonation.DataDonationCDNServerUrl +import de.rki.coronawarnapp.util.CWADebug import kotlinx.coroutines.CoroutineScope +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.protobuf.ProtoConverterFactory import timber.log.Timber import javax.inject.Singleton @Module class BugReportingSharedModule { + @Reusable + @Provides + fun logUploadApi( + @LogUploadHttpClient client: OkHttpClient, + @LogUploadServerUrl url: String, + protoConverterFactory: ProtoConverterFactory, + gsonConverterFactory: GsonConverterFactory + ): LogUploadApiV1 = Retrofit.Builder() + .client(client) + .baseUrl(url) + .addConverterFactory(protoConverterFactory) + .addConverterFactory(gsonConverterFactory) + .build() + .create(LogUploadApiV1::class.java) + + @Reusable + @Provides + fun logUploadAuthApi( + @DataDonationCDNHttpClient client: OkHttpClient, + @DataDonationCDNServerUrl url: String, + protoConverterFactory: ProtoConverterFactory, + gsonConverterFactory: GsonConverterFactory + ): LogUploadAuthApiV1 = Retrofit.Builder() + .client(client) + .baseUrl(url) + .addConverterFactory(protoConverterFactory) + .addConverterFactory(gsonConverterFactory) + .build() + .create(LogUploadAuthApiV1::class.java) + @Singleton @Provides - fun debugLogger() = DebugLogger + fun debugLogger() = CWADebug.debugLogger @Singleton @DebuggerScope diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt index 6a30f19455a285626af3ae95ad45c2ca044f3f00..61dc4cc8d637eb73fcb54388058daf3eab9ce3fe 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensor.kt @@ -1,8 +1,8 @@ package de.rki.coronawarnapp.bugreporting.censors import dagger.Reusable -import de.rki.coronawarnapp.bugreporting.debuglog.DebuggerScope import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.util.CWADebug import kotlinx.coroutines.CoroutineScope diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt index f2406f14ec04ace933a352628fbed51aed8425b5..591c8db2cfc412e096e96393ac74ddc6e6669650 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensor.kt @@ -1,8 +1,8 @@ package de.rki.coronawarnapp.bugreporting.censors import dagger.Reusable -import de.rki.coronawarnapp.bugreporting.debuglog.DebuggerScope import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.util.CWADebug import kotlinx.coroutines.CoroutineScope diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt index e6e33aeeb1e308b9af1237b3d52575b7560cfeec..57f7e38a498fc264bd3d577753690e6c9bc0ea09 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogger.kt @@ -1,15 +1,22 @@ package de.rki.coronawarnapp.bugreporting.debuglog import android.annotation.SuppressLint -import android.app.Application import android.content.Context import android.util.Log +import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebugLogStorageCheck +import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebugLogTree +import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebugLoggerScope +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogWriter import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.di.ApplicationComponent import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex @@ -18,53 +25,61 @@ import kotlinx.coroutines.yield import timber.log.Timber import java.io.File -@SuppressLint("LogNotTimber") +@SuppressLint("LogNotTimber", "StaticFieldLeak") @Suppress("BlockingMethodInNonBlockingContext") -object DebugLogger : DebugLoggerBase() { +class DebugLogger( + private val scope: CoroutineScope = DebugLoggerScope, + private val context: Context, + private val debugDir: File = File(context.cacheDir, "debuglog"), + private val logWriter: LogWriter = LogWriter(File(debugDir, "debug.log")) +) : DebugLoggerBase() { - private val scope = DebugLoggerScope - private lateinit var context: Context - - private val debugDir by lazy { - File(context.cacheDir, "debuglog").also { - if (!it.exists()) it.mkdir() - } - } - - private val triggerFile by lazy { File(debugDir, "debug.trigger") } - - internal val runningLog by lazy { File(debugDir, "debug.log") } - val sharedDirectory by lazy { File(debugDir, "shared") } + private val triggerFile = File(debugDir, "debug.trigger") + internal val runningLog: File + get() = logWriter.logFile private val mutex = Mutex() - private var logJob: Job? = null private var logTree: DebugLogTree? = null + private var isDaggerReady = false - fun init(application: Application) { - context = application + val storageCheck = DebugLogStorageCheck(targetPath = debugDir, logWriter = logWriter) + internal val isLogging = MutableStateFlow(false) + + val logState: Flow<LogState> = combine( + storageCheck.isLowStorage, + logWriter.logSize, + isLogging + ) { isLowStorage, logSize, isRunning -> + LogState( + isLogging = isRunning, + isLowStorage = isLowStorage, + logSize = logSize + ) + } - try { - val startLogger = when { - triggerFile.exists() -> { - Timber.tag(TAG).i("Trigger file exists, starting debug log.") - true - } - CWADebug.isDeviceForTestersBuild -> { - Timber.tag(TAG).i("Trigger file does not exist, but it's a tester build, starting debug log.") - true - } - else -> false + fun init() = try { + val startLogger = when { + triggerFile.exists() -> { + Timber.tag(TAG).i("Trigger file exists, starting debug log.") + true } - - if (startLogger) { - runBlocking { start() } + CWADebug.isDeviceForTestersBuild -> { + Timber.tag(TAG).i("Trigger file does not exist, but it's a tester build, starting debug log.") + true } - } catch (e: Exception) { - // This is called from Application.onCreate() never crash here. - Timber.tag(TAG).e(e, "DebugLogger init(%s) failed.", application) + else -> false } + + if (startLogger) { + runBlocking { start() } + } + + Unit + } catch (e: Exception) { + // This is called via Application.onCreate() never crash here. + Timber.tag(TAG).e(e, "DebugLogger init() failed.") } /** @@ -77,51 +92,29 @@ object DebugLogger : DebugLoggerBase() { isDaggerReady = true } - val isLogging: Boolean - get() = logJob?.isActive == true - suspend fun start(): Unit = mutex.withLock { Timber.tag(TAG).d("start()") - if (isLogging) { + if (logJob?.isActive == true) { Timber.tag(TAG).w("Ignoring start(), already running.") return@withLock } + isLogging.value = true logJob?.cancel() logTree?.let { Timber.uproot(it) } - DebugLogTree().apply { - Timber.plant(this) - logTree = this + if (!debugDir.exists()) { + debugDir.mkdirs() + } - if (!runningLog.exists()) { - runningLog.parentFile?.mkdirs() - if (runningLog.createNewFile()) { - Timber.tag(TAG).i("Log file didn't exist and was created.") - } - } + logTree = DebugLogTree().also { tree -> + Timber.plant(tree) - logJob = scope.launch { - try { - logLines.collect { rawLine -> - while (!isDaggerReady) { - yield() - } - launch { - // Censor data sources need a moment to know what to censor - delay(1000) - val censoredLine = bugCensors.get().fold(rawLine) { prev, censor -> - censor.checkLog(prev) ?: prev - } - appendLogLine(censoredLine) - } - } - } catch (e: CancellationException) { - Timber.tag(TAG).i("Logging was canceled.") - } catch (e: Exception) { - Log.e(TAG, "Failed to call appendLogLine(...)", e) - } + logWriter.setup() + + logJob = startNewLogJob(tree.logLines).apply { + invokeOnCompletion { isLogging.value = false } } } @@ -151,35 +144,35 @@ object DebugLogger : DebugLoggerBase() { } logJob = null - if (runningLog.exists() && runningLog.delete()) { - Timber.tag(TAG).d("Log file was deleted.") - } - - clearSharedFiles() + logWriter.teardown() } - private fun appendLogLine(line: LogLine) { - val formattedLine = line.format() - runningLog.appendText(formattedLine, Charsets.UTF_8) - } - - fun getLogSize(): Long = runningLog.length() - - fun getShareSize(): Long = sharedDirectory.listFiles() - ?.fold(0L) { prev, file -> prev + file.length() } - ?: 0L + private fun startNewLogJob(logLines: Flow<LogLine>) = scope.launch { + try { + logLines.collect { rawLine -> + while (!isDaggerReady) { + yield() + } - fun clearSharedFiles() { - if (!sharedDirectory.exists()) return + if (storageCheck.isLowStorage()) return@collect - sharedDirectory.listFiles()?.forEach { - if (it.delete()) { - Timber.tag(TAG).d("Deleted shared file: %s", it) - } else { - Timber.tag(TAG).w("Failed to delete shared file: %s", it) + launch { + // Censor data sources need a moment to know what to censor + delay(1000) + val censoredLine = bugCensors.get().fold(rawLine) { prev, censor -> + censor.checkLog(prev) ?: prev + } + logWriter.write(censoredLine) + } } + } catch (e: CancellationException) { + Timber.tag(TAG).i("Logging was canceled.") + } catch (e: Exception) { + Log.e(TAG, "Failed to call appendLogLine(...)", e) } } - private const val TAG = "DebugLogger" + companion object { + internal const val TAG = "DebugLogger" + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/LogState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/LogState.kt new file mode 100644 index 0000000000000000000000000000000000000000..f995538832697433486ac49d5cf0b602b5841636 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/LogState.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.bugreporting.debuglog + +data class LogState( + val isLogging: Boolean, + val isLowStorage: Boolean, + val logSize: Long +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExport.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExport.kt new file mode 100644 index 0000000000000000000000000000000000000000..12d91f7ef912e45f33766f9838c2a8499e784c01 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExport.kt @@ -0,0 +1,62 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.export + +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter +import de.rki.coronawarnapp.util.files.determineMimeType +import okio.buffer +import okio.sink +import okio.source +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SAFLogExport @Inject constructor() { + private var lastId = 1 + private val requestMap = mutableMapOf<Int, Request>() + + fun createSAFRequest(snapshot: LogSnapshotter.Snapshot): Request { + val request = Request( + id = ++lastId, + snapshot = snapshot + ) + requestMap[request.id] = request + return request + } + + fun getRequest(id: Int): Request? = requestMap[id] + + data class Request( + val id: Int, + val snapshot: LogSnapshotter.Snapshot, + ) { + fun createIntent() = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = snapshot.path.determineMimeType() + putExtra(Intent.EXTRA_TITLE, snapshot.path.name) + } + + fun storeSnapshot(resolver: ContentResolver, uri: Uri): Result { + Timber.tag(TAG).d("Writing to %s", uri) + resolver.openOutputStream(uri)!!.sink().buffer().use { dest -> + snapshot.path.source().buffer().use { source -> + dest.writeAll(source) + } + } + Timber.tag(TAG).i("%s was written to %s", snapshot, uri) + + snapshot.delete().also { + Timber.tag(TAG).d("Snapshot deleted: %s", snapshot) + } + return Result(uri) + } + + data class Result(val storageUri: Uri) + } + + companion object { + private const val TAG = "SAFLogSharing" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLogStorageCheck.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLogStorageCheck.kt new file mode 100644 index 0000000000000000000000000000000000000000..2e8e7aba28e5d7ff98e47bd3e462ad56739a48d4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLogStorageCheck.kt @@ -0,0 +1,78 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.internal + +import android.annotation.SuppressLint +import android.util.Log +import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger +import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import kotlinx.coroutines.flow.MutableStateFlow +import java.io.File +import javax.inject.Inject + +@SuppressLint("LogNotTimber") +class DebugLogStorageCheck @Inject constructor( + private val targetPath: File, + private val lowStorageLimit: Long = 200 * 1000 * 1024L, // 200MB default + private val timeProvider: () -> Long = { System.currentTimeMillis() }, + private val logWriter: LogWriter +) { + val isLowStorage = MutableStateFlow(false) + private var lastCheckAt: Long = 0 + + private val availableSpace: Long + @SuppressLint("UsableSpace") + get() = targetPath.usableSpace + + fun isLowStorage(forceCheck: Boolean = false): Boolean { + val now = timeProvider() + if (!forceCheck && now - lastCheckAt < 5_000) return isLowStorage.value + + val currentSpace = try { + availableSpace + } catch (e: Exception) { + Log.e(TAG, "Failed to call isLowStorage()", e) + logWriter.write(createStorageCheckErrorLine(e)) + return true + } + + val isNowLow = currentSpace < lowStorageLimit + lastCheckAt = now + + when { + isNowLow && !isLowStorage.value -> { + isLowStorage.value = true + logWriter.write(createLowStorageLogLine()) + } + !isNowLow -> { + isLowStorage.value = false + } + } + + if (isNowLow) { + Log.w(TAG, "Not enough storage to write debug log (${currentSpace}B free)") + } + + return isNowLow + } + + companion object { + private const val TAG = DebugLogger.TAG + private val createStorageCheckErrorLine: (Throwable) -> LogLine = { + LogLine( + timestamp = System.currentTimeMillis(), + priority = Log.ERROR, + tag = TAG, + message = "Low storage check failed.", + throwable = it + ) + } + private val createLowStorageLogLine: () -> LogLine = { + LogLine( + timestamp = System.currentTimeMillis(), + priority = Log.WARN, + tag = TAG, + message = "Low storage, debug logger halted.", + throwable = null + ) + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogTree.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLogTree.kt similarity index 88% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogTree.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLogTree.kt index 68dc7f45181883d2907138e228231a228866675b..f0620098244ac14c51d4211636b9ad12b6a38a12 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLogTree.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLogTree.kt @@ -1,5 +1,6 @@ -package de.rki.coronawarnapp.bugreporting.debuglog +package de.rki.coronawarnapp.bugreporting.debuglog.internal +import de.rki.coronawarnapp.bugreporting.debuglog.LogLine import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerScope.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLoggerScope.kt similarity index 91% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerScope.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLoggerScope.kt index 6b48f8632877b7999c9e56a5fa2ce704788d104a..9e60dc67be1c4e384fe51ab05f5b545aabd8625b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerScope.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLoggerScope.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.bugreporting.debuglog +package de.rki.coronawarnapp.bugreporting.debuglog.internal import de.rki.coronawarnapp.util.threads.NamedThreadFactory import kotlinx.coroutines.CoroutineScope diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotter.kt new file mode 100644 index 0000000000000000000000000000000000000000..8bf4d816bfdff47cc35fe8a7535243b206097d78 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotter.kt @@ -0,0 +1,57 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.internal + +import android.content.Context +import dagger.Reusable +import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.files.Zipper +import org.joda.time.format.DateTimeFormat +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +@Reusable +class LogSnapshotter @Inject constructor( + @AppContext private val context: Context, + private val debugLogger: DebugLogger, + private val timeStamper: TimeStamper +) { + + private val snapshotDir = File(context.cacheDir, "debuglog_snapshots") + + /** + * Use **[Snapshot#delete]** after you are done. + * Otherwise it will be deleted when another snapshot is taken. + * @return a snapshot of the current debug log, or null if there was no log + */ + fun snapshot(): Snapshot { + Timber.tag(TAG).d("snapshot()") + + snapshotDir.listFiles()?.forEach { + if (it.delete()) Timber.tag(TAG).w("Deleted stale snapshot: %s", it) + } + + val now = timeStamper.nowUTC + val formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS") + val formattedFileName = "CWA Log ${now.toString(formatter)}" + if (!snapshotDir.exists() && snapshotDir.mkdirs()) { + Timber.tag(TAG).v("Created %s", snapshotDir) + } + val zipFile = File(snapshotDir, "$formattedFileName.zip") + + Zipper(zipFile).zip( + listOf(Zipper.Entry(name = "$formattedFileName.txt", path = debugLogger.runningLog)) + ) + + return Snapshot(path = zipFile) + } + + data class Snapshot(val path: File) { + fun delete() = path.delete() + } + + companion object { + private const val TAG = "LogSnapshots" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogWriter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogWriter.kt new file mode 100644 index 0000000000000000000000000000000000000000..ebb188567e11bf2a885afb9d35f2018ce6cbb09d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogWriter.kt @@ -0,0 +1,44 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.internal + +import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import kotlinx.coroutines.flow.MutableStateFlow +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +class LogWriter @Inject constructor(val logFile: File) { + private var ioLimiter = 0 + val logSize = MutableStateFlow(logFile.length()) + + private fun updateLogSize() { + logSize.value = logFile.length() + } + + fun setup() { + if (!logFile.exists()) { + logFile.parentFile?.mkdirs() + if (logFile.createNewFile()) { + Timber.i("Log file didn't exist and was created.") + } + } + updateLogSize() + } + + fun teardown() { + if (logFile.exists() && logFile.delete()) { + Timber.d("Log file was deleted.") + } + updateLogSize() + } + + fun write(line: LogLine) { + val formattedLine = line.format() + logFile.appendText(formattedLine, Charsets.UTF_8) + + if (ioLimiter % 10 == 0) { + updateLogSize() + ioLimiter = 0 + } + ioLimiter++ + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragment.kt index def69cfa0727f9ec0c22fbdfad50849ca48e38ac..41f63891d30d37f5c88b944ffb680da7b35a4bef 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragment.kt @@ -1,19 +1,29 @@ package de.rki.coronawarnapp.bugreporting.debuglog.ui +import android.app.Activity +import android.content.Intent import android.os.Bundle +import android.provider.Settings import android.text.format.Formatter import android.view.View import android.widget.Toast import androidx.core.view.isGone import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.BugreportingDebuglogFragmentBinding +import de.rki.coronawarnapp.util.ContextExtensions.getDrawableCompat import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.setUrl +import de.rki.coronawarnapp.util.tryHumanReadableError +import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.setGone import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import timber.log.Timber import javax.inject.Inject class DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), AutoInject { @@ -22,41 +32,170 @@ class DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), Auto private val vm: DebugLogViewModel by cwaViewModels { viewModelFactory } private val binding: BugreportingDebuglogFragmentBinding by viewBindingLazy() + @Suppress("ComplexMethod") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Static screen elements + binding.apply { + toolbar.setNavigationOnClickListener { popBackStack() } + + if (explanationSectionTwo.text == + getString(R.string.debugging_debuglog_intro_explanation_section_two) + ) { + explanationSectionTwo.setUrl( + R.string.debugging_debuglog_intro_explanation_section_two, + R.string.debugging_debuglog_intro_explanation_section_two_link_label, + R.string.debugging_debuglog_intro_explanation_section_two_faq_link + ) + } + debugLogPrivacyInformation.setOnClickListener { vm.onPrivacyButtonPress() } + } + vm.state.observe2(this) { binding.apply { - debuglogActivityIndicator.isGone = !it.isRecording - debuglogStatusPrimary.text = getString( - if (it.isRecording) R.string.debugging_debuglog_status_recording - else R.string.debugging_debuglog_status_not_recording + debuglogActivityIndicator.setImageDrawable( + requireContext().getDrawableCompat( + if (it.isRecording) R.drawable.ic_debug_log_indicator_activated + else R.drawable.ic_debug_log_indicator_deactivated + ) ) - debuglogStatusSecondary.text = getString( - R.string.debugging_debuglog_status_additional_infos, - Formatter.formatFileSize(context, it.currentSize) - ) - toggleRecording.text = getString( - if (it.isRecording) R.string.debugging_debuglog_action_stop_recording - else R.string.debugging_debuglog_action_start_recording + + debuglogStatusPrimary.text = getString( + when { + it.isRecording && it.isLowStorage -> R.string.debugging_debuglog_status_lowstorage + it.isRecording -> R.string.debugging_debuglog_status_recording + else -> R.string.debugging_debuglog_status_not_recording + } ) - shareRecording.isEnabled = it.currentSize > 0L && !it.sharingInProgress - toggleRecording.isEnabled = !it.sharingInProgress + + debuglogStatusSecondary.text = + getString( + R.string.debugging_debuglog_status_additional_infos, + Formatter.formatFileSize(context, it.currentSize) + ) + + toggleRecording.apply { + isActivated = it.isRecording + isEnabled = !it.isActionInProgress + text = getString( + if (it.isRecording) R.string.debugging_debuglog_action_stop_recording + else R.string.debugging_debuglog_action_start_recording + ) + setOnClickListener { vm.onToggleRecording() } + } + + toggleSendErrorLog.apply { + isGone = !it.isRecording + isEnabled = it.currentSize > 0L && !it.isActionInProgress + setOnClickListener { vm.onShareButtonPress() } + } + + toggleStoreLog.apply { + isGone = !it.isRecording + isEnabled = it.currentSize > 0L && !it.isActionInProgress + setOnClickListener { vm.onStoreLog() } + } } } - vm.errorEvent.observe2(this) { - Toast.makeText(requireContext(), it.toString(), Toast.LENGTH_LONG).show() + vm.events.observe2(this) { + when (it) { + DebugLogViewModel.Event.ShowLogDeletedConfirmation -> { + showLogDeletionConfirmation() + } + DebugLogViewModel.Event.NavigateToPrivacyFragment -> { + doNavigate( + DebugLogFragmentDirections.actionDebuglogFragmentToInformationPrivacyFragment() + ) + } + DebugLogViewModel.Event.NavigateToUploadFragment -> { + doNavigate( + DebugLogFragmentDirections.actionDebuglogFragmentToDebugLogUploadFragment() + ) + } + DebugLogViewModel.Event.NavigateToUploadHistory -> { + doNavigate( + DebugLogFragmentDirections.actionDebuglogFragmentToLogUploadHistoryFragment() + ) + } + is DebugLogViewModel.Event.Error -> { + Toast.makeText(requireContext(), it.toString(), Toast.LENGTH_LONG).show() + } + DebugLogViewModel.Event.ShowLowStorageDialog -> { + showLowStorageError() + } + is DebugLogViewModel.Event.LocalExport -> { + startActivityForResult(it.request.createIntent(), it.request.id) + } + is DebugLogViewModel.Event.ExportResult -> { + showExportResult() + } + is DebugLogViewModel.Event.ShowLocalExportError -> { + showLocalExportError(it.error) + } + } } - vm.shareEvent.observe2(this) { - startActivity(it.get(requireActivity())) + vm.logUploads.observe2(this@DebugLogFragment) { + binding.debugLogHistoryContainer.setGone(it.logs.isEmpty()) } + binding.debugLogHistoryContainer.setOnClickListener { vm.onIdHistoryPress() } + } - binding.apply { - toggleRecording.setOnClickListener { vm.toggleRecording() } - shareRecording.setOnClickListener { vm.shareRecording() } - toolbar.setNavigationOnClickListener { popBackStack() } - } + override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { + Timber.d("onActivityResult(requestCode=$requestCode, resultCode=$resultCode, resultData=$resultData") + vm.processSAFResult( + requestCode, + if (resultCode == Activity.RESULT_OK) resultData?.data else null + ) + } + + private fun showLogDeletionConfirmation() { + MaterialAlertDialogBuilder(requireContext()).apply { + setMessage(R.string.debugging_debuglog_stop_confirmation_message) + setPositiveButton(android.R.string.yes) { _, _ -> } + }.show() + } + + private fun showLowStorageError() { + MaterialAlertDialogBuilder(requireContext()).apply { + setTitle(R.string.errors_generic_headline_short) + setMessage(R.string.debugging_debuglog_start_low_storage_error) + setPositiveButton(android.R.string.yes) { _, _ -> /* dismiss */ } + setNeutralButton(R.string.menu_settings) { _, _ -> + try { + startActivity(Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS)) + } catch (e: Exception) { + Toast.makeText(requireContext(), e.toString(), Toast.LENGTH_LONG).show() + } + } + }.show() + } + + private fun showExportResult() { + MaterialAlertDialogBuilder(requireContext()).apply { + setTitle(R.string.debugging_debuglog_localexport_title) + setMessage(R.string.debugging_debuglog_localexport_message) + setPositiveButton(android.R.string.yes) { _, _ -> /* dismiss */ } + }.show() + } + + private fun showLocalExportError(cause: Throwable) { + MaterialAlertDialogBuilder(requireContext()).apply { + setTitle(R.string.errors_generic_headline_short) + setMessage( + getString(R.string.debugging_debuglog_localexport_error_message) + "\n(" + + cause.tryHumanReadableError(requireContext()).description + ")" + ) + setPositiveButton(android.R.string.yes) { _, _ -> /* dismiss */ } + setNeutralButton(R.string.menu_settings) { _, _ -> + try { + startActivity(Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS)) + } catch (e: Exception) { + Toast.makeText(requireContext(), e.toString(), Toast.LENGTH_LONG).show() + } + } + }.show() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragmentModule.kt index 11725335abd04709340bc9f82ebedba55b01d473..fc8c3129c27082fc9be655365958a5f2677420a8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragmentModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragmentModule.kt @@ -13,8 +13,8 @@ abstract class DebugLogFragmentModule { @Binds @IntoMap @CWAViewModelKey(DebugLogViewModel::class) - abstract fun onboardingNotificationsVM(factory: DebugLogViewModel.Factory): CWAViewModelFactory<out CWAViewModel> + abstract fun debugLogViewModel(factory: DebugLogViewModel.Factory): CWAViewModelFactory<out CWAViewModel> @ContributesAndroidInjector - abstract fun debuglogFragment(): DebugLogFragment + abstract fun debugLogFragment(): DebugLogFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogNavigationEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogNavigationEvents.kt new file mode 100644 index 0000000000000000000000000000000000000000..76d9704a1087780794cb20f792b7eee145b25366 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogNavigationEvents.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui + +sealed class DebugLogNavigationEvents { + object NavigateToPrivacyFragment : DebugLogNavigationEvents() + object NavigateToUploadHistory : DebugLogNavigationEvents() + object NavigateToShareFragment : DebugLogNavigationEvents() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt index d31e9f34ad2ea548ae5195d118ad0a5dba0d0bd5..ec5b7e32885e1c8442c4ea6db27388008d247144 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt @@ -1,116 +1,160 @@ package de.rki.coronawarnapp.bugreporting.debuglog.ui +import android.content.ContentResolver +import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.bugreporting.BugReportingSettings import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger +import de.rki.coronawarnapp.bugreporting.debuglog.export.SAFLogExport +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.util.CWADebug -import de.rki.coronawarnapp.util.TimeStamper -import de.rki.coronawarnapp.util.compression.Zipper import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import de.rki.coronawarnapp.util.sharing.FileSharing import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import kotlinx.coroutines.delay +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flow -import org.joda.time.format.DateTimeFormat import timber.log.Timber -import java.io.File +import java.io.IOException class DebugLogViewModel @AssistedInject constructor( private val debugLogger: DebugLogger, dispatcherProvider: DispatcherProvider, - private val timeStamper: TimeStamper, - private val fileSharing: FileSharing, - private val enfClient: ENFClient + private val enfClient: ENFClient, + bugReportingSettings: BugReportingSettings, + private val logSnapshotter: LogSnapshotter, + private val safLogExport: SAFLogExport, + private val contentResolver: ContentResolver, ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { - private val ticker = flow { - while (true) { - emit(Unit) - delay(500) - } - } - private val manualTick = MutableStateFlow(Unit) - private val sharingInProgress = MutableStateFlow(false) - val state: LiveData<State> = combine(ticker, manualTick, sharingInProgress) { _, _, sharingInProgress -> + + private val isActionInProgress = MutableStateFlow(false) + + val logUploads = bugReportingSettings.uploadHistory.flow + .asLiveData(context = dispatcherProvider.Default) + + val state: LiveData<State> = combine( + isActionInProgress, + debugLogger.logState + ) { isActionInProgress, logState -> State( - isRecording = debugLogger.isLogging, - currentSize = debugLogger.getLogSize() + debugLogger.getShareSize(), - sharingInProgress = sharingInProgress + isRecording = logState.isLogging, + isLowStorage = logState.isLowStorage, + currentSize = logState.logSize, + isActionInProgress = isActionInProgress ) }.asLiveData(context = dispatcherProvider.Default) - val errorEvent = SingleLiveEvent<Throwable>() - val shareEvent = SingleLiveEvent<FileSharing.ShareIntentProvider>() + val events = SingleLiveEvent<Event>() - fun toggleRecording() = launch { - try { - if (debugLogger.isLogging) { - debugLogger.stop() - } else { - debugLogger.start() - printExtendedLogInfos() + fun onPrivacyButtonPress() { + events.postValue(Event.NavigateToPrivacyFragment) + } + + fun onShareButtonPress() { + events.postValue(Event.NavigateToUploadFragment) + } + + fun onIdHistoryPress() { + events.postValue(Event.NavigateToUploadHistory) + } + + fun onToggleRecording() = launchWithProgress { + if (debugLogger.isLogging.value) { + debugLogger.stop() + events.postValue(Event.ShowLogDeletedConfirmation) + } else { + if (debugLogger.storageCheck.isLowStorage(forceCheck = true)) { + Timber.d("Low storage, not starting logger.") + events.postValue(Event.ShowLowStorageDialog) + return@launchWithProgress + } + + debugLogger.start() + + CWADebug.logDeviceInfos() + try { + val enfVersion = enfClient.getENFClientVersion() + Timber.tag("ENFClient").i("ENF Version: %d", enfVersion) + } catch (e: Exception) { + Timber.tag("ENFClient").e(e, "Failed to get ENF version for debug log.") } - } catch (e: Exception) { - errorEvent.postValue(e) - } finally { - manualTick.value = Unit } } - private suspend fun printExtendedLogInfos() { - CWADebug.logDeviceInfos() - try { - val enfVersion = enfClient.getENFClientVersion() - Timber.tag("ENFClient").i("ENF Version: %d", enfVersion) - } catch (e: Exception) { - Timber.tag("ENFClient").e(e, "Failed to get ENF version for debug log.") - } + fun onStoreLog() = launchWithProgress(finishProgressAction = false) { + Timber.d("storeLog()") + val snapshot = logSnapshotter.snapshot() + val shareRequest = safLogExport.createSAFRequest(snapshot) + events.postValue(Event.LocalExport(shareRequest)) } - fun shareRecording() { - sharingInProgress.value = true - launch { - try { - debugLogger.clearSharedFiles() + fun processSAFResult(requestCode: Int, safPath: Uri?) = launchWithProgress { + if (safPath == null) { + Timber.i("No SAF path available.") + return@launchWithProgress + } - val now = timeStamper.nowUTC - val formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS") - val formattedFileName = "CWA Log ${now.toString(formatter)}" - val zipFile = File(debugLogger.sharedDirectory, "$formattedFileName.zip") + val request = safLogExport.getRequest(requestCode) + if (request == null) { + Timber.w("Unknown request with code $requestCode") + return@launchWithProgress + } - Zipper(zipFile).zip( - listOf(Zipper.Entry(name = "$formattedFileName.txt", path = debugLogger.runningLog)) - ) + try { + val storageResult = request.storeSnapshot(contentResolver, safPath) + Timber.i("Log stored %s", storageResult) + events.postValue(Event.ExportResult(storageResult)) + } catch (e: IOException) { + Timber.e(e, "Failed to store log file.") + events.postValue(Event.ShowLocalExportError(e)) + } + } - val intentProvider = fileSharing.getIntentProvider( - path = zipFile, - title = zipFile.name, - chooserTitle = R.string.debugging_debuglog_sharing_dialog_title - ) + private fun launchWithProgress( + finishProgressAction: Boolean = true, + block: suspend CoroutineScope.() -> Unit + ) { + val startTime = System.currentTimeMillis() + isActionInProgress.value = true - shareEvent.postValue(intentProvider) - } catch (e: Exception) { - Timber.e(e, "Sharing debug log failed.") - errorEvent.postValue(e) + launch { + try { + block() + } catch (e: Throwable) { + Timber.e(e, "launchWithProgress() failed.") + events.postValue(Event.Error(e)) } finally { - sharingInProgress.value = false + val duration = System.currentTimeMillis() - startTime + Timber.v("launchWithProgress() took ${duration}ms") + if (finishProgressAction) isActionInProgress.value = false } } } data class State( val isRecording: Boolean, - val sharingInProgress: Boolean = false, + val isLowStorage: Boolean, + val isActionInProgress: Boolean = false, val currentSize: Long = 0 ) + sealed class Event { + object NavigateToPrivacyFragment : Event() + object NavigateToUploadFragment : Event() + object NavigateToUploadHistory : Event() + object ShowLogDeletedConfirmation : Event() + object ShowLowStorageDialog : Event() + data class ShowLocalExportError(val error: Throwable) : Event() + data class Error(val error: Throwable) : Event() + data class LocalExport(val request: SAFLogExport.Request) : Event() + data class ExportResult(val result: SAFLogExport.Request.Result) : Event() + } + @AssistedFactory interface Factory : SimpleCWAViewModelFactory<DebugLogViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/legal/DebugLogLegalFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/legal/DebugLogLegalFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..a56f0df1882c8fceef57fe95538714f45a7870ec --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/legal/DebugLogLegalFragment.kt @@ -0,0 +1,36 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.legal + +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityEvent +import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.BugreportingLegalFragmentBinding +import de.rki.coronawarnapp.util.di.AutoInject +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.cwaViewModels +import javax.inject.Inject + +class DebugLogLegalFragment : Fragment(R.layout.bugreporting_legal_fragment), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + + private val vm: DebugLogLegalViewModel by cwaViewModels { viewModelFactory } + private val binding: BugreportingLegalFragmentBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.apply { + toolbar.setOnClickListener { + popBackStack() + } + } + } + + override fun onResume() { + super.onResume() + binding.contentContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/legal/DebugLogLegalModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/legal/DebugLogLegalModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..d3a707cb25c2b02946ca26951387da38a2ca23ec --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/legal/DebugLogLegalModule.kt @@ -0,0 +1,20 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.legal + +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector +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 DebugLogLegalModule { + @Binds + @IntoMap + @CWAViewModelKey(DebugLogLegalViewModel::class) + abstract fun debugLogLegalViewModel(factory: DebugLogLegalViewModel.Factory): CWAViewModelFactory<out CWAViewModel> + + @ContributesAndroidInjector + abstract fun debugLogLegalFragment(): DebugLogLegalFragment +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/legal/DebugLogLegalViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/legal/DebugLogLegalViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a162171fa7bacc589a588c50bdb6ba5b1eb4c50 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/legal/DebugLogLegalViewModel.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.legal + +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class DebugLogLegalViewModel @AssistedInject constructor() : CWAViewModel() { + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<DebugLogLegalViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..6aac39aad2378643f4bd87797f38c2fb34e66bb8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragment.kt @@ -0,0 +1,69 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload + +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityEvent +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.BugreportingDebuglogUploadFragmentBinding +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.tryHumanReadableError +import de.rki.coronawarnapp.util.ui.doNavigate +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.cwaViewModels +import javax.inject.Inject + +class DebugLogUploadFragment : Fragment(R.layout.bugreporting_debuglog_upload_fragment), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: DebugLogUploadViewModel by cwaViewModels { viewModelFactory } + private val binding: BugreportingDebuglogUploadFragmentBinding by viewBindingLazy() + private lateinit var uploadDialog: LogUploadBlockingDialog + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + uploadDialog = LogUploadBlockingDialog(requireContext()) + + binding.apply { + uploadAction.setOnClickListener { + vm.onUploadLog() + } + + debugLogSharePrivacyInformation.setOnClickListener { + vm.onPrivacyButtonPress() + } + + toolbar.setNavigationOnClickListener { popBackStack() } + } + + vm.routeToScreen.observe2(this) { + when (it) { + null -> popBackStack() + else -> doNavigate(it) + } + } + + vm.errorEvent.observe2(this) { + AlertDialog.Builder(requireContext()).apply { + val errorForHumans = it.tryHumanReadableError(requireContext()) + setTitle(errorForHumans.title ?: getString(R.string.errors_generic_headline)) + setMessage(errorForHumans.description) + }.show() + } + + vm.uploadInProgress.observe2(this) { uploadDialog.setState(it) } + vm.uploadSuccess.observe2(this) { + Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() + } + } + + override fun onResume() { + super.onResume() + binding.contentContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..a92282daf95df598dab8991ec1df0f46d91165c6 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadFragmentModule.kt @@ -0,0 +1,20 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload + +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector +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 DebugLogUploadFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(DebugLogUploadViewModel::class) + abstract fun debugLogViewModel(factory: DebugLogUploadViewModel.Factory): CWAViewModelFactory<out CWAViewModel> + + @ContributesAndroidInjector + abstract fun debugLogFragment(): DebugLogUploadFragment +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..1f058f22ae5daa60cc0edaf43782e53264a10944 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/DebugLogUploadViewModel.kt @@ -0,0 +1,41 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload + +import androidx.lifecycle.MutableLiveData +import androidx.navigation.NavDirections +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.bugreporting.debuglog.upload.SnapshotUploader +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.SimpleCWAViewModelFactory + +class DebugLogUploadViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider, + private val snapshotUploader: SnapshotUploader +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + val routeToScreen: SingleLiveEvent<NavDirections?> = SingleLiveEvent() + val uploadInProgress = MutableLiveData(false) + val errorEvent = SingleLiveEvent<Throwable>() + val uploadSuccess = SingleLiveEvent<String>() + + fun onUploadLog() = launch { + uploadInProgress.postValue(true) + try { + snapshotUploader.uploadSnapshot() + uploadSuccess.postValue("\uD83D\uDC4D") + routeToScreen.postValue(null) + } catch (e: Throwable) { + errorEvent.postValue(e) + } finally { + uploadInProgress.postValue(false) + } + } + + fun onPrivacyButtonPress() { + routeToScreen.postValue(DebugLogUploadFragmentDirections.actionDebugLogUploadFragmentToDebugLogLegalFragment()) + } + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<DebugLogUploadViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/LogUploadBlockingDialog.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/LogUploadBlockingDialog.kt new file mode 100644 index 0000000000000000000000000000000000000000..07c7289b695d34fc2f25ba4bb98b7bf14544ec9f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/LogUploadBlockingDialog.kt @@ -0,0 +1,23 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import de.rki.coronawarnapp.R + +class LogUploadBlockingDialog(val context: Context) { + + private val dialog by lazy { + AlertDialog.Builder(context).apply { + setCancelable(false) + setView(R.layout.bugreporting_debuglog_upload_dialog) + }.create() + } + + fun setState(show: Boolean) { + if (show && !dialog.isShowing) { + dialog.show() + } else if (!show && dialog.isShowing) { + dialog.dismiss() + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/HistoryItemAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/HistoryItemAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..969b307edf350297b18bfc3090774b8b5b72f5c3 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/HistoryItemAdapter.kt @@ -0,0 +1,66 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.databinding.BugreportingUploadHistoryItemBinding +import de.rki.coronawarnapp.ui.lists.BaseAdapter +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone +import de.rki.coronawarnapp.util.lists.BindableVH +import org.joda.time.format.DateTimeFormat +import timber.log.Timber + +class HistoryItemAdapter : BaseAdapter<HistoryItemAdapter.CachedKeyViewHolder>() { + + val data = mutableListOf<LogUpload>() + + override fun getItemCount(): Int = data.size + + override fun onCreateBaseVH(parent: ViewGroup, viewType: Int): CachedKeyViewHolder = CachedKeyViewHolder(parent) + + override fun onBindBaseVH(holder: CachedKeyViewHolder, position: Int, payloads: MutableList<Any>) { + data[position].let { + holder.bind(it) + } + } + + class CachedKeyViewHolder( + val parent: ViewGroup + ) : BaseAdapter.VH(R.layout.bugreporting_upload_history_item, parent), + BindableVH<LogUpload, BugreportingUploadHistoryItemBinding> { + + override val viewBinding = lazy { BugreportingUploadHistoryItemBinding.bind(itemView) } + + override val onBindData: BugreportingUploadHistoryItemBinding.( + item: LogUpload, + payloads: List<Any> + ) -> Unit = { item, _ -> + title.text = FORMATTER.print(item.uploadedAt.toUserTimeZone()) + description.text = "ID ${item.id}" + itemView.setOnClickListener { + try { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip( + ClipData.newPlainText( + context.getString(R.string.debugging_debuglog_share_log_title), + """ + ${context.getString(R.string.debugging_debuglog_share_log_title)} + ${title.text} + ${description.text} + """.trimIndent() + ) + ) + } catch (e: Throwable) { + Timber.e(e, "Failed to copy ID to clipboard.") + } + } + } + } + + companion object { + private val FORMATTER = DateTimeFormat.forPattern("yyyy.MM.dd - HH:mm:ss") + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..a608c49a4547607b6d9e6b43249bcaf42429444e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryFragment.kt @@ -0,0 +1,43 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.DividerItemDecoration +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.util.clearAndAddAll +import de.rki.coronawarnapp.databinding.BugreportingUploadHistoryFragmentBinding +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.cwaViewModels +import javax.inject.Inject + +class LogUploadHistoryFragment : Fragment(R.layout.bugreporting_upload_history_fragment), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: LogUploadHistoryViewModel by cwaViewModels { viewModelFactory } + private val binding: BugreportingUploadHistoryFragmentBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val historyAdapter = HistoryItemAdapter() + + binding.uploadHistory.apply { + addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) + adapter = historyAdapter + } + + vm.logUploads.observe2(this) { + historyAdapter.apply { + data.clearAndAddAll(it) + notifyDataSetChanged() + } + } + + binding.toolbar.setNavigationOnClickListener { popBackStack() } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..7ecbd6b53ece2cf03d47022633f75a3e1a21fb70 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryModule.kt @@ -0,0 +1,23 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history + +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector +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 LogUploadHistoryModule { + + @Binds + @IntoMap + @CWAViewModelKey(LogUploadHistoryViewModel::class) + abstract fun uploadHistoryViewModel( + factory: LogUploadHistoryViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> + + @ContributesAndroidInjector + abstract fun uploadHistory(): LogUploadHistoryFragment +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..93d6191cb3d580b873b522771439ed0b840b41c4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/upload/history/LogUploadHistoryViewModel.kt @@ -0,0 +1,25 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history + +import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.bugreporting.BugReportingSettings +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.map + +class LogUploadHistoryViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider, + bugReportingSettings: BugReportingSettings +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + val logUploads: LiveData<List<LogUpload>> = bugReportingSettings.uploadHistory.flow + .map { history -> history.logs.sortedByDescending { it.uploadedAt } } + .asLiveData(context = dispatcherProvider.Default) + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<LogUploadHistoryViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploader.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploader.kt new file mode 100644 index 0000000000000000000000000000000000000000..b2c1dac5df52dc5dac5f970de091243340d8954a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploader.kt @@ -0,0 +1,58 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload + +import de.rki.coronawarnapp.bugreporting.BugReportingSettings +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadServer +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadAuthorizer +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SnapshotUploader @Inject constructor( + private val snapshotter: LogSnapshotter, + private val uploadServer: LogUploadServer, + private val authorizer: LogUploadAuthorizer, + private val bugReportingSettings: BugReportingSettings +) { + + suspend fun uploadSnapshot(): LogUpload { + Timber.tag(TAG).v("uploadSnapshot()") + + val authorizedOtp = authorizer.getAuthorizedOTP().also { + Timber.tag(TAG).d("Authorized OTP obtained: %s", it) + } + + val snapshot = snapshotter.snapshot().also { + Timber.tag(TAG).d("Snapshot created: %s", it) + } + + val logUpload = try { + uploadServer.uploadLog(authorizedOtp, snapshot).also { + Timber.tag(TAG).d("Log uploaded: %s", it) + } + } finally { + snapshot.delete().also { + Timber.tag(TAG).d("Snapshot was deleted after upload: %b", it) + } + } + + bugReportingSettings.uploadHistory.update { oldHistory -> + val newLogs = oldHistory.logs.toMutableList() + if (newLogs.size >= 10) { + newLogs.removeFirst().also { + Timber.tag(TAG).d("Removed oldest entry from history: %s", it) + } + } + newLogs.add(logUpload) + oldHistory.copy(logs = newLogs) + } + + return logUpload + } + + companion object { + private const val TAG = "SnapshotUploader" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/LogUpload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/LogUpload.kt new file mode 100644 index 0000000000000000000000000000000000000000..e5edc8b30101ce568dd0980d554355356e2510cf --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/LogUpload.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.history + +import org.joda.time.Instant + +data class LogUpload( + val id: String, + val uploadedAt: Instant +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/UploadHistory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/UploadHistory.kt new file mode 100644 index 0000000000000000000000000000000000000000..fa8dc33641b885984586503f6ebfe2690c77b4a9 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/history/UploadHistory.kt @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.history + +data class UploadHistory( + val logs: List<LogUpload> = emptyList() +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..12223408be7053f0b72f1b934383610b097d892e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiV1.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server + +import com.google.gson.annotations.SerializedName +import okhttp3.MultipartBody +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +interface LogUploadApiV1 { + + @Multipart + @POST("/api/logs") + suspend fun uploadLog( + @Header("cwa-otp") otp: String, + @Part logZip: MultipartBody.Part + ): UploadResponse + + data class UploadResponse( + @SerializedName("id") val id: String, + @SerializedName("hash") val hash: String?, + @SerializedName("errorCode") val errorCode: String? + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServer.kt new file mode 100644 index 0000000000000000000000000000000000000000..4cc673a1dca2acf4a692fb2ab56c7fedaaf9a7fa --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServer.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server + +import dagger.Lazy +import dagger.Reusable +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadOtp +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.files.determineMimeType +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class LogUploadServer @Inject constructor( + private val uploadApiProvider: Lazy<LogUploadApiV1>, + private val timeStamper: TimeStamper +) { + + private val uploadApi: LogUploadApiV1 + get() = uploadApiProvider.get() + + suspend fun uploadLog(uploadOtp: LogUploadOtp, snapshot: LogSnapshotter.Snapshot): LogUpload { + val response = uploadApi.uploadLog( + otp = uploadOtp.otp, + logZip = MultipartBody.Part.createFormData( + name = "file", + filename = snapshot.path.name, + body = snapshot.path.asRequestBody(snapshot.path.determineMimeType().toMediaType()) + ) + ) + Timber.tag(TAG).d("Upload response: %s", response) + + return LogUpload(id = response.id, uploadedAt = timeStamper.nowUTC) + } + + companion object { + private const val TAG = "LogUploadServer" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt new file mode 100644 index 0000000000000000000000000000000000000000..ef9ea5988795eccfa701d6bd790727d7c3ab69f7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiV1.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth + +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtpRequestAndroid +import retrofit2.http.Body +import retrofit2.http.POST + +interface LogUploadAuthApiV1 { + + data class AuthResponse( + @SerializedName("expirationDate") val expirationDate: String + ) + + data class AuthError( + @SerializedName("errorCode") val errorCode: String? + ) + + @POST("version/v1/android/log") + suspend fun authOTP( + @Body requestBody: ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid + ): AuthResponse +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizer.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a0a77ce39de5095615995934125dbd28da67e2c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizer.kt @@ -0,0 +1,63 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth + +import dagger.Lazy +import dagger.Reusable +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation +import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtp +import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtpRequestAndroid +import kotlinx.coroutines.flow.first +import org.joda.time.Instant +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject + +@Reusable +class LogUploadAuthorizer @Inject constructor( + private val authApiProvider: Lazy<LogUploadAuthApiV1>, + private val deviceAttestation: DeviceAttestation, + private val configProvider: AppConfigProvider +) { + + private val authApi: LogUploadAuthApiV1 + get() = authApiProvider.get() + + suspend fun getAuthorizedOTP(otp: UUID = UUID.randomUUID()): LogUploadOtp { + Timber.tag(TAG).d("getAuthorizedOTP() trying to authorize %s", otp) + + val elsOtp = ElsOtp.ELSOneTimePassword.newBuilder().apply { + setOtp(otp.toString()) + }.build() + + val appConfig = configProvider.currentConfig.first() + + val attestationRequest = object : DeviceAttestation.Request { + override val configData: ConfigData = appConfig + override val checkDeviceTime: Boolean = false + override val scenarioPayload: ByteArray = elsOtp.toByteArray() + } + val attestionResult = deviceAttestation.attest(attestationRequest) + Timber.tag(TAG).d("Attestation passed, requesting authorization from server for %s", attestionResult) + + attestionResult.requirePass(appConfig.logUpload.safetyNetRequirements) + + val elsRequest = ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid.newBuilder().apply { + authentication = attestionResult.accessControlProtoBuf + payload = elsOtp + }.build() + + // TODO This was written without backend available, retest. + val authResponse = authApi.authOTP(elsRequest).also { + Timber.tag(TAG).v("Auth response received: %s", it) + } + + return LogUploadOtp(otp = otp.toString(), expirationDate = Instant.parse(authResponse.expirationDate)).also { + Timber.tag(TAG).d("%s created", it) + } + } + + companion object { + private const val TAG = "LogUploadOtpServer" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadOtp.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadOtp.kt new file mode 100644 index 0000000000000000000000000000000000000000..8c9532da72554a2533d82b6a3aa78187a5087b69 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadOtp.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth + +import org.joda.time.Instant + +data class LogUploadOtp( + val otp: String, + val expirationDate: Instant +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt index a35fb01fba3801ff429094d5c863dc25478f1dab..dcf91e1f6f93e69204fb752aacc52d38168133aa 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/AttestationContainer.kt @@ -4,6 +4,7 @@ import de.rki.coronawarnapp.appconfig.SafetyNetRequirements import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException.Type import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid import okio.ByteString.Companion.toByteString +import timber.log.Timber internal data class AttestationContainer( private val ourSalt: ByteArray, @@ -16,6 +17,8 @@ internal data class AttestationContainer( }.build() override fun requirePass(reqs: SafetyNetRequirements) { + Timber.v("requirePass(%s)", reqs) + if (reqs.requireBasicIntegrity && !report.basicIntegrity) { throw SafetyNetException( Type.BASIC_INTEGRITY_REQUIRED, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt index e29f43cc5c8ea7baca9a7cd4b3f4a409ce4e2b83..3e39b6d5075d56c1e8f146d99047c01b90accc54 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNet.kt @@ -51,26 +51,11 @@ class CWASafetyNet @Inject constructor( throw SafetyNetException(Type.PLAY_SERVICES_VERSION_MISMATCH, "Google Play Services too old.") } - appConfigProvider.getAppConfig().apply { - if (deviceTimeState == ConfigData.DeviceTimeState.ASSUMED_CORRECT) { - throw SafetyNetException(Type.DEVICE_TIME_UNVERIFIED, "Device time is unverified") - } - if (deviceTimeState == ConfigData.DeviceTimeState.INCORRECT) { - throw SafetyNetException(Type.DEVICE_TIME_INCORRECT, "Device time is incorrect") - } - } - - val skip24hCheck = CWADebug.isDeviceForTestersBuild && testSettings.skipSafetyNetTimeCheck.value - val nowUTC = timeStamper.nowUTC - val firstReliableTimeStamp = cwaSettings.firstReliableDeviceTime - val timeSinceOnboarding = Duration(firstReliableTimeStamp, nowUTC) - Timber.d("firstReliableTimeStamp=%s, now=%s", firstReliableTimeStamp, nowUTC) - Timber.d("skip24hCheck=%b, timeSinceOnboarding=%dh", skip24hCheck, timeSinceOnboarding.standardHours) - - if (firstReliableTimeStamp == Instant.EPOCH) { - throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "No first reliable timestamp available") - } else if (!skip24hCheck && timeSinceOnboarding < Duration.standardHours(24)) { - throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "Time since first reliable timestamp <24h") + if (request.checkDeviceTime) { + Timber.tag(TAG).d("Checking device time.") + requireCorrectDeviceTime(request.configData) + } else { + Timber.tag(TAG).d("Device time check not required.") } val salt = generateSalt() @@ -105,6 +90,32 @@ class CWASafetyNet @Inject constructor( return AttestationContainer(salt, report) } + private suspend fun requireCorrectDeviceTime(suppliedConfig: ConfigData?) { + val configData = suppliedConfig ?: appConfigProvider.getAppConfig() + + configData.apply { + if (deviceTimeState == ConfigData.DeviceTimeState.ASSUMED_CORRECT) { + throw SafetyNetException(Type.DEVICE_TIME_UNVERIFIED, "Device time is unverified") + } + if (deviceTimeState == ConfigData.DeviceTimeState.INCORRECT) { + throw SafetyNetException(Type.DEVICE_TIME_INCORRECT, "Device time is incorrect") + } + } + + val skip24hCheck = CWADebug.isDeviceForTestersBuild && testSettings.skipSafetyNetTimeCheck.value + val nowUTC = timeStamper.nowUTC + val firstReliableTimeStamp = cwaSettings.firstReliableDeviceTime + val timeSinceOnboarding = Duration(firstReliableTimeStamp, nowUTC) + Timber.d("firstReliableTimeStamp=%s, now=%s", firstReliableTimeStamp, nowUTC) + Timber.d("skip24hCheck=%b, timeSinceOnboarding=%dh", skip24hCheck, timeSinceOnboarding.standardHours) + + if (firstReliableTimeStamp == Instant.EPOCH) { + throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "No first reliable timestamp available") + } else if (!skip24hCheck && timeSinceOnboarding < Duration.standardHours(24)) { + throw SafetyNetException(Type.TIME_SINCE_ONBOARDING_UNVERIFIED, "Time since first reliable timestamp <24h") + } + } + companion object { private const val TAG = "CWASafetyNet" } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt index 6222c05f6c04605ec0e68b27c5a570cb8bf96be4..945f7ef47deb26608595a11193d3c3d66d96c3ad 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestation.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.datadonation.safetynet +import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.SafetyNetRequirements import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid @@ -13,6 +14,13 @@ interface DeviceAttestation { suspend fun attest(request: Request): Result interface Request { + + val configData: ConfigData? + get() = null + + val checkDeviceTime: Boolean + get() = true + /** * e.g. for EventSurvey, a UUID, base64 encoded. */ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt index b83bd416320695e997e6f5e36bfda2a3888614ab..b99710e6d4a6bd5a07a5bb7d08931aecf4594746 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentModule.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.environment import dagger.Module +import de.rki.coronawarnapp.environment.bugreporting.BugReportingServerModule import de.rki.coronawarnapp.environment.datadonation.DataDonationCDNModule import de.rki.coronawarnapp.environment.download.DownloadCDNModule import de.rki.coronawarnapp.environment.submission.SubmissionCDNModule @@ -11,7 +12,8 @@ import de.rki.coronawarnapp.environment.verification.VerificationCDNModule DownloadCDNModule::class, SubmissionCDNModule::class, VerificationCDNModule::class, - DataDonationCDNModule::class + DataDonationCDNModule::class, + BugReportingServerModule::class ] ) class EnvironmentModule diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt index 76bfaa4a444f2fdf833d31b60efe3f1de711923a..1476022658f27fc6711345379d875e055c8098d3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt @@ -7,6 +7,7 @@ import com.google.gson.JsonObject import com.google.gson.JsonPrimitive import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DATA_DONATION import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.DOWNLOAD +import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.LOG_UPLOAD import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.SAFETYNET_API_KEY import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.SUBMISSION import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.USE_EUR_KEY_PKGS @@ -33,6 +34,7 @@ class EnvironmentSetup @Inject constructor( DOWNLOAD("DOWNLOAD_CDN_URL"), VERIFICATION_KEYS("PUB_KEYS_SIGNATURE_VERIFICATION"), DATA_DONATION("DATA_DONATION_CDN_URL"), + LOG_UPLOAD("LOG_UPLOAD_SERVER_URL"), SAFETYNET_API_KEY("SAFETYNET_API_KEY") } @@ -123,6 +125,9 @@ class EnvironmentSetup @Inject constructor( val safetyNetApiKey: String get() = getEnvironmentValue(SAFETYNET_API_KEY).asString + val logUploadServerUrl: String + get() = getEnvironmentValue(LOG_UPLOAD).asString + companion object { private const val PKEY_CURRENT_ENVINROMENT = "environment.current" } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..652903409aa6e11fc50990477e90b5a77bfa4d28 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModule.kt @@ -0,0 +1,28 @@ +package de.rki.coronawarnapp.environment.bugreporting + +import dagger.Module +import dagger.Provides +import dagger.Reusable +import de.rki.coronawarnapp.environment.BaseEnvironmentModule +import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.http.HttpClientDefault +import okhttp3.OkHttpClient +import javax.inject.Singleton + +@Module +class BugReportingServerModule : BaseEnvironmentModule() { + + @Reusable + @LogUploadHttpClient + @Provides + fun cdnHttpClient(@HttpClientDefault defaultHttpClient: OkHttpClient): OkHttpClient = + defaultHttpClient.newBuilder().build() + + @Singleton + @LogUploadServerUrl + @Provides + fun provideBugReportingServerUrl(environment: EnvironmentSetup): String { + val url = environment.logUploadServerUrl + return requireValidUrl(url) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadHttpClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadHttpClient.kt new file mode 100644 index 0000000000000000000000000000000000000000..039fd9ef69478b3a464e3ff7061281a72eebb7a6 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadHttpClient.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.environment.bugreporting + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class LogUploadHttpClient diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadServerUrl.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadServerUrl.kt new file mode 100644 index 0000000000000000000000000000000000000000..932c084aa72c7476b86d585d6894c25e87f198c4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/bugreporting/LogUploadServerUrl.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.environment.bugreporting + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class LogUploadServerUrl diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt index e52ddb99205c428db477710548307ba7caf0b4f2..d80f14654c58ce92655b071703e815822d1a9d9b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt @@ -5,11 +5,21 @@ import dagger.Module import dagger.android.ContributesAndroidInjector import dagger.multibindings.IntoMap import de.rki.coronawarnapp.bugreporting.debuglog.ui.DebugLogFragmentModule +import de.rki.coronawarnapp.bugreporting.debuglog.ui.legal.DebugLogLegalModule +import de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.DebugLogUploadFragmentModule +import de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history.LogUploadHistoryModule import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey -@Module(includes = [DebugLogFragmentModule::class]) +@Module( + includes = [ + DebugLogFragmentModule::class, + LogUploadHistoryModule::class, + DebugLogLegalModule::class, + DebugLogUploadFragmentModule::class + ] +) abstract class InformationFragmentModule { @Binds @IntoMap diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt index c794c54b9521708e3cf4e11237e7f81d65ef935f..d08cc5a6c3dd54ec1e878886eeb09813c8d87455 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.util +import android.annotation.SuppressLint import android.app.Application import android.os.Build import androidx.annotation.VisibleForTesting @@ -11,6 +12,9 @@ import timber.log.Timber object CWADebug { + @SuppressLint("StaticFieldLeak") + lateinit var debugLogger: DebugLogger + fun init(application: Application) { if (isDebugBuildOrMode) System.setProperty("kotlinx.coroutines.debug", "on") @@ -20,15 +24,23 @@ object CWADebug { setupExceptionHandler() - DebugLogger.init(application) + debugLogger = DebugLogger(context = application).also { + it.init() + } logDeviceInfos() } fun initAfterInjection(component: ApplicationComponent) { - DebugLogger.setInjectionIsReady(component) + debugLogger.setInjectionIsReady(component) } + val isLogging: Boolean + get() { + if (!this::debugLogger.isInitialized) return false + return debugLogger.isLogging.value + } + val isDebugBuildOrMode: Boolean get() = BuildConfig.DEBUG || buildFlavor == BuildFlavor.DEVICE_FOR_TESTERS diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt index 01c1799d73fbd6b47e66c37efba2a6f4f71b093f..d78661d2b7a2a4ac2d2152c02ade522a4c19440c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.util import android.annotation.SuppressLint import android.content.Context import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.bugreporting.BugReportingSettings import de.rki.coronawarnapp.contactdiary.storage.ContactDiaryPreferences import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.datadonation.analytics.Analytics @@ -43,7 +44,8 @@ class DataReset @Inject constructor( private val statisticsProvider: StatisticsProvider, private val surveySettings: SurveySettings, private val analyticsSettings: AnalyticsSettings, - private val analytics: Analytics + private val analytics: Analytics, + private val bugReportingSettings: BugReportingSettings ) { private val mutex = Mutex() @@ -80,6 +82,8 @@ class DataReset @Inject constructor( statisticsProvider.clear() + bugReportingSettings.clear() + Timber.w("CWA LOCAL DATA DELETION COMPLETED.") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt index 05090cb511b29a6a47f37ce59ce1af4b6cd49a7b..0a8833e9d86b937a49eab52c690c9e6a276cad6a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt @@ -13,6 +13,7 @@ import org.joda.time.format.DateTimeFormat import timber.log.Timber import java.math.RoundingMode import java.util.Date +import java.util.TimeZone import java.util.concurrent.TimeUnit object TimeAndDateExtensions { @@ -87,4 +88,6 @@ object TimeAndDateExtensions { fun Instant.toLocalTime(): LocalTime = this.toDateTime(DateTimeZone.UTC).toLocalTime() val Instant.seconds get() = TimeUnit.MILLISECONDS.toSeconds(millis) + + fun Instant.toUserTimeZone() = this.toDateTime(DateTimeZone.forTimeZone(TimeZone.getDefault())) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/UncaughtExceptionLogger.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/UncaughtExceptionLogger.kt index 2b7abc116354400d187f0d10533c82848a2d4fc2..9211c208ac46fab9ed713599db11e38789e7213c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/UncaughtExceptionLogger.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/UncaughtExceptionLogger.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.util.debug -import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger +import de.rki.coronawarnapp.util.CWADebug import timber.log.Timber class UncaughtExceptionLogger( @@ -13,7 +13,7 @@ class UncaughtExceptionLogger( override fun uncaughtException(thread: Thread, error: Throwable) { Timber.tag(thread.name).e(error, "Uncaught exception!") - if (DebugLogger.isLogging) { + if (CWADebug.isLogging) { // Make sure this crash is written before killing the app. Thread.sleep(1500) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt index e70f708ad45a87382060f765cff9ee3bb4d31560..d548e33cdd1043f678b41558e4d97da5c38a2a43 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt @@ -4,6 +4,7 @@ import android.app.ActivityManager import android.app.Application import android.app.NotificationManager import android.bluetooth.BluetoothAdapter +import android.content.ContentResolver import android.content.Context import android.content.SharedPreferences import androidx.core.app.NotificationManagerCompat @@ -76,4 +77,7 @@ class AndroidModule { @Provides @Singleton fun safetyNet(@AppContext context: Context): SafetyNetClient = SafetyNet.getClient(context) + + @Provides + fun contentResolver(@AppContext context: Context): ContentResolver = context.contentResolver } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..8bf768b7824255e8be19e89883b8ae0b2172823d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileExtensions.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.util.files + +import java.io.File + +fun File.determineMimeType(): String = when { + name.endsWith(".zip") -> "application/zip" + else -> throw UnsupportedOperationException("Unsupported MIME type: $path") +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/sharing/FileSharing.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileSharing.kt similarity index 84% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/sharing/FileSharing.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileSharing.kt index 59dff05d68a47b0d753c7edfafbc7c5f01a9e156..4303506d6e561362417e660cb3a0e2833f31af53 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/sharing/FileSharing.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/FileSharing.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.util.sharing +package de.rki.coronawarnapp.util.files import android.app.Activity import android.content.Context @@ -32,7 +32,7 @@ class FileSharing @Inject constructor( ): ShareIntentProvider = object : ShareIntentProvider { override fun get(activity: Activity): Intent { val builder = ShareCompat.IntentBuilder.from(activity).apply { - setType(determineMimeType(path)) + setType(path.determineMimeType()) setStream(getFileUri(path)) setSubject(title) chooserTitle?.let { setChooserTitle(it) } @@ -54,11 +54,6 @@ class FileSharing @Inject constructor( fun get(activity: Activity): Intent } - private fun determineMimeType(path: File): String = when { - path.name.endsWith(".zip") -> "application/zip" - else -> throw UnsupportedOperationException("Unsupported MIME type: $path") - } - companion object { private const val AUTHORITY = "${BuildConfig.APPLICATION_ID}.fileProvider" private const val TAG = "FileSharing" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/compression/Zipper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/Zipper.kt similarity index 96% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/compression/Zipper.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/Zipper.kt index 2c7924c2b5e2a462303ef3f74b32452e090e466a..49e5fd32ef870263fb573d4babe0e2b87657abb7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/compression/Zipper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/files/Zipper.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.util.compression +package de.rki.coronawarnapp.util.files import timber.log.Timber import java.io.File diff --git a/Corona-Warn-App/src/main/res/color/button_alert_red.xml b/Corona-Warn-App/src/main/res/color/button_alert_red.xml new file mode 100644 index 0000000000000000000000000000000000000000..061c98a5f2089e244665acb2c6e7852b55bd42d8 --- /dev/null +++ b/Corona-Warn-App/src/main/res/color/button_alert_red.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@color/colorAccentTintButton" android:state_activated="false" /> <!-- default --> + <item android:color="@color/colorSemanticHighRiskPressed" /> <!-- enabled --> +</selector> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/ic_debug_log_indicator_activated.xml b/Corona-Warn-App/src/main/res/drawable/ic_debug_log_indicator_activated.xml new file mode 100644 index 0000000000000000000000000000000000000000..96147bb65c461b5acda1c6f1c29d6d098f85c841 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_debug_log_indicator_activated.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32dp" + android:height="32dp" + android:viewportWidth="32" + android:viewportHeight="32"> + <path + android:pathData="M16.1494,31.9512C24.6602,31.9512 31.6621,24.9492 31.6621,16.4385C31.6621,7.9131 24.6602,0.9111 16.1348,0.9111C7.6094,0.9111 0.6221,7.9131 0.6221,16.4385C0.6221,24.9492 7.624,31.9512 16.1494,31.9512ZM16.1494,22.5029C12.7656,22.5029 10.041,19.7783 10.041,16.3945C10.041,13.04 12.7656,10.3154 16.1494,10.3154C19.5186,10.3154 22.2432,13.04 22.2432,16.3945C22.2432,19.7783 19.5186,22.5029 16.1494,22.5029Z" + android:fillColor="#BF0F2D"/> +</vector> diff --git a/Corona-Warn-App/src/main/res/drawable/ic_debug_log_indicator_deactivated.xml b/Corona-Warn-App/src/main/res/drawable/ic_debug_log_indicator_deactivated.xml new file mode 100644 index 0000000000000000000000000000000000000000..733482b1e51d00e818e624604f0c8996b4bd0e09 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_debug_log_indicator_deactivated.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32dp" + android:height="32dp" + android:viewportWidth="32" + android:viewportHeight="32"> + <path + android:pathData="M16.1494,31.9512C24.6602,31.9512 31.6621,24.9492 31.6621,16.4385C31.6621,7.9131 24.6602,0.9111 16.1348,0.9111C7.6094,0.9111 0.6221,7.9131 0.6221,16.4385C0.6221,24.9492 7.624,31.9512 16.1494,31.9512ZM16.1494,22.5029C12.7656,22.5029 10.041,19.7783 10.041,16.3945C10.041,13.04 12.7656,10.3154 16.1494,10.3154C19.5186,10.3154 22.2432,13.04 22.2432,16.3945C22.2432,19.7783 19.5186,22.5029 16.1494,22.5029Z" + android:fillColor="#D6D6D9"/> +</vector> 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 c33bf0eb51c4e7592e3811508f95817d162f132f..dfc9429fea8f718c297e10d12afeae2dbed68eb9 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 @@ -1,114 +1,241 @@ <?xml version="1.0" encoding="utf-8"?> - -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/content_container" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_height="match_parent" + android:contentDescription="@string/contact_diary_title" + android:focusable="true"> - <Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" - style="@style/CWAToolbar" + style="@style/CWAToolbar.BackArrow.Transparent" android:layout_width="match_parent" android:layout_height="wrap_content" - android:navigationIcon="@drawable/ic_back" - android:title="@string/debugging_debuglog_title" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:title="@string/debugging_debuglog_title" /> <ScrollView + android:id="@+id/scrollview" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="0dp" + android:layout_marginBottom="12dp" + android:clipToPadding="false" + android:paddingBottom="32dp" + app:layout_constraintBottom_toTopOf="@id/log_control_container" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> - <LinearLayout + <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_height="0dp"> + <TextView - android:id="@+id/explanation" + android:id="@+id/explanation_section_one" style="@style/body1" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_margin="@dimen/spacing_normal" - android:text="@string/debugging_debuglog_intro_explanation" /> + android:layout_margin="24dp" + android:padding="16dp" + android:text="@string/debugging_debuglog_intro_explanation_section_one" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> <TextView - android:id="@+id/sensitive_information" + android:id="@+id/explanation_section_two" style="@style/body1" - android:layout_width="match_parent" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_margin="24dp" + android:text="@string/debugging_debuglog_intro_explanation_section_two" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/explanation_section_one" /> + + <include + android:id="@+id/debug_log_privacy_card" + layout="@layout/include_debuglog_privacy_card" + android:layout_width="0dp" android:layout_height="wrap_content" - android:background="@color/cwaGrayHighlight" - android:padding="@dimen/spacing_normal" - android:text="@string/debugging_debuglog_intro_warning" - android:textColor="@color/colorStableLight" /> + android:layout_marginHorizontal="@dimen/guideline_card" + android:layout_marginTop="16dp" + android:focusable="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/explanation_section_two" /> <androidx.constraintlayout.widget.ConstraintLayout - style="@style/cardTracing" + android:id="@+id/debug_log_history_container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacing_small" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/spacing_small" - android:layout_marginBottom="@dimen/spacing_tiny"> - - <ProgressBar - android:id="@+id/debuglog_activity_indicator" - android:layout_width="36dp" - android:layout_height="36dp" - android:visibility="gone" - app:layout_constraintBottom_toBottomOf="@+id/debuglog_status_secondary" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/debuglog_status_primary" - tools:visibility="visible" /> + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/debug_log_privacy_card"> <TextView - android:id="@+id/debuglog_status_primary" + android:id="@+id/log_history_title" style="@style/body1" - android:layout_width="0dp" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" + android:layout_margin="24dp" + android:text="@string/debugging_debuglog_id_history_title" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/debuglog_activity_indicator" - app:layout_constraintTop_toTopOf="parent" - app:layout_goneMarginStart="0dp" - tools:text="@string/debugging_debuglog_status_not_recording" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> <TextView - android:id="@+id/debuglog_status_secondary" + android:id="@+id/log_history_body" style="@style/TextAppearance.AppCompat.Caption" - android:layout_width="0dp" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="4dp" + android:text="@string/debugging_debuglog_id_history_body" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/debuglog_activity_indicator" - app:layout_constraintTop_toBottomOf="@id/debuglog_status_primary" - app:layout_goneMarginStart="0dp" - tools:text="@string/debugging_debuglog_status_additional_infos" /> - - <Button - android:id="@+id/share_recording" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:layout_marginEnd="8dp" - android:enabled="false" - android:text="@string/debugging_debuglog_action_share_log" - app:layout_constraintEnd_toStartOf="@+id/toggle_recording" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/debuglog_status_secondary" /> - <Button - android:id="@+id/toggle_recording" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="16dp" - android:text="@string/debugging_debuglog_action_start_recording" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@id/share_recording" - app:layout_constraintTop_toBottomOf="@id/debuglog_status_secondary" /> - + app:layout_constraintTop_toBottomOf="@id/log_history_title" /> </androidx.constraintlayout.widget.ConstraintLayout> - </LinearLayout> + + <View + android:id="@+id/debug_log_first_divider" + android:layout_width="0dp" + android:layout_height="@dimen/card_divider" + android:layout_marginHorizontal="24dp" + android:layout_marginTop="24dp" + android:background="?android:attr/listDivider" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/debug_log_history_container" /> + + <TextView + android:id="@+id/debug_log_privacy_information" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:background="?selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:paddingVertical="8dp" + android:text="@string/contact_diary_onboarding_legal_information" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/debug_log_first_divider" + tools:text="@string/contact_diary_onboarding_legal_information" /> + + <View + android:id="@+id/debug_log_second_divider" + android:layout_width="0dp" + android:layout_height="@dimen/card_divider" + android:layout_marginHorizontal="24dp" + android:layout_marginBottom="24dp" + android:background="?android:attr/listDivider" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/debug_log_privacy_information" /> + + </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> -</LinearLayout> \ No newline at end of file + + <LinearLayout + android:id="@+id/log_control_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/colorSurface1" + android:orientation="vertical" + android:paddingStart="24dp" + android:paddingTop="16dp" + android:paddingEnd="24dp" + android:paddingBottom="16dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + + <TextView + android:id="@+id/debug_log_analysis_title" + style="@style/headline5" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp" + android:accessibilityHeading="true" + android:focusable="true" + android:text="@string/debugging_debuglog_current_status_title" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/debug_log_current_status_card" + style="@style/cardTracing" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp"> + + <ImageView + android:id="@+id/debuglog_activity_indicator" + android:layout_width="36dp" + android:layout_height="36dp" + android:importantForAccessibility="no" + android:src="@drawable/ic_debug_log_indicator_deactivated" + app:layout_constraintBottom_toBottomOf="@id/debuglog_status_secondary" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/debuglog_status_primary" + tools:src="@drawable/ic_debug_log_indicator_deactivated" /> + + <TextView + android:id="@+id/debuglog_status_primary" + style="@style/body1" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/debuglog_activity_indicator" + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/debugging_debuglog_status_not_recording" /> + + <TextView + android:id="@+id/debuglog_status_secondary" + style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/debuglog_activity_indicator" + app:layout_constraintTop_toBottomOf="@id/debuglog_status_primary" + tools:text="@string/debugging_debuglog_status_additional_infos" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <android.widget.Button + android:id="@+id/toggle_send_error_log" + style="@style/buttonPrimary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp" + android:text="@string/debugging_debuglog_action_share_log" + tools:text="@string/debugging_debuglog_action_share_log" /> + + <android.widget.Button + android:id="@+id/toggle_store_log" + style="@style/buttonPrimary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="16dp" + android:text="@string/debugging_debuglog_action_local_log_store" + tools:text="@string/debugging_debuglog_action_local_log_store" /> + + <android.widget.Button + android:id="@+id/toggle_recording" + style="@style/buttonBarAlertRed" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/debugging_debuglog_action_start_recording" + tools:text="@string/debugging_debuglog_action_start_recording" /> + </LinearLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_dialog.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_dialog.xml new file mode 100644 index 0000000000000000000000000000000000000000..7a719f9e6982e7503cc6c138eb329dd4ce278869 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_dialog.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <ProgressBar + android:id="@+id/progress_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + style="@style/Widget.AppCompat.ProgressBar" + android:layout_marginTop="16dp" + android:layout_marginBottom="16dp" + android:progressTint="@color/colorAccentTintIcon" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/progress_message" + style="@style/body1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:text="@string/debugging_debuglog_share_log_title" + app:layout_constraintBottom_toBottomOf="@+id/progress_indicator" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toEndOf="@+id/progress_indicator" + app:layout_constraintTop_toTopOf="@+id/progress_indicator" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..236c03b46084c06bf16e5abe51d1809b155e74c8 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_upload_fragment.xml @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:bind="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/content_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/debugging_debuglog_share_log_title" + android:focusable="true"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar.BackArrow.Transparent" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:title="@string/debugging_debuglog_share_log_title" /> + + <ScrollView + android:id="@+id/scrollview" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginBottom="@dimen/spacing_small" + app:layout_constraintBottom_toTopOf="@id/upload_action" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/bugreporting_share_log_body_one" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debugging_debuglog_share_log_section_one" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/debugging_debuglog_share_log_section_one" /> + + <TextView + android:id="@+id/bugreporting_share_log_body_two" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debugging_debuglog_share_log_section_two" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/bugreporting_share_log_body_one" + tools:text="@string/debugging_debuglog_share_log_section_two" /> + + <include + android:id="@+id/bugreporting_share_log_privacy_card" + layout="@layout/include_debugging_debuglog_share_privacy_card" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/guideline_card" + android:layout_marginTop="@dimen/spacing_normal" + android:focusable="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/bugreporting_share_log_body_two" /> + + <View + android:id="@+id/bugreporting_share_log_first_divider" + android:layout_width="0dp" + android:layout_height="@dimen/card_divider" + android:layout_marginTop="@dimen/spacing_small" + android:background="?android:attr/listDivider" + app:layout_constraintEnd_toStartOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/bugreporting_share_log_privacy_card" /> + + <TextView + android:id="@+id/debug_log_share_privacy_information" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:background="?selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:paddingVertical="@dimen/spacing_tiny" + android:text="@string/debugging_debuglog_share_log_privacy_information" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/bugreporting_share_log_first_divider" + tools:text="@string/debugging_debuglog_share_log_privacy_information" /> + + <View + android:id="@+id/bugreporting_share_log_second_divider" + android:layout_width="0dp" + android:layout_height="@dimen/card_divider" + android:background="?android:attr/listDivider" + app:layout_constraintBottom_toBottomOf="parent" + bind:layout_constraintEnd_toStartOf="@id/guideline_end" + bind:layout_constraintStart_toStartOf="@id/guideline_start" + bind:layout_constraintTop_toBottomOf="@id/debug_log_share_privacy_information" /> + + <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> + + </ScrollView> + + <android.widget.Button + android:id="@+id/upload_action" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:layout_marginEnd="@dimen/spacing_normal" + android:layout_marginBottom="@dimen/spacing_small" + android:text="@string/debugging_debuglog_share_log_button" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + tools:text="@string/debugging_debuglog_share_log_button" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_legal_fragment.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_legal_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..14ac91c11ad23d8c92ee3de9a6ae2899c4600992 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/bugreporting_legal_fragment.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/content_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/contact_diary_title" + android:focusable="true"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar.Close.Transparent" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <ScrollView + android:layout_width="0dp" + android:layout_height="0dp" + android:clipToPadding="false" + android:paddingBottom="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/debuglog_legal_headline" + style="@style/headline4" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" + android:focusable="true" + android:text="@string/debugging_debuglog_legal_dialog_title" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/debugging_debuglog_legal_dialog_title" /> + + <include + android:id="@+id/debuglog_legal_privacy_card" + layout="@layout/include_debuglog_legal_privacy_card" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/guideline_card" + android:layout_marginTop="@dimen/spacing_normal" + android:focusable="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/debuglog_legal_headline" /> + + <TextView + android:id="@+id/debuglog_legal_body_headline" + style="@style/subtitleBoldSixteen" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debugging_debuglog_legal_section_title" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/debuglog_legal_privacy_card" + tools:text="@string/debugging_debuglog_legal_section_title" /> + + <TextView + android:id="@+id/debuglog_legal_body_text" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debugging_debuglog_legal_section_body" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/debuglog_legal_body_headline" + tools:text="@string/debugging_debuglog_legal_section_body" /> + + <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="47dp" /> + + <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> + + </ScrollView> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_fragment.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..d9d7f09e9e7fdb6e27c9e7676931afa64e0721ad --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_fragment.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:navigationIcon="@drawable/ic_back" + app:title="@string/debugging_debuglog_uploadhistory_title" /> + + <androidx.core.widget.NestedScrollView + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:id="@+id/description" + style="@style/body1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="24dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="24dp" + android:layout_marginBottom="16dp" + android:text="@string/debugging_debuglog_uploadhistory_description" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/upload_history" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginStart="24dp" + android:layout_marginEnd="24dp" + android:nestedScrollingEnabled="false" + app:layoutManager="LinearLayoutManager" + tools:listitem="@layout/bugreporting_upload_history_item" /> + + </LinearLayout> + + </androidx.core.widget.NestedScrollView> +</LinearLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..f9a3c78a3a47fa9780ae2c173d4246d38d66c808 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/bugreporting_upload_history_item.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?selectableItemBackground"> + + <TextView + android:id="@+id/title" + style="@style/body1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="22.02.22 um 09:32" /> + + <TextView + android:id="@+id/description" + style="@style/TextAppearance.MaterialComponents.Caption" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="16dp" + android:textIsSelectable="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@id/title" + app:layout_constraintStart_toStartOf="@id/title" + app:layout_constraintTop_toBottomOf="@id/title" + tools:text="ID 001" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file 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 b506237672f54d2ad6991c22004fec640dd7da96..25dfed9ca1d242afaa0acc5111e66759c8d63cd1 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information.xml @@ -89,18 +89,18 @@ app:subtitle="@{@string/information_contact_title}" /> <include - android:id="@+id/information_legal" + android:id="@+id/information_debuglog" layout="@layout/include_row" android:layout_width="match_parent" android:layout_height="wrap_content" - app:subtitle="@{@string/information_legal_title}" /> + app:subtitle="@{@string/debugging_debuglog_title}" /> <include - android:id="@+id/information_debuglog" + android:id="@+id/information_legal" layout="@layout/include_row" android:layout_width="match_parent" android:layout_height="wrap_content" - app:subtitle="@{@string/debugging_debuglog_title}" /> + app:subtitle="@{@string/information_legal_title}" /> <TextView android:id="@+id/information_version" diff --git a/Corona-Warn-App/src/main/res/layout/include_debugging_debuglog_share_privacy_card.xml b/Corona-Warn-App/src/main/res/layout/include_debugging_debuglog_share_privacy_card.xml new file mode 100644 index 0000000000000000000000000000000000000000..11e4b7383169c29b5971b8d8bccfe4de21279d96 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/include_debugging_debuglog_share_privacy_card.xml @@ -0,0 +1,115 @@ +<?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/debug_log_share_privacy_card" + style="@style/GreyCard" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/spacing_normal" + app:layout_constraintTop_toTopOf="parent"> + + <TextView + android:id="@+id/debug_log_share_privacy_card_title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:accessibilityHeading="true" + android:focusable="true" + android:text="@string/debugging_debuglog_share_privacy_card_title" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/debug_log_share_privacy_card_section_one" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" + android:focusable="true" + android:text="@string/debugging_debuglog_share_privacy_card_body_1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/debug_log_share_privacy_card_title" /> + + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/debug_log_share_privacy_card_bullet_point_container_one" + 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/debug_log_share_privacy_card_section_one"> + + <include + android:id="@+id/debug_log_share_privacy_card_bulletpoint_one" + layout="@layout/include_bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + app:layout_constraintBaseline_toBaselineOf="@id/debug_log_share_privacy_card_bullet_point_text_one" /> + + <TextView + android:id="@+id/debug_log_share_privacy_card_bullet_point_text_one" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debugging_debuglog_share_privacy_card_bullet_point_1" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/debug_log_share_privacy_card_bulletpoint_one" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/debug_log_share_privacy_card_bullet_point_container_two" + 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/debug_log_share_privacy_card_bullet_point_container_one"> + + <include + android:id="@+id/debug_log_share_privacy_card_bulletpoint_two" + layout="@layout/include_bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + app:layout_constraintBaseline_toBaselineOf="@id/debug_log_share_privacy_card_bullet_point_text_two" /> + + <TextView + android:id="@+id/debug_log_share_privacy_card_bullet_point_text_two" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debugging_debuglog_share_privacy_card_bullet_point_2" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/debug_log_share_privacy_card_bulletpoint_two" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <TextView + android:id="@+id/debug_log_share_privacy_card_section_two" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" + android:focusable="true" + android:text="@string/debugging_debuglog_share_privacy_card_body_2" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/debug_log_share_privacy_card_bullet_point_container_two" /> + + </androidx.constraintlayout.widget.ConstraintLayout> +</layout> diff --git a/Corona-Warn-App/src/main/res/layout/include_debuglog_legal_privacy_card.xml b/Corona-Warn-App/src/main/res/layout/include_debuglog_legal_privacy_card.xml new file mode 100644 index 0000000000000000000000000000000000000000..03c929577cd99652eb8991f8a8c20ff3308cd8df --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/include_debuglog_legal_privacy_card.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/debug_log_legal_privacy_card" + style="@style/GreyCard" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/spacing_normal" + app:layout_constraintTop_toTopOf="parent"> + + <TextView + android:id="@+id/debug_log_legal_privacy_card_title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:accessibilityHeading="true" + android:focusable="true" + android:text="@string/debugging_debuglog_legal_privacy_card_title" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/debug_log_legal_privacy_card_first_section" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" + android:focusable="true" + android:text="@string/debugging_debuglog_legal_privacy_card_first_section" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/debug_log_legal_privacy_card_title" /> + + <TextView + android:id="@+id/debug_log_legal_privacy_card_second_section" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" + android:focusable="true" + android:text="@string/debugging_debuglog_legal_privacy_card_second_section" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/debug_log_legal_privacy_card_first_section" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/include_debuglog_privacy_card.xml b/Corona-Warn-App/src/main/res/layout/include_debuglog_privacy_card.xml new file mode 100644 index 0000000000000000000000000000000000000000..94d852b774215c0e6136c35508d9b15f566afe3c --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/include_debuglog_privacy_card.xml @@ -0,0 +1,183 @@ +<?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/debug_log_privacy_card" + style="@style/GreyCard" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/spacing_normal" + app:layout_constraintTop_toTopOf="parent"> + + <TextView + android:id="@+id/debug_log_privacy_card_title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:accessibilityHeading="true" + android:focusable="true" + android:text="@string/debugging_debuglog_privacy_card_title" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/debug_log_privacy_card_container_section_one" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.104" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/debug_log_privacy_card_title"> + + <include + android:id="@+id/bulletpoint_one" + layout="@layout/include_bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + app:layout_constraintBaseline_toBaselineOf="@id/debug_log_privacy_card_first_section_body_one" /> + + <TextView + android:id="@+id/debug_log_privacy_card_first_section_body_one" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debug_log_privacy_card_section_body_one" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/bulletpoint_one" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/debug_log_privacy_card_container_section_two" + 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/debug_log_privacy_card_container_section_one"> + + <include + android:id="@+id/bulletpoint_two" + layout="@layout/include_bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + app:layout_constraintBaseline_toBaselineOf="@id/debug_log_privacy_card_first_section_body_two" /> + + <TextView + android:id="@+id/debug_log_privacy_card_first_section_body_two" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debug_log_privacy_card_container_section_two" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/bulletpoint_two" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/debug_log_privacy_card_container_section_three" + 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/debug_log_privacy_card_container_section_two"> + + <include + android:id="@+id/bulletpoint_three" + layout="@layout/include_bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + app:layout_constraintBaseline_toBaselineOf="@id/debug_log_privacy_card_second_section_body_one" /> + + <TextView + android:id="@+id/debug_log_privacy_card_second_section_body_one" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debug_log_privacy_card_container_section_three" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/bulletpoint_three" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/debug_log_privacy_card_container_section_four" + 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/debug_log_privacy_card_container_section_three"> + + <include + android:id="@+id/bulletpoint_four" + layout="@layout/include_bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + app:layout_constraintBaseline_toBaselineOf="@id/debug_log_privacy_card_second_section_body_two" /> + + <TextView + android:id="@+id/debug_log_privacy_card_second_section_body_two" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debug_log_privacy_card_container_section_four" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/bulletpoint_four" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/debug_log_privacy_card_container_section_five" + 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/debug_log_privacy_card_container_section_four"> + + <include + android:id="@+id/bulletpoint_five" + layout="@layout/include_bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + app:layout_constraintBaseline_toBaselineOf="@id/debug_log_privacy_card_second_section_body_five" /> + + <TextView + android:id="@+id/debug_log_privacy_card_second_section_body_five" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:focusable="true" + android:text="@string/debug_log_privacy_card_container_section_five" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/bulletpoint_five" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </androidx.constraintlayout.widget.ConstraintLayout> +</layout> diff --git a/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml b/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml index 40cf09352152cdeeae077ace3141b38048ff4d62..3ae19f36bc16dedab8b84a46e54bdb24e113bbe3 100644 --- a/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml +++ b/Corona-Warn-App/src/main/res/layout/submission_blocking_dialog_view.xml @@ -23,7 +23,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:text="@string/submission_status_card_title_fetching" + android:text="@string/submission_test_result_pending_steps_waiting_body" app:layout_constraintBottom_toBottomOf="@+id/progress_indicator" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" 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 b4fe423e381104f4636848740dbe208f909fe8a7..3f864197f4d746404191f1e46ff49d39f8d020f4 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -501,7 +501,18 @@ <fragment android:id="@+id/debuglogFragment" android:name="de.rki.coronawarnapp.bugreporting.debuglog.ui.DebugLogFragment" - android:label="DebuglogFragment" /> + android:label="DebuglogFragment" + tools:layout="@layout/bugreporting_debuglog_fragment"> + <action + android:id="@+id/action_debuglogFragment_to_informationPrivacyFragment" + app:destination="@id/informationPrivacyFragment" /> + <action + android:id="@+id/action_debuglogFragment_to_logUploadHistoryFragment" + app:destination="@id/logUploadHistoryFragment" /> + <action + android:id="@+id/action_debuglogFragment_to_debugLogUploadFragment" + app:destination="@id/debugLogUploadFragment" /> + </fragment> <!-- New Release --> <fragment @@ -575,4 +586,23 @@ android:id="@+id/action_settingsPrivacyPreservingAnalyticsFragment_to_ppaMoreInfoFragment" app:destination="@id/ppaMoreInfoFragment" /> </fragment> + <fragment + android:id="@+id/logUploadHistoryFragment" + android:name="de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history.LogUploadHistoryFragment" + android:label="LogUploadHistoryFragment" + tools:layout="@layout/bugreporting_upload_history_fragment" /> + <fragment + android:id="@+id/debugLogUploadFragment" + android:name="de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.DebugLogUploadFragment" + android:label="DebugLogUploadFragment" + tools:layout="@layout/bugreporting_debuglog_upload_fragment" > + <action + android:id="@+id/action_debugLogUploadFragment_to_debugLogLegalFragment" + app:destination="@id/debugLogLegalFragment" /> + </fragment> + <fragment + android:id="@+id/debugLogLegalFragment" + android:name="de.rki.coronawarnapp.bugreporting.debuglog.ui.legal.DebugLogLegalFragment" + android:label="DebugLogLegalFragment" + tools:layout="@layout/bugreporting_legal_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 64892b65dae6282aed43db095d9abba1d30fee96..e8f448daba8728232e67c4ff5388eb4b27eabc2e 100644 --- a/Corona-Warn-App/src/main/res/values-bg/strings.xml +++ b/Corona-Warn-App/src/main/res/values-bg/strings.xml @@ -854,7 +854,7 @@ <!-- XHED: Headline for debug log screen --> <string name="debugging_debuglog_title">"Отчети за грешки"</string> <!-- YTXT: Description for the debug option to record log files --> - <string name="debugging_debuglog_intro_explanation">"Тази Ð¾Ð¿Ñ†Ð¸Ñ Ð·Ð°Ð¿Ð¸Ñва дейÑтвиÑта на приложението в журнал. Ðко е активна и възникне грешка, ще можете да Ñподелите отчета Ñ Ñ€Ð°Ð·Ñ€Ð°Ð±Ð¾Ñ‚Ñ‡Ð¸Ñ†Ð¸Ñ‚Ðµ, за да им помогнете да решат проблема.\nÐко оÑтавите опциÑта активна, това може да доведе до запълване на проÑтранÑтвото за Ñъхранение. Ðко Ñ Ð´ÐµÐ·Ð°ÐºÑ‚Ð¸Ð²Ð¸Ñ€Ð°Ñ‚Ðµ, отчетите за грешки Ñе изтриват."</string> + <string name="debugging_debuglog_intro_explanation_section_one">"Тази Ð¾Ð¿Ñ†Ð¸Ñ Ð·Ð°Ð¿Ð¸Ñва дейÑтвиÑта на приложението в журнал. Ðко е активна и възникне грешка, ще можете да Ñподелите отчета Ñ Ñ€Ð°Ð·Ñ€Ð°Ð±Ð¾Ñ‚Ñ‡Ð¸Ñ†Ð¸Ñ‚Ðµ, за да им помогнете да решат проблема.\nÐко оÑтавите опциÑта активна, това може да доведе до запълване на проÑтранÑтвото за Ñъхранение. Ðко Ñ Ð´ÐµÐ·Ð°ÐºÑ‚Ð¸Ð²Ð¸Ñ€Ð°Ñ‚Ðµ, отчетите за грешки Ñе изтриват."</string> <!-- YTXT: Warning regarding downsides of recording a log file --> <string name="debugging_debuglog_intro_warning">"МолÑ, имайте предвид, че отчетите за грешки може да Ñъдържат поверителни данни (например резултати от теÑтове или запиÑи в дневника на контактите). Ето защо не бива да ÑподелÑте отчетите за грешки публично."</string> <!-- XBUT: Button text to start the log recording --> diff --git a/Corona-Warn-App/src/main/res/values-de/legal_strings.xml b/Corona-Warn-App/src/main/res/values-de/legal_strings.xml index de6248a18b15efc04b890a2fe7e2beca24aee64d..cadef810a86e7fed663708e5eb055ea4eb4787e7 100644 --- a/Corona-Warn-App/src/main/res/values-de/legal_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/legal_strings.xml @@ -86,4 +86,24 @@ <!-- XTXT: Body for Privacy-preserving Analytics settings --> <string name="ppa_settings_privacy_information_body" translatable="false">"Indem Sie oben „Datenspende“ aktivieren, willigen Sie ein:\n\nDie App übermittelt täglich von ihr erfasste Angaben an das RKI. Die Daten betreffen angezeigte Risiko-Begegnungen und Warnungen, durch Sie abgerufene Testergebnisse, ob Sie andere Nutzer gewarnt haben sowie Angaben über das Betriebssystem Ihres Smartphones. Wenn Sie oben weitere Angaben gemacht haben (Region, Altersgruppe), werden auch diese an das RKI übermittelt.\n\nDas RKI wird diese Daten zu Statistiken zusammenfassen und auswerten, um die Wirksamkeit und Funktionsweise der App zu bewerten und Rückschlüsse auf das Pandemiegeschehen zu ziehen. Die dabei gefundenen Erkenntnisse helfen bei der Verbesserung der Funktionen und Nutzerfreundlichkeit der App sowie bei der Steuerung anderer Maßnahmen der Pandemiebekämpfung.\n\nBevor Ihre Daten ausgewertet werden, muss sichergestellt sein, dass jede an der Datenspende teilnehmende App nur einmal gezählt wird und die Statistiken nicht verfälscht werden. Hierfür muss die Echtheit Ihrer App geprüft werden. Dazu wird durch Ihr Smartphone eine eindeutige Kennung erzeugt und an Google in die USA oder andere Drittländer übermittelt, damit Google die Echtheit Ihrer App gegenüber dem RKI bestätigen kann. Die Kennung enthält Informationen über die Version Ihres Smartphones und der App. Google kann damit möglicherweise auf Ihre Identität schließen und nachvollziehen, dass die Echtheitsprüfung Ihres Smartphones stattgefunden hat.\nWeitere Angaben aus der App erhält Google hierbei nicht.\n\nSie können Ihr Einverständnis jederzeit zurücknehmen, indem Sie oben „Datenspende“ deaktivieren."</string> + <!-- XHED: Title for debug legal screen privacy card --> + <string name="debugging_debuglog_legal_privacy_card_title" translatable="false">"Prüfung der Echtheit und Drittlandsübermittlung"</string> + <!-- YTXT: First section for debug legal screen privacy card --> + <string name="debugging_debuglog_legal_privacy_card_first_section" translatable="false">"Um die Echtheit Ihrer App zu bestätigen, erzeugt Ihr Smartphone eine eindeutige Kennung, die Informationen über die Version Ihres Smartphones und der App enthält. Das ist erforderlich, um sicherzustellen, dass nur Nutzer Daten auf diesem Weg an den technischen Support übersenden, die tatsächlich die Corona-Warn-App nutzen und nicht manipulierte Fehlerberichte bereitstellen. Die Kennung wird dafür einmalig an Google übermittelt. Dabei kann es auch zu einer Datenübermittlung in die USA oder andere Drittländer 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, auch ohne einen konkreten Verdacht, auf die übermittelten Daten bei Google zugreifen und diese auswerten, beispielsweise indem sie Daten mit anderen Informationen verknüpfen. Dies betrifft nur die an Google übermittelte Kennung. Die Angaben aus Ihrem Fehlerbericht erhält Google nicht. Möglicherweise kann Google jedoch anhand der Kennung auf Ihre Identität schließen und nachvollziehen, dass die Echtheitsprüfung Ihres Smartphones stattgefunden hat."</string> + <!-- YTXT: Second section for debug legal screen privacy card --> + <string name="debugging_debuglog_legal_privacy_card_second_section" translatable="false">"Wenn Sie mit der Drittlandsübermittlung nicht einverstanden sind, tippen Sie bitte nicht „Einverstanden und Fehlerbericht senden“ an. Sie können die App weiterhin nutzen, eine Ãœbersendung des Fehlerberichtes über die App ist dann jedoch nicht möglich."</string> + + <!-- XHED: Title for debug upload screen privacy card --> + <string name="debugging_debuglog_privacy_card_title" translatable="false">"Datenschutz und Datensicherheit"</string> + <!-- YTXT: First bullet point for debug upload screen privacy card --> + <string name="debug_log_privacy_card_section_body_one" translatable="false">"Die Aufzeichnung des Fehlerberichts ist freiwillig."</string> + <!-- YTXT: Second bullet point for debug upload screen privacy card --> + <string name="debug_log_privacy_card_container_section_two" translatable="false">"Auf den Fehlerbericht haben zunächst nur Sie Zugriff. Sie können im Anschluss entscheiden, ob Sie den Fehlerbericht an den technischen Support senden, ob Sie den Fehlerbericht zunächst auf Ihrem Smartphone speichern oder die Aufzeichnung stoppen und löschen wollen. Wenn Sie den Fehlerbericht zunächst lokal speichern, haben Sie die Möglichkeit, sich den Fehlerbericht selbst anzuschauen, bevor Sie diesen an den Support übermitteln."</string> + <!-- YTXT: Third bullet point for debug upload screen privacy card --> + <string name="debug_log_privacy_card_container_section_three" translatable="false">"Der Fehlerbericht enthält sensible Informationen, zum Beispiel Angaben über Ihr Testergebnis oder das für Sie ermittelte Ansteckungsrisiko. Er enthält keine Informationen über die QR-Codes, die Sie bei der Testregistrierung verwendet haben, über Angaben, die Sie im Kontakt-Tagebuch erfasste haben oder Angaben zu Ihrer Identität."</string> + <!-- YTXT: Fourth bullet point for debug upload screen privacy card --> + <string name="debug_log_privacy_card_container_section_four" translatable="false">"Wir empfehlen die Fehlerberichte nicht zu veröffentlichen und nicht per E-Mail zu versenden."</string> + <!-- YTXT: Fifth bullet point for debug upload screen privacy card --> + <string name="debug_log_privacy_card_container_section_five" translatable="false">"Private Personen oder Unternehmen dürfen von Ihnen nicht die Weitergabe eines aufgezeichneten Fehlerberichtes verlangen."</string> + </resources> diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index 8d0bdcd39f4f1f33b1b89fa0c1fd7d2249539246..f53807cefc58568551b1e572d5157d4b7ce3ffe8 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -614,9 +614,6 @@ <string name="onboarding_ppa_more_info_much_privacy_body">"Die Angaben können nicht mit Ihrer Person verknüpft werden. Ihre Identität bleibt weiterhin geheim.\nDie Angaben werden statistisch ausgewertet, sie werden nicht zu einem Profil gespeichert."</string> - - - <!-- #################################### Onboarding sixteen include ###################################### --> @@ -866,26 +863,86 @@ <!-- XACT: describes illustration --> <string name="information_legal_illustration_description">"Eine Hand hält ein Smartphone mit viel Text, daneben ist ein Paragraphenzeichen als Symbol für das Impressum."</string> + <!-- #################################### + App Information - Bug Reporting + ###################################### --> + <!-- XHED: Headline for debug log screen --> - <string name="debugging_debuglog_title">"Fehlerbericht"</string> - <!-- YTXT: Description for the debug option to record log files --> - <string name="debugging_debuglog_intro_explanation">"Durch diese Option wird das App-Verhalten in einer Textdatei protokolliert. Ist die Option aktiviert bevor ein Fehler auftritt, können Sie diesen Bericht zur Verfügung stellen, um die Entwickler bei der Problemlösung zu unterstützen.\nEine dauerhafte Aktivierung dieser Option führt zu hohem Speicherbedarf. Der Fehlerbericht wird durch Deaktivieren der Option gelöscht."</string> + <string name="debugging_debuglog_title">"Fehlerberichte"</string> + <!-- XHED: Headline for current status of debug log screen --> + <string name="debugging_debuglog_current_status_title">"Fehleranalyse"</string> + <!-- YTXT: Description one for the debug option to record log files --> + <string name="debugging_debuglog_intro_explanation_section_one">"Um den technischen Support der App bei der Fehleranalyse zu unterstützen, können Sie einen Fehlerbericht der CWA aufzeichnen. Hierbei werden die einzelnen technischen Schritte und Ereignisse beim Ablauf der App detailliert aufgezeichnet. Den Fehlerbericht können Sie dann an den technischen Support senden und so helfen, Fehler zu erkennen und zu beheben."</string> + <!-- YTXT: Description two for the debug option to record log files --> + <string name="debugging_debuglog_intro_explanation_section_two">"Weitere Informationen finden Sie in den FAQ:\nFAQ zu den Fehlerberichten"</string> + <!-- XTXT: Debug Log screen increased risk level link label - HAS TO MATCH the link text above --> + <string name="debugging_debuglog_intro_explanation_section_two_link_label">"FAQ zu den Fehlerberichten"</string> + <!-- XTXT: Explains user about about debug log: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#further_details --> + <string name="debugging_debuglog_intro_explanation_section_two_faq_link">"https://www.coronawarn.app/en/faq/#error_log"</string> + <!-- YTXT: Title of ID History --> + <string name="debugging_debuglog_id_history_title">"ID History"</string> + <!-- YTXT: Description of ID History --> + <string name="debugging_debuglog_id_history_body">"IDs der bisher geteilten Fehleranalysen"</string> <!-- YTXT: Warning regarding downsides of recording a log file --> <string name="debugging_debuglog_intro_warning">"Bitte beachten Sie, dass in Fehlerberichten sensible Daten (z.B. Testergebnis oder Kontakt-Tagebuch-Einträge) enthalten sein können. Aus diesem Grund sollten Sie Fehlerberichte nicht öffentlich teilen."</string> <!-- XBUT: Button text to start the log recording --> <string name="debugging_debuglog_action_start_recording">"Starten"</string> <!-- XBUT: Button text to stop the log recording --> - <string name="debugging_debuglog_action_stop_recording">"Löschen"</string> + <string name="debugging_debuglog_action_stop_recording">"Stoppen und löschen"</string> <!-- XBUT: Button text to share the log recording --> - <string name="debugging_debuglog_action_share_log">"Teilen"</string> + <string name="debugging_debuglog_action_share_log">"Fehlerbericht senden"</string> + <!-- XBUT: Button text to locally store the log recording --> + <string name="debugging_debuglog_action_local_log_store">"Lokal speichern und fortsetzen"</string> <!-- YTXT: Status text if a debug log is being recorded --> <string name="debugging_debuglog_status_recording">"Aufzeichnung läuft"</string> + <!-- YTXT: Status text if a debug log is being recorded but there is not enough free storage space --> + <string name="debugging_debuglog_status_lowstorage">"Zu wenig Speicherplatz"</string> <!-- YTXT: Status text if a debug log is not being recorded --> <string name="debugging_debuglog_status_not_recording">"Inaktiv"</string> <!-- YTXT: Describtion for current logging status text if a debug log is not being recorded --> <string name="debugging_debuglog_status_additional_infos">"Derzeitige Größe: %1$s (unkomprimiert)"</string> - <!-- XHED: Title for native sharing dialog --> - <string name="debugging_debuglog_sharing_dialog_title">"CWA Fehlerbericht teilen"</string> + <!-- YTXT: Dialog message if the log recording is stopped, and thus deleted. --> + <string name="debugging_debuglog_stop_confirmation_message">"Der Fehlerbericht wurde gelöscht. Wenn Sie eine lokale Kopie des Fehlerberichts gespeichert haben, wurde diese nicht gelöscht."</string> + <!-- YTXT: Dialog message if there is not enough free storage to start a debug log --> + <string name="debugging_debuglog_start_low_storage_error">"Sie brauchen mindestens 200 MB Speicherplatz, um die Fehleranalyse zu starten. Bitte geben Sie Speicherplatz frei."</string> + <!-- XHED: Dialog title if a user has stored a debug log locally --> + <string name="debugging_debuglog_localexport_title">"Lokal gespeichert"</string> + <!-- YTXT: Dialog message if a user has stored a debug log locally --> + <string name="debugging_debuglog_localexport_message">"Die Fehleranalyse wurde lokal gespeichert."</string> + <!-- YTXT: Dialog message if local export has failed --> + <string name="debugging_debuglog_localexport_error_message">"Das Speichern des Fehlerberichts ist fehlgeschlagen. Bitte überprüfen Sie, ob genügend Speicherplatz zur Verfügung steht."</string> + + <!-- XHED: Title for debug legal screen --> + <string name="debugging_debuglog_legal_dialog_title">"Ausführliche Informationen zur Ãœbersendung der Fehlerberichte"</string> + <!-- YTXT: Section Title for debug legal screen --> + <string name="debugging_debuglog_legal_section_title">"Zur Auswertung der Fehlerberichte durch das RKI"</string> + <!-- YTXT: Section Body for debug legal screen --> + <string name="debugging_debuglog_legal_section_body">"Nachdem die Echtheit Ihrer App geprüft wurde, wird der Fehlerbericht über eine gesicherte Verbindung an das RKI übermittelt. Die Fehlerberichte werden nur zur Fehleranalyse und Fehlerbehebung im Rahmen zukünftiger Updates der App genutzt. Nur Mitarbeiter des technischen Support können auf die Fehlerberichte zugreifen. Die Fehlerberichte enthalten eine Vielzahl von Statusmeldungen und Ereignissen, die in der App ausgelöst wurden, sie enthalten aber keine Hinweise anhand derer das RKI auf Ihre Identität schließen kann. Erst wenn Sie die Fehlerbericht-ID im Zusammenhang mit weiteren Mitteilungen nennen, kann ein Zusammenhang zwischen der Mitteilung (und z.B. Ihrem dort enthaltenen Namen) und den im Fehlerbericht enthaltenen Angaben (z.B. technische Meldungen zur Berechnung im Rahmen der Risiko-Ermittlung, in der App angezeigte Informationen und durchlaufene Schritte, aber ggf. auch einem abgerufenen Testergebnis und ggf. im Rahmen der Warnung geteilter Zufalls-IDs) hergestellt werden."</string> + + <!-- XHED: Title for Bugreporting share log screen --> + <string name="debugging_debuglog_share_log_title">"Fehlerbericht senden"</string> + <!-- YTXT: First body section for bugreporting share log screen --> + <string name="debugging_debuglog_share_log_section_one">"Bevor Sie den aufgezeichneten Fehlerbericht an den technischen Support des RKI übersenden können, ist Ihr Einverständnis erforderlich."</string> + <!-- YTXT: Second body section for bugreporting share log screen --> + <string name="debugging_debuglog_share_log_section_two">"Nach der Ãœbersendung erhalten Sie eine Fehlerbericht-ID. Diese können Sie angeben, um dem technischen Support z.B. weitere Informationen zukommen zu lassen und dabei eine Zuordnung zu Ihrem Fehlerbericht zu ermöglichen. Wenn Sie die Fehlerbericht-ID nicht mitteilen, ist dem RKI eine Zuordnung zu Ihrer Person nicht möglich."</string> + <!-- YTXT: Privacy Information section for bugreporting share log screen --> + <string name="debugging_debuglog_share_log_privacy_information">"Ausführliche Informationen zu dieser Datenverarbeitung und den Datenschutzrisiken in den USA und anderen Drittländern."</string> + <!-- XBUT: Button for bugreporting share log screen --> + <string name="debugging_debuglog_share_log_button">"Einverstanden und senden"</string> + <!-- XHED: Title for Bugreporting share log Privacy Card --> + <string name="debugging_debuglog_share_privacy_card_title">"Ihr Einverständnis"</string> + <!-- YTXT: First body section for Bugreporting share log Privacy Card --> + <string name="debugging_debuglog_share_privacy_card_body_1">"Durch Antippen von „Einverstanden und senden“ willigen Sie in folgende Schritte ein:"</string> + <!-- YTXT: First bullet point for Bugreporting share log Privacy Card --> + <string name="debugging_debuglog_share_privacy_card_bullet_point_1">"Der von Ihnen erstellte Fehlerbericht wird zu Zwecken der Fehleranalyse an das RKI übermittelt. Das RKI wird den Fehlerbericht auswerten, um mögliche Ursachen der in Ihrer App auftretenden Fehler erkennen und beheben zu können. Wenn Sie dem technischen Support die Fehlerbericht-ID mitteilen, kann das RKI den Fehlerbericht Ihrer Person zuordnen."</string> + <!-- YTXT: Second bullet point for Bugreporting share log Privacy Card --> + <string name="debugging_debuglog_share_privacy_card_bullet_point_2">"Bevor der Bericht an das RKI übersendet wird, wird die Echtheit Ihrer App einmalig geprüft. Dazu wird durch Ihr Smartphone eine eindeutige Kennung erzeugt und an Google in die USA oder andere Drittländer übermittelt, damit Google die Echtheit Ihrer App gegenüber dem RKI bestätigen kann. Die Kennung enthält Informationen über die Version Ihres Smartphones und der App. Google kann damit möglicherweise auf Ihre Identität schließen und nachvollziehen, dass die Echtheitsprüfung Ihres Smartphones stattgefunden hat. Weitere Angaben aus der App oder dem Fehlerbericht erhält Google hierbei nicht."</string> + <!-- YTXT: Second body section for Bugreporting share log Privacy Card --> + <string name="debugging_debuglog_share_privacy_card_body_2">"Sollten Sie dem technischen Support nach der Ãœbersendung Ihre Fehlerbericht-ID mitgeteilt haben, können Sie auch jederzeit die Löschung der weiteren mitgeteilten Informationen, der Fehlerbericht-ID und des Fehlerberichtes verlangen. Andernfalls wird der Fehlerbericht automatisch nach 14 Tagen gelöscht."</string> + <!-- XHED: Title for log upload history --> + <string name="debugging_debuglog_uploadhistory_title">"ID Historie"</string> + <!-- YTXT: Description for log upload history --> + <string name="debugging_debuglog_uploadhistory_description">"Hier sehen Sie die IDs Ihrer Fehleranalyse-Protokolle."</string> <!-- #################################### Interoperability @@ -1573,6 +1630,8 @@ Generic Error Messages ###################################### --> <!-- XHED: error dialog - headline --> + <string name="errors_generic_headline_short">"Fehler"</string> + <!-- XHED: error dialog - headline --> <string name="errors_generic_headline">"Etwas ist schiefgelaufen."</string> <!-- XTXT: error dialog - short text for error reason --> <string name="errors_generic_details_headline">"Ursache"</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 9f51fb3e4e6a9c0ed12186e3ddd5f36401208d35..aba2825d1892a209d67585843c711f1da7f1535c 100644 --- a/Corona-Warn-App/src/main/res/values-en/strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/strings.xml @@ -854,7 +854,7 @@ <!-- XHED: Headline for debug log screen --> <string name="debugging_debuglog_title">"Error Report"</string> <!-- YTXT: Description for the debug option to record log files --> - <string name="debugging_debuglog_intro_explanation">"This option logs the app’s behavior in a text file. If this option is activated before an error occurs, you can make this report available to help the developers solve the problem.\nIf you leave this option activated, it will result in high storage requirements. If you deactivate the option, the error report is deleted."</string> + <string name="debugging_debuglog_intro_explanation_section_one">"This option logs the app’s behavior in a text file. If this option is activated before an error occurs, you can make this report available to help the developers solve the problem.\nIf you leave this option activated, it will result in high storage requirements. If you deactivate the option, the error report is deleted."</string> <!-- YTXT: Warning regarding downsides of recording a log file --> <string name="debugging_debuglog_intro_warning">"Please note that error reports may contain sensitive data (such as a test result or contact journal entries). Therefore, you should not share these error reports publicly."</string> <!-- XBUT: Button text to start the log recording --> 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 b851b08dee83958af48fd6b7e480db167af5e98b..8736f5ab18f43c9e62a50432db2f01e9abde6885 100644 --- a/Corona-Warn-App/src/main/res/values-pl/strings.xml +++ b/Corona-Warn-App/src/main/res/values-pl/strings.xml @@ -854,7 +854,7 @@ <!-- XHED: Headline for debug log screen --> <string name="debugging_debuglog_title">"Raport o bÅ‚Ä™dach"</string> <!-- YTXT: Description for the debug option to record log files --> - <string name="debugging_debuglog_intro_explanation">"Ta opcja umożliwia rejestrowanie zachowania aplikacji w pliku tekstowym. JeÅ›li zostanie ona aktywowana przed wystÄ…pieniem bÅ‚Ä™du, bÄ™dziesz mieć możliwość udostÄ™pnienia tego raportu, aby pomóc programistom rozwiÄ…zać problem.\nPozostawienie tej opcji w stanie aktywnoÅ›ci wiąże siÄ™ z dużym zapotrzebowaniem na pamięć. JeÅ›li jÄ… wyÅ‚Ä…czysz, raport o bÅ‚Ä™dach zostanie usuniÄ™ty."</string> + <string name="debugging_debuglog_intro_explanation_section_one">"Ta opcja umożliwia rejestrowanie zachowania aplikacji w pliku tekstowym. JeÅ›li zostanie ona aktywowana przed wystÄ…pieniem bÅ‚Ä™du, bÄ™dziesz mieć możliwość udostÄ™pnienia tego raportu, aby pomóc programistom rozwiÄ…zać problem.\nPozostawienie tej opcji w stanie aktywnoÅ›ci wiąże siÄ™ z dużym zapotrzebowaniem na pamięć. JeÅ›li jÄ… wyÅ‚Ä…czysz, raport o bÅ‚Ä™dach zostanie usuniÄ™ty."</string> <!-- YTXT: Warning regarding downsides of recording a log file --> <string name="debugging_debuglog_intro_warning">"Należy pamiÄ™tać, że raporty o bÅ‚Ä™dach mogÄ… zawierać dane wrażliwe (takie jak wynik testu lub wpisy dziennika kontaktów). Z tego wzglÄ™du takie raporty nie powinny być udostÄ™pniane publicznie."</string> <!-- XBUT: Button text to start the log recording --> 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 222ce34eb385bd33c512626c27786cb4a679a431..8dbf8987e307944ae2a75a3c8f7fbcaeb173c4d9 100644 --- a/Corona-Warn-App/src/main/res/values-ro/strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/strings.xml @@ -854,7 +854,7 @@ <!-- XHED: Headline for debug log screen --> <string name="debugging_debuglog_title">"Raport de erori"</string> <!-- YTXT: Description for the debug option to record log files --> - <string name="debugging_debuglog_intro_explanation">"Această opÈ›iune înregistrează comportamentul aplicaÈ›iei într-un fiÈ™ier text de jurnal. Dacă opÈ›iunea este activată înainte de apariÈ›ia unei erori, puteÈ›i face disponibil acest raport pentru a-i ajuta pe dezvoltatori să rezolve problema.\nDacă lăsaÈ›i activată această opÈ›iune, acest lucru va conduce la cerinÈ›e de stocare ridicate. Dacă dezactivaÈ›i opÈ›iunea, raportul de erori este È™ters."</string> + <string name="debugging_debuglog_intro_explanation_section_one">"Această opÈ›iune înregistrează comportamentul aplicaÈ›iei într-un fiÈ™ier text de jurnal. Dacă opÈ›iunea este activată înainte de apariÈ›ia unei erori, puteÈ›i face disponibil acest raport pentru a-i ajuta pe dezvoltatori să rezolve problema.\nDacă lăsaÈ›i activată această opÈ›iune, acest lucru va conduce la cerinÈ›e de stocare ridicate. Dacă dezactivaÈ›i opÈ›iunea, raportul de erori este È™ters."</string> <!-- YTXT: Warning regarding downsides of recording a log file --> <string name="debugging_debuglog_intro_warning">"ReÈ›ineÈ›i că rapoartele de erori pot conÈ›ine date sensibile (precum un rezultat al testului sau intrări în jurnal ale contactelor). ÃŽn consecință, nu este recomandat să împărtășiÈ›i public aceste rapoarte de erori."</string> <!-- XBUT: Button text to start the log recording --> 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 4efbf1533b5a2a7d0c2ed3b8e67875adb193527d..11ca6f8de7fd5ed2bd5a42f2133bd2315962832d 100644 --- a/Corona-Warn-App/src/main/res/values-tr/strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/strings.xml @@ -854,7 +854,7 @@ <!-- XHED: Headline for debug log screen --> <string name="debugging_debuglog_title">"Hata Raporu"</string> <!-- YTXT: Description for the debug option to record log files --> - <string name="debugging_debuglog_intro_explanation">"Bu seçenek, uygulama davranışını bir metin dosyasına kaydeder. Bir hata meydana gelmeden önce bu seçenek etkinleÅŸtirilirse geliÅŸtiricilerin sorunu çözmesine yardımcı olmak için bu raporu sunabilirsiniz.\nBu seçeneÄŸi etkinleÅŸtirilmiÅŸ durumda bırakırsanız yüksek depolama alanı gereklilikleri oluÅŸacaktır. SeçeneÄŸi devre dışı bırakırsanız hata raporu silinir."</string> + <string name="debugging_debuglog_intro_explanation_section_one">"Bu seçenek, uygulama davranışını bir metin dosyasına kaydeder. Bir hata meydana gelmeden önce bu seçenek etkinleÅŸtirilirse geliÅŸtiricilerin sorunu çözmesine yardımcı olmak için bu raporu sunabilirsiniz.\nBu seçeneÄŸi etkinleÅŸtirilmiÅŸ durumda bırakırsanız yüksek depolama alanı gereklilikleri oluÅŸacaktır. SeçeneÄŸi devre dışı bırakırsanız hata raporu silinir."</string> <!-- YTXT: Warning regarding downsides of recording a log file --> <string name="debugging_debuglog_intro_warning">"Lütfen hata raporlarının hassas veriler içerebileceÄŸini unutmayın (örneÄŸin, test sonucu ya da temas güncesi giriÅŸleri). Bu nedenle bu hata raporlarını herkese açık ÅŸekilde paylaÅŸmamanız gerekir."</string> <!-- XBUT: Button text to start the log recording --> diff --git a/Corona-Warn-App/src/main/res/values/legal_strings.xml b/Corona-Warn-App/src/main/res/values/legal_strings.xml index 1ad11eafa6fd0d46eb4aefe7d2c89c6bfdce6384..768683677d21768f0426553f485c0ac9c0a3ae24 100644 --- a/Corona-Warn-App/src/main/res/values/legal_strings.xml +++ b/Corona-Warn-App/src/main/res/values/legal_strings.xml @@ -71,20 +71,40 @@ <!-- XTXT: onboarding privacy preserving analytics (ppa) - consent title --> <string name="ppa_onboarding_consent_title" translatable="false">"Your Consent"</string> <!-- XTXT: Body for Privacy-preserving Analytics onboarding --> - <string name="ppa_onboarding_privacy_information_body" translatable="false">"By tapping on “Acceptâ€, you consent to the following:\n\nThe app will transmit information it records to the RKI, on a daily basis. The data concerns possible exposures and warnings that have been displayed to you, test results you have retrieved, and whether you have warned other users, and information about your smartphone’s operating system. If you have provided further details above (region, age group), then the RKI will also receive this information.\n\nThe RKI will compile this data into statistics and analyze it to assess the effectiveness and functioning of the app, and draw conclusions regarding the pandemic. The resulting knowledge will help to improve the app’s features and make it more user-friendly, as well as to inform other pandemic response measures.\n\nBefore your data is analyzed, it is necessary to ensure that each app that shares data is only counted once, so as not to distort the statistics. This is why the authenticity of your app needs to be verified. For this purpose, a unique identifier is generated by your smartphone and transmitted to Google in the U.S. or other third countries, so that Google can confirm the authenticity of your app to the RKI. The identifier contains information about the version of your smartphone and the app. On this basis, Google may be able to infer your identity and learn that your smartphone has been authenticated. Google does not receive any other information from the app during this process.\n\nYou can withdraw your consent at any time by disabling the data sharing feature in the app's settings."</string> - <!-- XTXT: Body point consent for Privacy-preserving Analytics --> - <string name="ppa_onboarding_privacy_information_point_consent" translatable="false">"Your consent is voluntary. If you do not give your consent, you will still be able to use the app."</string> - <!-- XTXT: Body point identity for Privacy-preserving Analytics --> - <string name="ppa_onboarding_privacy_information_point_identity" translatable="false">"If you share data about your use of the app, your identity will still be kept secret from the RKI. This means the RKI will not find out who you are or who you have met. No profiles will be created either."</string> - <!-- XTXT: Body point sixteen for Privacy-preserving Analytics --> - <string name="ppa_onboarding_privacy_information_point_sixteen" translatable="false">"You need to be at least 16 years old to give your consent."</string> - <!-- XHED: Title for Privacy-preserving Analytics additional info --> - <string name="ppa_onboarding_more_info_title" translatable="false">"Verification of authenticity and transfer to a third country"</string> - <!-- XTXT: Body for Privacy-preserving Analytics additional info --> - <string name="ppa_onboarding_more_info_body" translatable="false">"To confirm the authenticity of your app, your smartphone will generate a unique identifier that contains information about the version of your smartphone and the app. This is necessary to prevent data from being transmitted to the RKI more than once or in an unauthorised manner, as this could distort the results of the analysis. The identifier will be transmitted to Google. This may result in data being transferred to the U.S. or other third countries. There, the level of data protection may not be considered equivalent under European law and it may not be possible to enforce your European data protection rights. In particular, there is a possibility that once the transmitted data reaches Google, it may be accessed and analyzed by security authorities in the third country, even if they have no specific grounds for suspicion, for example by linking the data with other information. This only concerns the identifier sent to Google. Google will not receive the other information about how you use the Corona-Warn-App. It may however be possible for Google to infer your identity from the identifier and learn that your smartphone has been authenticated.\n\nIf you do not consent to this transfer of your data to a third country, please do not tap on “Acceptâ€. You will still be able to use the app, but not the data sharing feature."</string> - <!-- XTXT: onboarding privacy preserving analytics (ppa) - consent title --> - <string name="ppa_onboarding_privacy_information_title" translatable="false">"Your Consent"</string> - <!-- XTXT: Body for Privacy-preserving Analytics settings --> - <string name="ppa_settings_privacy_information_body" translatable="false">"By enabling “Share Data†above, you consent to the following:\n\nThe app will transmit information it records to the RKI, on a daily basis. The data concerns possible exposures and warnings that have been displayed to you, test results you have retrieved, and whether you have warned other users, and information about your smartphone’s operating system. If you have provided further details above (region, age group), then the RKI will also receive this information.\n\nThe RKI will compile this data into statistics and analyze it to assess the effectiveness and functioning of the app, and draw conclusions regarding the pandemic. The resulting knowledge will help to improve the app’s features and make it more user-friendly, as well as to inform other pandemic response measures.\n\nBefore your data is analyzed, it is necessary to ensure that each app that shares data is only counted once, so as not to distort the statistics. This is why the authenticity of your app needs to be verified. For this purpose, a unique identifier is generated by your smartphone and transmitted to Google in the U.S. or other third countries, so that Google can confirm the authenticity of your app to the RKI. The identifier contains information about the version of your smartphone and the app. On this basis, Google may be able to infer your identity and learn that your smartphone has been authenticated. Google does not receive any other information from the app during this process.\n\n You can withdraw your consent at any time by disabling “Share Data†above."</string> + <string name="ppa_onboarding_privacy_information_body" translatable="false">"By tapping on “Acceptâ€, you consent to the following:\n\nThe app will transmit information it records to the RKI, on a daily basis. The data concerns possible exposures and warnings that have been displayed to you, test results you have retrieved, and whether you have warned other users, and information about your smartphone’s operating system. If you have provided further details above (region, age group), then the RKI will also receive this information.\n\nThe RKI will compile this data into statistics and analyze it to assess the effectiveness and functioning of the app, and draw conclusions regarding the pandemic. The resulting knowledge will help to improve the app’s features and make it more user-friendly, as well as to inform other pandemic response measures.\n\nBefore your data is analyzed, it is necessary to ensure that each app that shares data is only counted once, so as not to distort the statistics. This is why the authenticity of your app needs to be verified. For this purpose, a unique identifier is generated by your smartphone and transmitted to Google in the U.S. or other third countries, so that Google can confirm the authenticity of your app to the RKI. The identifier contains information about the version of your smartphone and the app. On this basis, Google may be able to infer your identity and learn that your smartphone has been authenticated. Google does not receive any other information from the app during this process.\n\nYou can withdraw your consent at any time by disabling the data sharing feature in the app's settings."</string> + <!-- XTXT: Body point consent for Privacy-preserving Analytics --> + <string name="ppa_onboarding_privacy_information_point_consent" translatable="false">"Your consent is voluntary. If you do not give your consent, you will still be able to use the app."</string> + <!-- XTXT: Body point identity for Privacy-preserving Analytics --> + <string name="ppa_onboarding_privacy_information_point_identity" translatable="false">"If you share data about your use of the app, your identity will still be kept secret from the RKI. This means the RKI will not find out who you are or who you have met. No profiles will be created either."</string> + <!-- XTXT: Body point sixteen for Privacy-preserving Analytics --> + <string name="ppa_onboarding_privacy_information_point_sixteen" translatable="false">"You need to be at least 16 years old to give your consent."</string> + <!-- XHED: Title for Privacy-preserving Analytics additional info --> + <string name="ppa_onboarding_more_info_title" translatable="false">"Verification of authenticity and transfer to a third country"</string> + <!-- XTXT: Body for Privacy-preserving Analytics additional info --> + <string name="ppa_onboarding_more_info_body" translatable="false">"To confirm the authenticity of your app, your smartphone will generate a unique identifier that contains information about the version of your smartphone and the app. This is necessary to prevent data from being transmitted to the RKI more than once or in an unauthorised manner, as this could distort the results of the analysis. The identifier will be transmitted to Google. This may result in data being transferred to the U.S. or other third countries. There, the level of data protection may not be considered equivalent under European law and it may not be possible to enforce your European data protection rights. In particular, there is a possibility that once the transmitted data reaches Google, it may be accessed and analyzed by security authorities in the third country, even if they have no specific grounds for suspicion, for example by linking the data with other information. This only concerns the identifier sent to Google. Google will not receive the other information about how you use the Corona-Warn-App. It may however be possible for Google to infer your identity from the identifier and learn that your smartphone has been authenticated.\n\nIf you do not consent to this transfer of your data to a third country, please do not tap on “Acceptâ€. You will still be able to use the app, but not the data sharing feature."</string> + <!-- XTXT: onboarding privacy preserving analytics (ppa) - consent title --> + <string name="ppa_onboarding_privacy_information_title" translatable="false">"Your Consent"</string> + <!-- XTXT: Body for Privacy-preserving Analytics settings --> + <string name="ppa_settings_privacy_information_body" translatable="false">"By enabling “Share Data†above, you consent to the following:\n\nThe app will transmit information it records to the RKI, on a daily basis. The data concerns possible exposures and warnings that have been displayed to you, test results you have retrieved, and whether you have warned other users, and information about your smartphone’s operating system. If you have provided further details above (region, age group), then the RKI will also receive this information.\n\nThe RKI will compile this data into statistics and analyze it to assess the effectiveness and functioning of the app, and draw conclusions regarding the pandemic. The resulting knowledge will help to improve the app’s features and make it more user-friendly, as well as to inform other pandemic response measures.\n\nBefore your data is analyzed, it is necessary to ensure that each app that shares data is only counted once, so as not to distort the statistics. This is why the authenticity of your app needs to be verified. For this purpose, a unique identifier is generated by your smartphone and transmitted to Google in the U.S. or other third countries, so that Google can confirm the authenticity of your app to the RKI. The identifier contains information about the version of your smartphone and the app. On this basis, Google may be able to infer your identity and learn that your smartphone has been authenticated. Google does not receive any other information from the app during this process.\n\n You can withdraw your consent at any time by disabling “Share Data†above."</string> + + <!-- XHED: Title for debug legal screen privacy card --> + <string name="debugging_debuglog_legal_privacy_card_title" translatable="false">"Prüfung der Echtheit und Drittlandsübermittlung"</string> + <!-- YTXT: First section for debug legal screen privacy card --> + <string name="debugging_debuglog_legal_privacy_card_first_section" translatable="false">"Um die Echtheit Ihrer App zu bestätigen, erzeugt Ihr Smartphone eine eindeutige Kennung, die Informationen über die Version Ihres Smartphones und der App enthält. Das ist erforderlich, um sicherzustellen, dass nur Nutzer Daten auf diesem Weg an den technischen Support übersenden, die tatsächlich die Corona-Warn-App nutzen und nicht manipulierte Fehlerberichte bereitstellen. Die Kennung wird dafür einmalig an Google übermittelt. Dabei kann es auch zu einer Datenübermittlung in die USA oder andere Drittländer 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, auch ohne einen konkreten Verdacht, auf die übermittelten Daten bei Google zugreifen und diese auswerten, beispielsweise indem sie Daten mit anderen Informationen verknüpfen. Dies betrifft nur die an Google übermittelte Kennung. Die Angaben aus Ihrem Fehlerbericht erhält Google nicht. Möglicherweise kann Google jedoch anhand der Kennung auf Ihre Identität schließen und nachvollziehen, dass die Echtheitsprüfung Ihres Smartphones stattgefunden hat."</string> + <!-- YTXT: Second section for debug legal screen privacy card --> + <string name="debugging_debuglog_legal_privacy_card_second_section" translatable="false">"Wenn Sie mit der Drittlandsübermittlung nicht einverstanden sind, tippen Sie bitte nicht „Einverstanden und Fehlerbericht senden“ an. Sie können die App weiterhin nutzen, eine Ãœbersendung des Fehlerberichtes über die App ist dann jedoch nicht möglich."</string> + + <!-- XHED: Title for debug upload screen privacy card --> + <string name="debugging_debuglog_privacy_card_title" translatable="false">"Datenschutz und Datensicherheit"</string> + <!-- YTXT: First bullet point for debug upload screen privacy card --> + <string name="debug_log_privacy_card_section_body_one" translatable="false">"Die Aufzeichnung des Fehlerberichts ist freiwillig."</string> + <!-- YTXT: Second bullet point for debug upload screen privacy card --> + <string name="debug_log_privacy_card_container_section_two" translatable="false">"Auf den Fehlerbericht haben zunächst nur Sie Zugriff. Sie können im Anschluss entscheiden, ob Sie den Fehlerbericht an den technischen Support senden, ob Sie den Fehlerbericht zunächst auf Ihrem Smartphone speichern oder die Aufzeichnung stoppen und löschen wollen. Wenn Sie den Fehlerbericht zunächst lokal speichern, haben Sie die Möglichkeit, sich den Fehlerbericht selbst anzuschauen, bevor Sie diesen an den Support übermitteln."</string> + <!-- YTXT: Third bullet point for debug upload screen privacy card --> + <string name="debug_log_privacy_card_container_section_three" translatable="false">"Der Fehlerbericht enthält sensible Informationen, zum Beispiel Angaben über Ihr Testergebnis oder das für Sie ermittelte Ansteckungsrisiko. Er enthält keine Informationen über die QR-Codes, die Sie bei der Testregistrierung verwendet haben, über Angaben, die Sie im Kontakt-Tagebuch erfasste haben oder Angaben zu Ihrer Identität."</string> + <!-- YTXT: Fourth bullet point for debug upload screen privacy card --> + <string name="debug_log_privacy_card_container_section_four" translatable="false">"Wir empfehlen die Fehlerberichte nicht zu veröffentlichen und nicht per E-Mail zu versenden."</string> + <!-- YTXT: Fifth bullet point for debug upload screen privacy card --> + <string name="debug_log_privacy_card_container_section_five" translatable="false">"Private Personen oder Unternehmen dürfen von Ihnen nicht die Weitergabe eines aufgezeichneten Fehlerberichtes verlangen."</string> </resources> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index 6b43861850949f36e823b51041a6933e7586fc09..7ec59c027d570ce190a0ffa40fc22f8cb4f8e47f 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -879,26 +879,88 @@ <string name="information_legal_illustration_description">"A hand holds a smartphone displaying a large body of text on the screen. Next to the text is a section symbol representing the imprint."</string> + <!-- #################################### + App Information - Bug Reporting + ###################################### --> + <!-- XHED: Headline for debug log screen --> <string name="debugging_debuglog_title">"Error Report"</string> - <!-- YTXT: Description for the debug option to record log files --> - <string name="debugging_debuglog_intro_explanation">"This option logs the app’s behavior in a text file. If this option is activated before an error occurs, you can make this report available to help the developers solve the problem.\nIf you leave this option activated, it will result in high storage requirements. If you deactivate the option, the error report is deleted."</string> + <!-- XHED: Headline for current status of debug log screen --> + <string name="debugging_debuglog_current_status_title">"Fehleranalyse"</string> + <!-- YTXT: Description one for the debug option to record log files --> + <string name="debugging_debuglog_intro_explanation_section_one">"Um den technischen Support der App bei der Fehleranalyse zu unterstützen, können Sie einen Fehlerbericht der CWA aufzeichnen. Hierbei werden die einzelnen technischen Schritte und Ereignisse beim Ablauf der App detailliert aufgezeichnet. Den Fehlerbericht können Sie dann an den technischen Support senden und so helfen, Fehler zu erkennen und zu beheben."</string> + <!-- YTXT: Description two for the debug option to record log files --> + <string name="debugging_debuglog_intro_explanation_section_two">"Weitere Informationen finden Sie in den FAQ:\nFAQ zu den Fehlerberichten"</string> + <!-- XTXT: Debug Log screen increased risk level link label - HAS TO MATCH the link text above --> + <string name="debugging_debuglog_intro_explanation_section_two_link_label">"FAQ zu den Fehlerberichten"</string> + <!-- XTXT: Explains user about about debug log: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#further_details --> + <string name="debugging_debuglog_intro_explanation_section_two_faq_link">"https://www.coronawarn.app/en/faq/#error_log"</string> + <!-- YTXT: Title of ID History --> + <string name="debugging_debuglog_id_history_title">"ID History"</string> + <!-- YTXT: Description of ID History --> + <string name="debugging_debuglog_id_history_body">"IDs der bisher geteilten Fehleranalysen"</string> <!-- YTXT: Warning regarding downsides of recording a log file --> <string name="debugging_debuglog_intro_warning">"Please note that error reports may contain sensitive data (such as a test result or contact journal entries). Therefore, you should not share these error reports publicly."</string> <!-- XBUT: Button text to start the log recording --> <string name="debugging_debuglog_action_start_recording">"Start"</string> <!-- XBUT: Button text to stop the log recording --> - <string name="debugging_debuglog_action_stop_recording">"Delete"</string> + <string name="debugging_debuglog_action_stop_recording">"Stoppen und löschen"</string> <!-- XBUT: Button text to share the log recording --> - <string name="debugging_debuglog_action_share_log">"Share"</string> + <string name="debugging_debuglog_action_share_log">"Fehlerbericht senden"</string> + <!-- XBUT: Button text to locally store the log recording --> + <string name="debugging_debuglog_action_local_log_store">"Lokal speichern und fortsetzen"</string> <!-- YTXT: Status text if a debug log is being recorded --> <string name="debugging_debuglog_status_recording">"Recording Active"</string> + <!-- YTXT: Status text if a debug log is being recorded but there is not enough free storage space --> + <string name="debugging_debuglog_status_lowstorage">"Zu wenig Speicherplatz"</string> <!-- YTXT: Status text if a debug log is not being recorded --> <string name="debugging_debuglog_status_not_recording">"Inactive"</string> <!-- YTXT: Describtion for current logging status text if a debug log is not being recorded --> <string name="debugging_debuglog_status_additional_infos">"Current size: %1$s (uncompressed)"</string> <!-- XHED: Title for native sharing dialog --> <string name="debugging_debuglog_sharing_dialog_title">"Share CWA Error Report"</string> + <!-- YTXT: Dialog message if the log recording is stopped, and thus deleted. --> + <string name="debugging_debuglog_stop_confirmation_message">"Der Fehlerbericht wurde gelöscht. Wenn Sie eine lokale Kopie des Fehlerberichts gespeichert haben, wurde diese nicht gelöscht."</string> + <!-- YTXT: Dialog message if there is not enough free storage to start a debug log --> + <string name="debugging_debuglog_start_low_storage_error">Sie brauchen mindestens 200 MB Speicherplatz, um die Fehleranalyse zu starten. Bitte geben Sie Speicherplatz frei.</string> + <!-- XHED: Dialog title if a user has stored a debug log locally --> + <string name="debugging_debuglog_localexport_title">"Lokal gespeichert"</string> + <!-- YTXT: Dialog message if a user has stored a debug log locally --> + <string name="debugging_debuglog_localexport_message">"Die Fehleranalyse wurde lokal gespeichert."</string> + <!-- YTXT: Dialog message if local export has failed --> + <string name="debugging_debuglog_localexport_error_message">"Das Speichern des Fehlerberichts ist fehlgeschlagen. Bitte überprüfen Sie, ob genügend Speicherplatz zur Verfügung steht."</string> + + <!-- XHED: Title for debug legal screen --> + <string name="debugging_debuglog_legal_dialog_title">"Ausführliche Informationen zur Ãœbersendung der Fehlerberichte"</string> + <!-- YTXT: Section Title for debug legal screen --> + <string name="debugging_debuglog_legal_section_title">"Zur Auswertung der Fehlerberichte durch das RKI"</string> + <!-- YTXT: Section Body for debug legal screen --> + <string name="debugging_debuglog_legal_section_body">"Nachdem die Echtheit Ihrer App geprüft wurde, wird der Fehlerbericht über eine gesicherte Verbindung an das RKI übermittelt. Die Fehlerberichte werden nur zur Fehleranalyse und Fehlerbehebung im Rahmen zukünftiger Updates der App genutzt. Nur Mitarbeiter des technischen Support können auf die Fehlerberichte zugreifen. Die Fehlerberichte enthalten eine Vielzahl von Statusmeldungen und Ereignissen, die in der App ausgelöst wurden, sie enthalten aber keine Hinweise anhand derer das RKI auf Ihre Identität schließen kann. Erst wenn Sie die Fehlerbericht-ID im Zusammenhang mit weiteren Mitteilungen nennen, kann ein Zusammenhang zwischen der Mitteilung (und z.B. Ihrem dort enthaltenen Namen) und den im Fehlerbericht enthaltenen Angaben (z.B. technische Meldungen zur Berechnung im Rahmen der Risiko-Ermittlung, in der App angezeigte Informationen und durchlaufene Schritte, aber ggf. auch einem abgerufenen Testergebnis und ggf. im Rahmen der Warnung geteilter Zufalls-IDs) hergestellt werden."</string> + + <!-- XHED: Title for Bugreporting share log screen --> + <string name="debugging_debuglog_share_log_title">"Fehlerbericht senden"</string> + <!-- YTXT: First body section for bugreporting share log screen --> + <string name="debugging_debuglog_share_log_section_one">"Bevor Sie den aufgezeichneten Fehlerbericht an den technischen Support des RKI übersenden können, ist Ihr Einverständnis erforderlich."</string> + <!-- YTXT: Second body section for bugreporting share log screen --> + <string name="debugging_debuglog_share_log_section_two">"Nach der Ãœbersendung erhalten Sie eine Fehlerbericht-ID. Diese können Sie angeben, um dem technischen Support z.B. weitere Informationen zukommen zu lassen und dabei eine Zuordnung zu Ihrem Fehlerbericht zu ermöglichen. Wenn Sie die Fehlerbericht-ID nicht mitteilen, ist dem RKI eine Zuordnung zu Ihrer Person nicht möglich."</string> + <!-- YTXT: Privacy Information section for bugreporting share log screen --> + <string name="debugging_debuglog_share_log_privacy_information">"Ausführliche Informationen zu dieser Datenverarbeitung und den Datenschutzrisiken in den USA und anderen Drittländern."</string> + <!-- XBUT: Button for bugreporting share log screen --> + <string name="debugging_debuglog_share_log_button">"Einverstanden und senden"</string> + <!-- XHED: Title for Bugreporting share log Privacy Card --> + <string name="debugging_debuglog_share_privacy_card_title">"Ihr Einverständnis"</string> + <!-- YTXT: First body section for Bugreporting share log Privacy Card --> + <string name="debugging_debuglog_share_privacy_card_body_1">"Durch Antippen von „Einverstanden und senden“ willigen Sie in folgende Schritte ein:"</string> + <!-- YTXT: First bullet point for Bugreporting share log Privacy Card --> + <string name="debugging_debuglog_share_privacy_card_bullet_point_1">"Der von Ihnen erstellte Fehlerbericht wird zu Zwecken der Fehleranalyse an das RKI übermittelt. Das RKI wird den Fehlerbericht auswerten, um mögliche Ursachen der in Ihrer App auftretenden Fehler erkennen und beheben zu können. Wenn Sie dem technischen Support die Fehlerbericht-ID mitteilen, kann das RKI den Fehlerbericht Ihrer Person zuordnen."</string> + <!-- YTXT: Second bullet point for Bugreporting share log Privacy Card --> + <string name="debugging_debuglog_share_privacy_card_bullet_point_2">"Bevor der Bericht an das RKI übersendet wird, wird die Echtheit Ihrer App einmalig geprüft. Dazu wird durch Ihr Smartphone eine eindeutige Kennung erzeugt und an Google in die USA oder andere Drittländer übermittelt, damit Google die Echtheit Ihrer App gegenüber dem RKI bestätigen kann. Die Kennung enthält Informationen über die Version Ihres Smartphones und der App. Google kann damit möglicherweise auf Ihre Identität schließen und nachvollziehen, dass die Echtheitsprüfung Ihres Smartphones stattgefunden hat. Weitere Angaben aus der App oder dem Fehlerbericht erhält Google hierbei nicht."</string> + <!-- YTXT: Second body section for Bugreporting share log Privacy Card --> + <string name="debugging_debuglog_share_privacy_card_body_2">"Sollten Sie dem technischen Support nach der Ãœbersendung Ihre Fehlerbericht-ID mitgeteilt haben, können Sie auch jederzeit die Löschung der weiteren mitgeteilten Informationen, der Fehlerbericht-ID und des Fehlerberichtes verlangen. Andernfalls wird der Fehlerbericht automatisch nach 14 Tagen gelöscht."</string> + <!-- XHED: Title for log upload history --> + <string name="debugging_debuglog_uploadhistory_title">"ID Historie"</string> + <!-- YTXT: Description for log upload history --> + <string name="debugging_debuglog_uploadhistory_description">"Hier sehen Sie die IDs Ihrer Fehleranalyse-Protokolle."</string> <!-- #################################### Interoperability @@ -1587,6 +1649,8 @@ Generic Error Messages ###################################### --> <!-- XHED: error dialog - headline --> + <string name="errors_generic_headline_short">"Error"</string> + <!-- XHED: error dialog - headline --> <string name="errors_generic_headline">"Something went wrong."</string> <!-- XTXT: error dialog - short text for error reason --> <string name="errors_generic_details_headline">"Cause"</string> diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index d1fc93e477682fd469efb0b74369d1b656953e38..e34e4c41d6fc611c3b172befc56cd522b71640b7 100644 --- a/Corona-Warn-App/src/main/res/values/styles.xml +++ b/Corona-Warn-App/src/main/res/values/styles.xml @@ -57,11 +57,19 @@ <item name="navigationContentDescription">@string/accessibility_back</item> </style> + <style name="CWAToolbar.BackArrow.Transparent"> + <item name="android:background">@color/colorTransparent</item> + </style> + <style name="CWAToolbar.Close"> <item name="navigationIcon">@drawable/ic_close</item> <item name="navigationContentDescription">@string/accessibility_close</item> </style> + <style name="CWAToolbar.Close.Transparent"> + <item name="android:background">@color/colorTransparent</item> + </style> + <!-- Dialog Theme--> <style name="DialogTheme" parent="Theme.MaterialComponents.DayNight.Dialog.Alert"> <item name="buttonBarPositiveButtonStyle">@style/DialogButtonTheme</item> @@ -104,6 +112,11 @@ <item name="android:textColor">@color/button_text_color_emphasized</item> </style> + <style name="buttonBarAlertRed" parent="button"> + <item name="android:backgroundTint">@color/button_alert_red</item> + <item name="android:textColor">@color/button_text_color_emphasized</item> + </style> + <style name="contactDiaryPersonButton" parent="Widget.MaterialComponents.Button.OutlinedButton"> <item name="strokeColor">@color/button_contact_diary_person</item> <item name="strokeWidth">2dp</item> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt index 46082ad4a64534c09ffb7b569c17bff9572ab56a..ab457e8182cffade0934f233d9fd08a9edecacb8 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt @@ -6,6 +6,7 @@ import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig +import de.rki.coronawarnapp.appconfig.LogUploadConfig import de.rki.coronawarnapp.appconfig.SurveyConfig import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe @@ -28,6 +29,7 @@ class ConfigParserTest : BaseTest() { @MockK lateinit var exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper @MockK lateinit var surveyConfigMapper: SurveyConfig.Mapper @MockK lateinit var analyticsConfigMapper: AnalyticsConfig.Mapper + @MockK lateinit var logUploadConfigMapper: LogUploadConfig.Mapper private val appConfig171 = File("src/test/resources/appconfig_1_7_1.bin") private val appConfig180 = File("src/test/resources/appconfig_1_8_0.bin") @@ -42,6 +44,7 @@ class ConfigParserTest : BaseTest() { every { exposureWindowRiskCalculationConfigMapper.map(any()) } returns mockk() every { surveyConfigMapper.map(any()) } returns mockk() every { analyticsConfigMapper.map(any()) } returns mockk() + every { logUploadConfigMapper.map(any()) } returns mockk() appConfig171.exists() shouldBe true appConfig180.exists() shouldBe true @@ -53,7 +56,8 @@ class ConfigParserTest : BaseTest() { exposureDetectionConfigMapper = exposureDetectionConfigMapper, exposureWindowRiskCalculationConfigMapper = exposureWindowRiskCalculationConfigMapper, surveyConfigMapper = surveyConfigMapper, - analyticsConfigMapper = analyticsConfigMapper + analyticsConfigMapper = analyticsConfigMapper, + logUploadConfigMapper = logUploadConfigMapper ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7fec96f255ee4de159f184d69c4b8e8426a9eeaa --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/BugReportSettingsTest.kt @@ -0,0 +1,75 @@ +package de.rki.coronawarnapp.bugreporting + +import android.content.Context +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.UploadHistory +import de.rki.coronawarnapp.util.serialization.SerializationModule +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.extensions.shouldMatchJson +import testhelpers.preferences.MockSharedPreferences + +class BugReportSettingsTest : BaseTest() { + @MockK lateinit var context: Context + lateinit var preferences: MockSharedPreferences + + private val baseGson = SerializationModule().baseGson() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + preferences = MockSharedPreferences() + every { context.getSharedPreferences("bugreporting_localdata", Context.MODE_PRIVATE) } returns preferences + } + + fun createInstance() = BugReportingSettings( + context = context, + gson = baseGson + ) + + @Test + fun `upload history is empty by default`() { + val instance = createInstance() + preferences.dataMapPeek.isEmpty() shouldBe true + instance.uploadHistory.value shouldBe UploadHistory() + } + + @Test + fun `upload history save and load`() { + var expectedData: UploadHistory? = null + val instance = createInstance() + instance.uploadHistory.update { + it.copy( + logs = listOf( + LogUpload(id = "id1", uploadedAt = Instant.parse("2021-02-01T15:00:00.000Z")), + LogUpload(id = "id2", uploadedAt = Instant.parse("2021-02-02T15:00:00.000Z")) + ) + ).also { + expectedData = it + } + } + + preferences.dataMapPeek["upload.history"] as String shouldMatchJson """ + { + "logs": [ + { + "id": "id1", + "uploadedAt": 1612191600000 + }, + { + "id": "id2", + "uploadedAt": 1612278000000 + } + ] + } + """ + + instance.uploadHistory.value shouldBe expectedData + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerTest.kt index 9824c5645efe5e565314e6df7906e72f33644a73..61dbd80eafbcd892ac79d14ba4b32cd4f0eb0b6a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerTest.kt @@ -3,24 +3,28 @@ package de.rki.coronawarnapp.bugreporting.debuglog import android.app.Application import dagger.Lazy import de.rki.coronawarnapp.bugreporting.censors.RegistrationTokenCensor +import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebugLogTree import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.di.ApplicationComponent import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.mockk.MockKAnnotations +import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockkObject -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseIOTest +import testhelpers.coroutines.test import testhelpers.logging.JUnitTree import timber.log.Timber import java.io.File -import kotlin.random.Random +@Suppress("BlockingMethodInNonBlockingContext") class DebugLoggerTest : BaseIOTest() { @MockK lateinit var application: Application @@ -30,7 +34,6 @@ class DebugLoggerTest : BaseIOTest() { private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!) private val cacheDir = File(testDir, "cache") private val debugLogDir = File(cacheDir, "debuglog") - private val sharedDir = File(debugLogDir, "shared") private val runningLog = File(debugLogDir, "debug.log") private val triggerFile = File(debugLogDir, "debug.trigger") @@ -48,22 +51,27 @@ class DebugLoggerTest : BaseIOTest() { val logger = arg<DebugLogger>(0) logger.bugCensors = Lazy { listOf(registrationTokenCensor) } } + + coEvery { registrationTokenCensor.checkLog(any()) } returns null } @AfterEach fun teardown() { - runBlocking { DebugLogger.stop() } testDir.deleteRecursively() Timber.uprootAll() } - private fun createInstance() = DebugLogger + private fun createInstance(scope: CoroutineScope) = DebugLogger( + context = application, + scope = scope + ) @Test - fun `init does nothing if there is no trigger file`() { - createInstance().apply { - init(application) - isLogging shouldBe false + fun `init does nothing if there is no trigger file`() = runBlockingTest { + createInstance(scope = this).apply { + init() + setInjectionIsReady(component) + isLogging.value shouldBe false } runningLog.exists() shouldBe false Timber.forest().apply { @@ -73,111 +81,151 @@ class DebugLoggerTest : BaseIOTest() { } @Test - fun `init calls start if there is a trigger file`() { + fun `init calls start if there is a trigger file`() = runBlockingTest { triggerFile.parentFile?.mkdirs() triggerFile.createNewFile() - createInstance().apply { - init(application) - isLogging shouldBe true + + val instance = createInstance(scope = this).apply { + init() + setInjectionIsReady(component) + isLogging.value shouldBe true } + runningLog.exists() shouldBe true + + instance.stop() } @Test - fun `init calls start if it is a tester build`() { + fun `init calls start if it is a tester build`() = runBlockingTest { every { CWADebug.isDeviceForTestersBuild } returns true - createInstance().apply { - init(application) - isLogging shouldBe true + + val instance = createInstance(scope = this).apply { + init() + setInjectionIsReady(component) + isLogging.value shouldBe true } + runningLog.exists() shouldBe true + + instance.stop() } @Test - fun `start plants a tree and starts a logging coroutine`() { - val instance = createInstance().apply { - init(application) - isLogging shouldBe false + fun `start plants a tree and starts a logging coroutine`() = runBlockingTest { + val instance = createInstance(scope = this).apply { + init() + setInjectionIsReady(component) + isLogging.value shouldBe false } Timber.forest().none { it is DebugLogTree } shouldBe true - runBlocking { - instance.start() - Timber.forest().single { it is DebugLogTree } shouldNotBe null - } + instance.start() + Timber.forest().single { it is DebugLogTree } shouldNotBe null + + instance.stop() + Timber.forest().none { it is DebugLogTree } shouldBe true } @Test - fun `multiple start have no effect`() { - val instance = createInstance().apply { - init(application) - isLogging shouldBe false + fun `multiple start have no effect`() = runBlockingTest { + val instance = createInstance(scope = this).apply { + init() + setInjectionIsReady(component) + isLogging.value shouldBe false } Timber.forest().none { it is DebugLogTree } shouldBe true - File(sharedDir, "1").apply { - parentFile?.mkdirs() - appendBytes(Random.nextBytes(10)) - } + instance.start() + instance.start() + instance.start() - runBlocking { - instance.start() - instance.start() - instance.start() + Timber.forest().single { it is DebugLogTree } shouldNotBe null - Timber.forest().single { it is DebugLogTree } shouldNotBe null - sharedDir.listFiles()!!.size shouldBe 1 + instance.stop() + instance.stop() - instance.stop() - instance.stop() - - Timber.forest().none { it is DebugLogTree } shouldBe true - DebugLogger.isLogging shouldBe false - sharedDir.listFiles()!!.size shouldBe 0 - } + Timber.forest().none { it is DebugLogTree } shouldBe true + instance.isLogging.value shouldBe false } @Test - fun `stop cancels the coroutine and uproots the tree and deletes any logs`() { - val instance = createInstance().apply { - init(application) - isLogging shouldBe false + fun `stop cancels the coroutine and uproots the tree and deletes any logs`() = runBlockingTest { + val instance = createInstance(scope = this).apply { + init() + setInjectionIsReady(component) + isLogging.value shouldBe false } Timber.forest().none { it is DebugLogTree } shouldBe true - runBlocking { - instance.start() - Timber.forest().single { it is DebugLogTree } shouldNotBe null + instance.start() + Timber.forest().single { it is DebugLogTree } shouldNotBe null - instance.stop() - Timber.forest().none { it is DebugLogTree } shouldBe true - DebugLogger.isLogging shouldBe false + instance.stop() + Timber.forest().none { it is DebugLogTree } shouldBe true + instance.isLogging.value shouldBe false - runningLog.exists() shouldBe false - } + runningLog.exists() shouldBe false } @Test - fun `log size returns current logfile size`() { - runningLog.parentFile?.mkdirs() - runningLog.appendBytes(Random.nextBytes(22)) - createInstance().apply { - init(application) - getLogSize() shouldBe 22 + fun `logwriter is setup and used`() = runBlockingTest { + val instance = createInstance(scope = this).apply { + init() + setInjectionIsReady(component) + } + + runBlockingTest { + instance.start() + + Timber.tag("Tag123").v("Message456") + advanceTimeBy(2000L) + + runningLog.readLines().last().substring(26) shouldBe """ + V/Tag123: Message456 + """.trimIndent() + + instance.stop() + advanceUntilIdle() } } @Test - fun `shared size aggregates shared folder size`() { - sharedDir.mkdirs() - File(sharedDir, "1").apply { appendBytes(Random.nextBytes(10)) } - File(sharedDir, "2").apply { appendBytes(Random.nextBytes(15)) } - createInstance().apply { - init(application) - getShareSize() shouldBe 25 + fun `low storage state is forwarded`() = runBlockingTest { + val instance = createInstance(scope = this).apply { + init() + setInjectionIsReady(component) } + + val testCollector = instance.logState.test(startOnScope = this) + + advanceUntilIdle() + testCollector.latestValue shouldBe LogState( + isLogging = false, + isLowStorage = false, + logSize = 0L + ) + + instance.start() + advanceUntilIdle() + testCollector.latestValue shouldBe LogState( + isLogging = true, + isLowStorage = false, + logSize = 78L + ) + + instance.stop() + advanceUntilIdle() + + testCollector.latestValue shouldBe LogState( + isLogging = false, + isLowStorage = false, + logSize = 0L + ) + + testCollector.cancel() } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExportTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExportTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..d7716d1931e92db753827a4b2983257ce7bcc9c7 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/export/SAFLogExportTest.kt @@ -0,0 +1,72 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.export + +import android.content.ContentResolver +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import timber.log.Timber +import java.io.File + +class SAFLogExportTest : BaseIOTest() { + + @MockK lateinit var contentResolver: ContentResolver + + private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!) + private val zipFile = File(testDir, "logfile.zip") + private val uriFakeFile = File(testDir, "urifakefile") + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + testDir.mkdirs() + testDir.exists() shouldBe true + + every { contentResolver.openOutputStream(any()) } answers { + uriFakeFile.outputStream() + } + } + + @AfterEach + fun teardown() { + testDir.deleteRecursively() + Timber.uprootAll() + } + + private fun createInstance() = SAFLogExport() + + @Test + fun `request creation and write`() { + val instance = createInstance() + + zipFile.createNewFile() + zipFile.exists() shouldBe true + zipFile.writeText("testcontent") + + val snapshot = LogSnapshotter.Snapshot(zipFile) + val request = instance.createSAFRequest(snapshot) + request.snapshot shouldBe snapshot + + request.storeSnapshot(contentResolver, mockk()) + zipFile.exists() shouldBe false + uriFakeFile.readText() shouldBe "testcontent" + } + + @Test + fun `new requests increase id`() { + val instance = createInstance() + instance.createSAFRequest(mockk()).id shouldBe 2 + instance.getRequest(2) shouldNotBe null + instance.createSAFRequest(mockk()).id shouldBe 3 + instance.getRequest(3) shouldNotBe null + instance.getRequest(4) shouldBe null + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLogStorageCheckTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLogStorageCheckTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..44a2d6e19db22d8fc66b3051676818880435a606 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/DebugLogStorageCheckTest.kt @@ -0,0 +1,101 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.internal + +import de.rki.coronawarnapp.bugreporting.debuglog.LogLine +import io.kotest.matchers.shouldBe +import io.mockk.Called +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.slot +import io.mockk.verify +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import java.io.File + +class DebugLogStorageCheckTest : BaseTest() { + + @MockK lateinit var targetPath: File + @MockK lateinit var logWriter: LogWriter + + private var currentTime: Long = 5001L + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { targetPath.usableSpace } returns 250 * 1000 * 1024L + every { logWriter.write(any()) } just Runs + } + + @AfterEach + fun teardown() { + } + + private fun createInstance() = DebugLogStorageCheck( + targetPath = targetPath, + timeProvider = { currentTime }, + logWriter = logWriter + ) + + @Test + fun `normal not low storage case`() { + val instance = createInstance() + instance.isLowStorage() shouldBe false + + verify { logWriter wasNot Called } + } + + @Test + fun `on errors we print it but do expect low storage`() { + val unexpectedError = Exception("ಠ_ಠ") + every { targetPath.usableSpace } throws unexpectedError + + val logSlot = slot<LogLine>() + every { logWriter.write(capture(logSlot)) } just Runs + + val instance = createInstance() + instance.isLowStorage() shouldBe true + + logSlot.captured.throwable shouldBe unexpectedError + } + + @Test + fun `low storage default is 200MB`() { + every { targetPath.usableSpace } returns 199 * 1000 * 1024L + val instance = createInstance() + instance.isLowStorage() shouldBe true + instance.isLowStorage() shouldBe true + + currentTime += 60 * 1000L + instance.isLowStorage() shouldBe true + + // We only write the warning once + verify(exactly = 1) { logWriter.write(any()) } + } + + @Test + fun `checks happen at most every 5 seconds`() { + val instance = createInstance() + instance.isLowStorage() shouldBe false + + every { targetPath.usableSpace } returns 1024L + + instance.isLowStorage() shouldBe false + + verify(exactly = 1) { targetPath.usableSpace } + + currentTime += 5000L + + instance.isLowStorage() shouldBe true + + every { targetPath.usableSpace } returns 250 * 1000 * 1024L + + instance.isLowStorage() shouldBe true + + verify(exactly = 2) { targetPath.usableSpace } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotterTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotterTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b4ccfd775f08b740cb3f13e00e4ee8e1b7c3384f --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogSnapshotterTest.kt @@ -0,0 +1,76 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.internal + +import android.content.Context +import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import timber.log.Timber +import java.io.File + +class LogSnapshotterTest : BaseIOTest() { + + @MockK lateinit var context: Context + @MockK lateinit var debugLogger: DebugLogger + @MockK lateinit var timeStamper: TimeStamper + + private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!) + private val cacheDir = File(testDir, "cache") + private val runningLogFake = File(testDir, "running.log") + + private val snapshotDir = File(cacheDir, "debuglog_snapshots") + private val expectedSnapshot = File(snapshotDir, "CWA Log 1970-01-01 00:00:00.000.zip") + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { context.cacheDir } returns cacheDir + + testDir.mkdirs() + testDir.exists() shouldBe true + + every { debugLogger.runningLog } returns runningLogFake + every { timeStamper.nowUTC } returns Instant.EPOCH + + runningLogFake.parentFile!!.mkdirs() + runningLogFake.writeText("1 Doge = 1 Doge") + } + + @AfterEach + fun teardown() { + testDir.deleteRecursively() + Timber.uprootAll() + } + + private fun createInstance() = LogSnapshotter( + context = context, + debugLogger = debugLogger, + timeStamper = timeStamper + ) + + @Test + fun `normal snapshot`() { + val instance = createInstance() + + val snapshot = instance.snapshot() + + snapshot.apply { + path shouldBe expectedSnapshot + path.exists() shouldBe true + path.length() shouldBe 197L + } + + snapshot.apply { + delete() + snapshot.path.exists() shouldBe false + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogWriterTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogWriterTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..19120d55091bae4e73d55d7e2bd18270d5a9540f --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/internal/LogWriterTest.kt @@ -0,0 +1,48 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.internal + +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import timber.log.Timber +import java.io.File +import kotlin.random.Random + +class LogWriterTest : BaseIOTest() { + + private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!) + private val logFile = File(testDir, "logfile.log") + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + testDir.mkdirs() + testDir.exists() shouldBe true + } + + @AfterEach + fun teardown() { + testDir.deleteRecursively() + Timber.uprootAll() + } + + private fun createInstance() = LogWriter( + logFile = logFile + ) + + @Test + fun `log size returns current logfile size`() = runBlockingTest { + logFile.parentFile?.mkdirs() + logFile.appendBytes(Random.nextBytes(22)) + createInstance().apply { + setup() + logSize.value shouldBe 22L + teardown() + logSize.value shouldBe 0L + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploaderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploaderTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..83d4946117e51104c6b3cfc5af4e2f32fed5c492 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/SnapshotUploaderTest.kt @@ -0,0 +1,100 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload + +import de.rki.coronawarnapp.bugreporting.BugReportingSettings +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.UploadHistory +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadServer +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadAuthorizer +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadOtp +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.preferences.mockFlowPreference +import java.io.IOException + +class SnapshotUploaderTest : BaseTest() { + + @MockK lateinit var snapshotter: LogSnapshotter + @MockK lateinit var uploadServer: LogUploadServer + @MockK lateinit var authorizer: LogUploadAuthorizer + @MockK lateinit var bugReportingSettings: BugReportingSettings + @MockK lateinit var snapshot: LogSnapshotter.Snapshot + + private val logUploadOtp = LogUploadOtp( + otp = "otp", + expirationDate = Instant.EPOCH + ) + + private val uploadHistoryPref = mockFlowPreference(UploadHistory()) + + private val expectedLogUpload = LogUpload( + id = "123e4567-e89b-12d3-a456-426652340000", + uploadedAt = Instant.parse("2020-08-20T23:00:00.000Z") + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { bugReportingSettings.uploadHistory } returns uploadHistoryPref + + coEvery { authorizer.getAuthorizedOTP(otp = any()) } returns logUploadOtp + coEvery { snapshotter.snapshot() } returns snapshot + coEvery { uploadServer.uploadLog(logUploadOtp, snapshot) } returns expectedLogUpload + + every { snapshot.delete() } returns true + } + + private fun createInstance() = SnapshotUploader( + snapshotter = snapshotter, + uploadServer = uploadServer, + authorizer = authorizer, + bugReportingSettings = bugReportingSettings + ) + + @Test + fun `upload a snapshot`() = runBlockingTest { + val instance = createInstance() + instance.uploadSnapshot() shouldBe expectedLogUpload + + uploadHistoryPref.value shouldBe UploadHistory(logs = listOf(expectedLogUpload)) + } + + @Test + fun `snapshots are deleted on errors too`() = runBlockingTest { + coEvery { uploadServer.uploadLog(logUploadOtp, snapshot) } throws IOException() + + val instance = createInstance() + + shouldThrow<IOException> { + instance.uploadSnapshot() + } + verify { snapshot.delete() } + } + + @Test + fun `upload history is capped at 10`() = runBlockingTest { + val existingEntries = (1..10L).map { LogUpload(id = "$it", Instant.ofEpochMilli(it)) } + uploadHistoryPref.update { UploadHistory(logs = existingEntries) } + + val instance = createInstance() + instance.uploadSnapshot() shouldBe expectedLogUpload + + uploadHistoryPref.value shouldBe UploadHistory( + logs = existingEntries.subList( + 1, + 10 + ) + listOf(expectedLogUpload) + ) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..3fd06b0bdf5ba30d9f165014f305e0fde8db4408 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadApiTest.kt @@ -0,0 +1,132 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server + +import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule +import de.rki.coronawarnapp.environment.download.DownloadCDNModule +import de.rki.coronawarnapp.exception.http.CwaWebException +import de.rki.coronawarnapp.http.HttpModule +import de.rki.coronawarnapp.util.files.determineMimeType +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import kotlinx.coroutines.runBlocking +import okhttp3.ConnectionSpec +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import testhelpers.extensions.toJsonResponse +import java.io.File +import java.util.concurrent.TimeUnit + +class LogUploadApiTest : BaseIOTest() { + + private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) + private lateinit var webServer: MockWebServer + private lateinit var serverAddress: String + private val otp = "15cff19f-af26-41bc-94f2-c1a65075e894" + private val expectedId = "11111111-af26-41bc-94f2-000000000000" + private val expectedHash = "22222222-af26-41bc-94f2-000000000000" + private val testSnapshotFile = File(testDir, "snapshot.zip") + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + webServer = MockWebServer() + webServer.start() + serverAddress = "http://${webServer.hostName}:${webServer.port}" + + testDir.mkdirs() + testSnapshotFile.writeText("We needed this months ago.") + testSnapshotFile.exists() shouldBe true + } + + @AfterEach + fun teardown() { + webServer.shutdown() + testDir.deleteRecursively() + } + + private fun createAPI(): LogUploadApiV1 { + val httpModule = HttpModule() + val defaultHttpClient = httpModule.defaultHttpClient() + val gsonConverterFactory = httpModule.provideGSONConverter() + val protoConverterFactory = httpModule.provideProtoConverter() + + val cdnHttpClient = DownloadCDNModule() + .cdnHttpClient(defaultHttpClient) + .newBuilder() + .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS)) + .build() + + return BugReportingSharedModule().logUploadApi( + client = cdnHttpClient, + url = serverAddress, + gsonConverterFactory = gsonConverterFactory, + protoConverterFactory = protoConverterFactory + ) + } + + @Test + fun `happy upload`(): Unit = runBlocking { + """ + { + id : "$expectedId", + hash: "$expectedHash" + } + """.toJsonResponse().apply { webServer.enqueue(this) } + + val api = createAPI() + api.uploadLog( + otp = otp, + logZip = MultipartBody.Part.createFormData( + name = "file", + filename = testSnapshotFile.name, + body = testSnapshotFile.asRequestBody(testSnapshotFile.determineMimeType().toMediaType()) + ) + ) + + webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { + path shouldBe "/api/logs" + val boundary = this.headers["Content-Type"]!!.replace("multipart/form-data; boundary=", "") + body.readUtf8().replace("\r\n", "\n") shouldBe """ + --$boundary + Content-Disposition: form-data; name="file"; filename="snapshot.zip" + Content-Type: application/zip + Content-Length: 26 + + We needed this months ago. + --$boundary-- + + """.trimIndent() + } + } + + @Test + fun `server returns 500`(): Unit = runBlocking { + """ + { + id : "$expectedId", + hash: "$expectedHash" + } + """.toJsonResponse().apply { webServer.enqueue(MockResponse().setResponseCode(500)) } + + val api = createAPI() + + shouldThrow<CwaWebException> { + api.uploadLog( + otp = otp, + logZip = MultipartBody.Part.createFormData( + name = "file", + filename = testSnapshotFile.name, + body = testSnapshotFile.asRequestBody(testSnapshotFile.determineMimeType().toMediaType()) + ) + ) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ecd46ea090b546a1f439c070fae6633847173fbb --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/LogUploadServerTest.kt @@ -0,0 +1,66 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server + +import de.rki.coronawarnapp.bugreporting.debuglog.internal.LogSnapshotter +import de.rki.coronawarnapp.bugreporting.debuglog.upload.history.LogUpload +import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth.LogUploadOtp +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.runBlocking +import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import java.io.File + +class LogUploadServerTest : BaseIOTest() { + + private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) + @MockK private lateinit var timeStamper: TimeStamper + @MockK private lateinit var uploadApiV1: LogUploadApiV1 + private val uploadOtp = LogUploadOtp( + otp = "1", + expirationDate = Instant.EPOCH.plus(Duration.standardDays(1)) + ) + private val snapshot = LogSnapshotter.Snapshot( + path = File(testDir, "snapshot.zip") + ) + private val uploadResponse = LogUploadApiV1.UploadResponse( + id = "123", + hash = null, + errorCode = null + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + coEvery { uploadApiV1.uploadLog(any(), any()) } returns uploadResponse + every { timeStamper.nowUTC } returns Instant.ofEpochMilli(1234) + } + + @AfterEach + fun teardown() { + testDir.deleteRecursively() + } + + fun createInstance() = LogUploadServer( + timeStamper = timeStamper, + uploadApiProvider = { uploadApiV1 } + ) + + @Test + fun `log upload`() = runBlocking { + val instance = createInstance() + + instance.uploadLog(uploadOtp = uploadOtp, snapshot = snapshot) shouldBe LogUpload( + id = "123", + uploadedAt = Instant.ofEpochMilli(1234) + ) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..46c4a59e9e75e3aca26e6b4b81030744f78e27e1 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthApiTest.kt @@ -0,0 +1,104 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth + +import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule +import de.rki.coronawarnapp.environment.download.DownloadCDNModule +import de.rki.coronawarnapp.exception.http.CwaWebException +import de.rki.coronawarnapp.http.HttpModule +import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtp +import de.rki.coronawarnapp.server.protocols.internal.ppdd.ElsOtpRequestAndroid +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import kotlinx.coroutines.runBlocking +import okhttp3.ConnectionSpec +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.extensions.toJsonResponse +import java.util.concurrent.TimeUnit + +class LogUploadAuthApiTest : BaseTest() { + + private lateinit var webServer: MockWebServer + private lateinit var serverAddress: String + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + webServer = MockWebServer() + webServer.start() + serverAddress = "http://${webServer.hostName}:${webServer.port}" + } + + @AfterEach + fun teardown() { + webServer.shutdown() + } + + private fun createAPI(): LogUploadAuthApiV1 { + val httpModule = HttpModule() + val defaultHttpClient = httpModule.defaultHttpClient() + val gsonConverterFactory = httpModule.provideGSONConverter() + val protoConverterFactory = httpModule.provideProtoConverter() + + val cdnHttpClient = DownloadCDNModule() + .cdnHttpClient(defaultHttpClient) + .newBuilder() + .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS)) + .build() + + return BugReportingSharedModule().logUploadAuthApi( + client = cdnHttpClient, + url = serverAddress, + gsonConverterFactory = gsonConverterFactory, + protoConverterFactory = protoConverterFactory + ) + } + + @Test + fun `test auth`(): Unit = runBlocking { + """ + { + expirationDate : "2020-08-20T14:00:00.000Z" + } + """.toJsonResponse().apply { webServer.enqueue(this) } + + val elsPayload = ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid.newBuilder() + .setPayload(ElsOtp.ELSOneTimePassword.newBuilder().setOtp("15cff19f-af26-41bc-94f2-c1a65075e894")) + .setAuthentication(PpacAndroid.PPACAndroid.newBuilder().setSafetyNetJws("abc").setSalt("def")) + .build() + + val api = createAPI() + api.authOTP(requestBody = elsPayload) + + webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { + path shouldBe "/version/v1/android/log" + body.readByteArray() shouldBe elsPayload.toByteArray() + } + } + + @Test + fun `server returns 500`(): Unit = runBlocking { + """ + { + errorCode: "Nope" + } + """.toJsonResponse().apply { webServer.enqueue(MockResponse().setResponseCode(500)) } + + val elsPayload = ElsOtpRequestAndroid.ELSOneTimePasswordRequestAndroid.newBuilder() + .setPayload(ElsOtp.ELSOneTimePassword.newBuilder().setOtp("15cff19f-af26-41bc-94f2-c1a65075e894")) + .setAuthentication(PpacAndroid.PPACAndroid.newBuilder().setSafetyNetJws("abc").setSalt("def")) + .build() + + val api = createAPI() + + shouldThrow<CwaWebException> { + api.authOTP(requestBody = elsPayload) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b2a8b3c9fa39ca76780f3ce666666641dd33f53b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/upload/server/auth/LogUploadAuthorizerTest.kt @@ -0,0 +1,85 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.upload.server.auth + +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.LogUploadConfig +import de.rki.coronawarnapp.appconfig.SafetyNetRequirements +import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.slot +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import java.io.File +import java.util.UUID + +class LogUploadAuthorizerTest : BaseIOTest() { + + private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) + + @MockK private lateinit var authApiV1: LogUploadAuthApiV1 + @MockK private lateinit var deviceAttestation: DeviceAttestation + @MockK private lateinit var configProvider: AppConfigProvider + @MockK private lateinit var attestationResult: DeviceAttestation.Result + @MockK private lateinit var configData: ConfigData + @MockK private lateinit var logUploadConfig: LogUploadConfig + @MockK private lateinit var safetyNetRequirements: SafetyNetRequirements + + private val attestationRequestSlot = slot<DeviceAttestation.Request>() + + private val uploadResponse = LogUploadAuthApiV1.AuthResponse( + expirationDate = Instant.EPOCH.toString() + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { configData.logUpload } returns logUploadConfig + every { logUploadConfig.safetyNetRequirements } returns safetyNetRequirements + + coEvery { authApiV1.authOTP(any()) } returns uploadResponse + coEvery { deviceAttestation.attest(capture(attestationRequestSlot)) } returns attestationResult + attestationResult.apply { + every { requirePass(safetyNetRequirements) } just Runs + every { accessControlProtoBuf } returns PpacAndroid.PPACAndroid.getDefaultInstance() + } + coEvery { configProvider.currentConfig } returns flowOf(configData) + } + + @AfterEach + fun teardown() { + testDir.deleteRecursively() + } + + fun createInstance() = LogUploadAuthorizer( + authApiProvider = { authApiV1 }, + deviceAttestation = deviceAttestation, + configProvider = configProvider + ) + + @Test + fun `otp generation`() = runBlockingTest { + val expectedOtp = UUID.fromString("15cff19f-af26-41bc-94f2-c1a65075e894") + val instance = createInstance() + + instance.getAuthorizedOTP(otp = expectedOtp).apply { + otp shouldBe expectedOtp.toString() + expirationDate shouldBe Instant.EPOCH + } + + attestationRequestSlot.captured.configData shouldBe configData + attestationRequestSlot.captured.checkDeviceTime shouldBe false + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt index 45debcbe03e1a8677e5a02cbeb8a92856c899910..fae3d1cedacb466af3cbfdcc9f4cea702038a6f1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/CWASafetyNetTest.kt @@ -17,6 +17,8 @@ import de.rki.coronawarnapp.util.gplay.GoogleApiVersion import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.Called import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -257,9 +259,35 @@ class CWASafetyNetTest : BaseTest() { exception.type shouldBe SafetyNetException.Type.TIME_SINCE_ONBOARDING_UNVERIFIED } + @Test + fun `device time checks can be disabled via request`() = runBlockingTest { + every { appConfigData.deviceTimeState } returns ConfigData.DeviceTimeState.ASSUMED_CORRECT + every { timeStamper.nowUTC } returns Instant.EPOCH + + val request = TestAttestationRequest( + "Computer says no.".toByteArray(), + checkDeviceTime = false + ) + createInstance().attest(request) shouldNotBe null + } + + @Test + fun `the request can contain a app config that should be used`() = runBlockingTest { + val request = TestAttestationRequest( + "Computer says no.".toByteArray(), + configData = appConfigData + ) + createInstance().attest(request) shouldNotBe null + + coVerify { appConfigProvider wasNot Called } + } + data class TestAttestationRequest( - override val scenarioPayload: ByteArray + override val scenarioPayload: ByteArray, + override val configData: ConfigData? = null, + override val checkDeviceTime: Boolean = true ) : DeviceAttestation.Request { + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestationTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..0e637c3c5dcff600ac28730a5140acd691a35d45 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/safetynet/DeviceAttestationTest.kt @@ -0,0 +1,17 @@ +package de.rki.coronawarnapp.datadonation.safetynet + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class DeviceAttestationTest : BaseTest() { + + @Test + fun `request has sane defaults`() { + val impl = object : DeviceAttestation.Request { + override val scenarioPayload: ByteArray = "".toByteArray() + } + impl.checkDeviceTime shouldBe true + impl.configData shouldBe null + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt index d3eb6d4aed8530a325264a5c608bd4e91fc4c2ca..62534f7290aa26c8cc7948ecfc0b71cd7b3166f1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt @@ -65,7 +65,8 @@ class EnvironmentSetupTest : BaseTest() { verificationCdnUrl shouldBe "https://verification-${env.rawKey}" appConfigVerificationKey shouldBe "12345678-${env.rawKey}" safetyNetApiKey shouldBe "placeholder-${env.rawKey}" - dataDonationCdnUrl shouldBe "https://placeholder-${env.rawKey}" + dataDonationCdnUrl shouldBe "https://datadonation-${env.rawKey}" + logUploadServerUrl shouldBe "https://logupload-${env.rawKey}" } } } @@ -121,8 +122,9 @@ class EnvironmentSetupTest : BaseTest() { EnvironmentSetup.EnvKey.DOWNLOAD.rawKey shouldBe "DOWNLOAD_CDN_URL" EnvironmentSetup.EnvKey.VERIFICATION_KEYS.rawKey shouldBe "PUB_KEYS_SIGNATURE_VERIFICATION" EnvironmentSetup.EnvKey.DATA_DONATION.rawKey shouldBe "DATA_DONATION_CDN_URL" + EnvironmentSetup.EnvKey.LOG_UPLOAD.rawKey shouldBe "LOG_UPLOAD_SERVER_URL" EnvironmentSetup.EnvKey.SAFETYNET_API_KEY.rawKey shouldBe "SAFETYNET_API_KEY" - EnvironmentSetup.EnvKey.values().size shouldBe 7 + EnvironmentSetup.EnvKey.values().size shouldBe 8 } companion object { @@ -141,7 +143,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-PROD", "DOWNLOAD_CDN_URL": "https://download-PROD", "VERIFICATION_CDN_URL": "https://verification-PROD", - "DATA_DONATION_CDN_URL": "https://placeholder-PROD", + "DATA_DONATION_CDN_URL": "https://datadonation-PROD", + "LOG_UPLOAD_SERVER_URL": "https://logupload-PROD", "SAFETYNET_API_KEY": "placeholder-PROD", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-PROD" }, @@ -150,7 +153,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-DEV", "DOWNLOAD_CDN_URL": "https://download-DEV", "VERIFICATION_CDN_URL": "https://verification-DEV", - "DATA_DONATION_CDN_URL": "https://placeholder-DEV", + "DATA_DONATION_CDN_URL": "https://datadonation-DEV", + "LOG_UPLOAD_SERVER_URL": "https://logupload-DEV", "SAFETYNET_API_KEY": "placeholder-DEV", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-DEV" }, @@ -159,7 +163,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-INT", "DOWNLOAD_CDN_URL": "https://download-INT", "VERIFICATION_CDN_URL": "https://verification-INT", - "DATA_DONATION_CDN_URL": "https://placeholder-INT", + "DATA_DONATION_CDN_URL": "https://datadonation-INT", + "LOG_UPLOAD_SERVER_URL": "https://logupload-INT", "SAFETYNET_API_KEY": "placeholder-INT", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-INT" }, @@ -168,7 +173,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-WRU", "DOWNLOAD_CDN_URL": "https://download-WRU", "VERIFICATION_CDN_URL": "https://verification-WRU", - "DATA_DONATION_CDN_URL": "https://placeholder-WRU", + "DATA_DONATION_CDN_URL": "https://datadonation-WRU", + "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU", "SAFETYNET_API_KEY": "placeholder-WRU", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU" }, @@ -177,7 +183,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-WRU-XD", "DOWNLOAD_CDN_URL": "https://download-WRU-XD", "VERIFICATION_CDN_URL": "https://verification-WRU-XD", - "DATA_DONATION_CDN_URL": "https://placeholder-WRU-XD", + "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XD", + "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XD", "SAFETYNET_API_KEY": "placeholder-WRU-XD", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XD" }, @@ -186,7 +193,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-WRU-XA", "DOWNLOAD_CDN_URL": "https://download-WRU-XA", "VERIFICATION_CDN_URL": "https://verification-WRU-XA", - "DATA_DONATION_CDN_URL": "https://placeholder-WRU-XA", + "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XA", + "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XA", "SAFETYNET_API_KEY": "placeholder-WRU-XA", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XA" }, @@ -195,7 +203,8 @@ class EnvironmentSetupTest : BaseTest() { "SUBMISSION_CDN_URL": "https://submission-LOCAL", "DOWNLOAD_CDN_URL": "https://download-LOCAL", "VERIFICATION_CDN_URL": "https://verification-LOCAL", - "DATA_DONATION_CDN_URL": "https://placeholder-LOCAL", + "DATA_DONATION_CDN_URL": "https://datadonation-LOCAL", + "LOG_UPLOAD_SERVER_URL": "https://logupload-LOCAL", "SAFETYNET_API_KEY": "placeholder-LOCAL", "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-LOCAL" } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModuleTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModuleTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ef3488e935830a29da318b1712d7c3ae34d36c52 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/bugreporting/BugReportingServerModuleTest.kt @@ -0,0 +1,50 @@ +package de.rki.coronawarnapp.environment.bugreporting + +import de.rki.coronawarnapp.environment.EnvironmentSetup +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest + +class BugReportingServerModuleTest : BaseIOTest() { + + private val validUrl = "https://logupload" + private val inValidUrl = "http://invalid" + + @MockK lateinit var environmentSetup: EnvironmentSetup + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + } + + private fun createModule() = BugReportingServerModule() + + @Test + fun `sideeffect free instantiation`() { + shouldNotThrowAny { + createModule() + } + } + + @Test + fun `valid downloaded URL comes from environment`() { + every { environmentSetup.logUploadServerUrl } returns validUrl + val module = createModule() + module.provideBugReportingServerUrl(environmentSetup) shouldBe validUrl + } + + @Test + fun `invalid downloaded URL comes from environment`() { + every { environmentSetup.logUploadServerUrl } returns inValidUrl + val module = createModule() + shouldThrowAny { + module.provideBugReportingServerUrl(environmentSetup) shouldBe validUrl + } + } +} diff --git a/Corona-Warn-App/src/test/java/testhelpers/extensions/JsonExtensions.kt b/Corona-Warn-App/src/test/java/testhelpers/extensions/JsonExtensions.kt index bc1304959208fcebe18ef38a3c8fb7ebfaafd631..f2cd8033abfb526121a95cba5ae241ebf8e715d7 100644 --- a/Corona-Warn-App/src/test/java/testhelpers/extensions/JsonExtensions.kt +++ b/Corona-Warn-App/src/test/java/testhelpers/extensions/JsonExtensions.kt @@ -1,7 +1,12 @@ package testhelpers.extensions import com.google.gson.Gson +import com.google.gson.GsonBuilder import com.google.gson.JsonObject +import io.kotest.assertions.assertionCounter +import io.kotest.assertions.collectOrThrow +import io.kotest.assertions.eq.eq +import io.kotest.assertions.errorCollector import okhttp3.mockwebserver.MockResponse fun String.toComparableJson() = try { @@ -10,4 +15,21 @@ fun String.toComparableJson() = try { throw IllegalArgumentException("'$this' wasn't valid JSON") } +fun String.toComparableJsonPretty(): String = try { + val gson = GsonBuilder().setPrettyPrinting().create() + val obj = gson.fromJson(this, JsonObject::class.java) + gson.toJson(obj) +} catch (e: Exception) { + throw IllegalArgumentException("'$this' wasn't valid JSON") +} + +@Suppress("UNCHECKED_CAST") +infix fun String.shouldMatchJson(expected: String) { + val actualPretty = this.toComparableJsonPretty() + val expectedPretty = expected.toComparableJsonPretty() + + assertionCounter.inc() + eq(actualPretty, expectedPretty)?.let(errorCollector::collectOrThrow) +} + fun String.toJsonResponse(): MockResponse = MockResponse().setBody(this.toComparableJson()) diff --git a/prod_environments.json b/prod_environments.json index 314c1c69483fa3a3f609a7913150a6b449efa95b..9cd0aaa9c3de612c39e495bb2f90885b167fb7cc 100644 --- a/prod_environments.json +++ b/prod_environments.json @@ -5,6 +5,7 @@ "DOWNLOAD_CDN_URL": "https://svc90.main.px.t-online.de", "VERIFICATION_CDN_URL": "https://verification.coronawarn.app", "DATA_DONATION_CDN_URL": "https://data.coronawarn.app", + "LOG_UPLOAD_SERVER_URL": "https://placeholder", "SAFETYNET_API_KEY": "placeholder", "PUB_KEYS_SIGNATURE_VERIFICATION": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" }