Skip to content
Snippets Groups Projects
Unverified Commit 838afe8b authored by Matthias Urhahn's avatar Matthias Urhahn Committed by GitHub
Browse files

Upload debug logs (EXPOSUREAPP-5032) (#2525)

* Setup environment for new LOG_UPLOAD url.

* Upload structure, WIP

* Resolve merge issue

* Packagename refactoring.

* Allow ID selection and copy to clipboard on clicking an item in the upload history.

* Log upload first draft.

* You get a test, YOU get a test, you get a test, everybody gets a test!

* Fix navigation to legal screen.

* Adress PR comments.
parent f29b75ae
No related branches found
No related tags found
No related merge requests found
Showing
with 272 additions and 38 deletions
......@@ -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)
}
......
package de.rki.coronawarnapp.appconfig
import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper
interface LogUploadConfig {
val safetyNetRequirements: SafetyNetRequirements
interface Mapper : ConfigMapper<LogUploadConfig>
}
......@@ -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
}
......@@ -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) {
......
......@@ -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,
......
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
}
......@@ -2,7 +2,7 @@ package de.rki.coronawarnapp.bugreporting
import android.content.Context
import com.google.gson.Gson
import de.rki.coronawarnapp.bugreporting.uploadhistory.UploadHistory
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
......
......@@ -2,6 +2,7 @@ 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
......@@ -9,14 +10,54 @@ import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor
import de.rki.coronawarnapp.bugreporting.censors.RegistrationTokenCensor
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() = CWADebug.debugLogger
......
package de.rki.coronawarnapp.bugreporting.debuglog.sharing
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
......@@ -12,7 +13,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SAFLogSharing @Inject constructor() {
class SAFLogExport @Inject constructor() {
private var lastId = 1
private val requestMap = mutableMapOf<Int, Request>()
......
package de.rki.coronawarnapp.bugreporting.debuglog.sharing
package de.rki.coronawarnapp.bugreporting.debuglog.internal
import android.content.Context
import dagger.Reusable
......@@ -27,8 +27,9 @@ class LogSnapshotter @Inject constructor(
*/
fun snapshot(): Snapshot {
Timber.tag(TAG).d("snapshot()")
snapshotDir.listFiles()?.forEach {
Timber.tag(TAG).w("Deleting stale snapshot: %s", it)
if (it.delete()) Timber.tag(TAG).w("Deleted stale snapshot: %s", it)
}
val now = timeStamper.nowUTC
......@@ -46,9 +47,7 @@ class LogSnapshotter @Inject constructor(
return Snapshot(path = zipFile)
}
data class Snapshot(
val path: File
) {
data class Snapshot(val path: File) {
fun delete() = path.delete()
}
......
......@@ -8,8 +8,8 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.bugreporting.BugReportingSettings
import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger
import de.rki.coronawarnapp.bugreporting.debuglog.sharing.LogSnapshotter
import de.rki.coronawarnapp.bugreporting.debuglog.sharing.SAFLogSharing
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.coroutine.DispatcherProvider
......@@ -27,7 +27,7 @@ class DebugLogViewModel @AssistedInject constructor(
private val enfClient: ENFClient,
bugReportingSettings: BugReportingSettings,
private val logSnapshotter: LogSnapshotter,
private val safLogSharing: SAFLogSharing,
private val safLogExport: SAFLogExport,
private val contentResolver: ContentResolver,
) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
......@@ -51,8 +51,8 @@ class DebugLogViewModel @AssistedInject constructor(
}.asLiveData(context = dispatcherProvider.Default)
val errorEvent = SingleLiveEvent<Throwable>()
val shareEvent = SingleLiveEvent<SAFLogSharing.Request>()
val logStoreResult = SingleLiveEvent<SAFLogSharing.Request.Result>()
val shareEvent = SingleLiveEvent<SAFLogExport.Request>()
val logStoreResult = SingleLiveEvent<SAFLogExport.Request.Result>()
fun onPrivacyButtonPress() {
routeToScreen.postValue(DebugLogNavigationEvents.NavigateToPrivacyFragment)
......@@ -82,15 +82,10 @@ class DebugLogViewModel @AssistedInject constructor(
}
}
fun onUploadLog() = launchWithProgress {
Timber.d("uploadLog()")
throw NotImplementedError("TODO")
}
fun onStoreLog() = launchWithProgress(finishProgressAction = false) {
Timber.d("storeLog()")
val snapshot = logSnapshotter.snapshot()
val shareRequest = safLogSharing.createSAFRequest(snapshot)
val shareRequest = safLogExport.createSAFRequest(snapshot)
shareEvent.postValue(shareRequest)
}
......@@ -100,7 +95,7 @@ class DebugLogViewModel @AssistedInject constructor(
return@launchWithProgress
}
val request = safLogSharing.getRequest(requestCode)
val request = safLogExport.getRequest(requestCode)
if (request == null) {
Timber.w("Unknown request with code $requestCode")
return@launchWithProgress
......
......@@ -3,10 +3,13 @@ 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
......@@ -20,12 +23,14 @@ class DebugLogUploadFragment : Fragment(R.layout.bugreporting_debuglog_upload_fr
@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)
binding.apply {
uploadDialog = LogUploadBlockingDialog(requireContext())
debugLogShareButton.setOnClickListener {
binding.apply {
uploadAction.setOnClickListener {
vm.onUploadLog()
}
......@@ -37,7 +42,23 @@ class DebugLogUploadFragment : Fragment(R.layout.bugreporting_debuglog_upload_fr
}
vm.routeToScreen.observe2(this) {
doNavigate(it)
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()
}
}
......
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.ui.SingleLiveEvent
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>()
val routeToScreen = SingleLiveEvent<NavDirections>()
fun onUploadLog() {
// TODO Implement Uploading
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() {
......
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()
}
}
}
package de.rki.coronawarnapp.bugreporting.uploadhistory.ui
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.uploadhistory.LogUpload
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>() {
......@@ -36,6 +40,23 @@ class HistoryItemAdapter : BaseAdapter<HistoryItemAdapter.CachedKeyViewHolder>()
) -> 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.")
}
}
}
}
......
package de.rki.coronawarnapp.bugreporting.uploadhistory.ui
package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history
import android.os.Bundle
import android.view.View
......
package de.rki.coronawarnapp.bugreporting.uploadhistory.ui
package de.rki.coronawarnapp.bugreporting.debuglog.ui.upload.history
import dagger.Binds
import dagger.Module
......
package de.rki.coronawarnapp.bugreporting.uploadhistory.ui
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.uploadhistory.LogUpload
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
......
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"
}
}
package de.rki.coronawarnapp.bugreporting.uploadhistory
package de.rki.coronawarnapp.bugreporting.debuglog.upload.history
import org.joda.time.Instant
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment