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 00ccf795235bae3fc73efa24865fafd46731301e..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 @@ -44,7 +44,7 @@ class DebugLogger( private var isDaggerReady = false - private val storageCheck = DebugLogStorageCheck(targetPath = debugDir, logWriter = logWriter) + val storageCheck = DebugLogStorageCheck(targetPath = debugDir, logWriter = logWriter) internal val isLogging = MutableStateFlow(false) val logState: Flow<LogState> = combine( @@ -154,7 +154,7 @@ class DebugLogger( yield() } - if (storageCheck.checkLowStorage()) return@collect + if (storageCheck.isLowStorage()) return@collect launch { // Censor data sources need a moment to know what to censor 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 index e61b0bf38c8ed5a749fca8a9f539f64f08d366e4..2e8e7aba28e5d7ff98e47bd3e462ad56739a48d4 100644 --- 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 @@ -22,9 +22,9 @@ class DebugLogStorageCheck @Inject constructor( @SuppressLint("UsableSpace") get() = targetPath.usableSpace - fun checkLowStorage(): Boolean { + fun isLowStorage(forceCheck: Boolean = false): Boolean { val now = timeProvider() - if (now - lastCheckAt < 5_000) return isLowStorage.value + if (!forceCheck && now - lastCheckAt < 5_000) return isLowStorage.value val currentSpace = try { availableSpace 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 867383a624a76ae8356d2e97322d8f7d599dadfe..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 @@ -3,16 +3,19 @@ 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 @@ -29,9 +32,11 @@ 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() } @@ -94,41 +99,48 @@ class DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), Auto } } - vm.shareEvent.observe2(this@DebugLogFragment) { - startActivityForResult(it.createIntent(), it.id) - } - - vm.logStoreResult.observe2(this) { - Toast.makeText( - requireContext(), - "TODO: Show store result dialog: ${it.storageUri}", - 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.logUploads.observe2(this@DebugLogFragment) { binding.debugLogHistoryContainer.setGone(it.logs.isEmpty()) } binding.debugLogHistoryContainer.setOnClickListener { vm.onIdHistoryPress() } - - vm.routeToScreen.observe2(this) { - when (it) { - DebugLogNavigationEvents.NavigateToPrivacyFragment -> doNavigate( - DebugLogFragmentDirections.actionDebuglogFragmentToInformationPrivacyFragment() - ) - DebugLogNavigationEvents.NavigateToUploadHistory -> doNavigate( - DebugLogFragmentDirections.actionDebuglogFragmentToLogUploadHistoryFragment() - ) - - DebugLogNavigationEvents.NavigateToShareFragment -> doNavigate( - DebugLogFragmentDirections.actionDebuglogFragmentToDebugLogUploadFragment() - ) - } - } - - vm.errorEvent.observe2(this) { - Toast.makeText(requireContext(), it.toString(), Toast.LENGTH_LONG).show() - } } override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) { @@ -138,4 +150,52 @@ class DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), Auto 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/DebugLogViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogViewModel.kt index cad3fa356498e026688e470506e56296c256ed6a..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 @@ -20,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import timber.log.Timber +import java.io.IOException class DebugLogViewModel @AssistedInject constructor( private val debugLogger: DebugLogger, @@ -33,8 +34,6 @@ class DebugLogViewModel @AssistedInject constructor( private val isActionInProgress = MutableStateFlow(false) - val routeToScreen = SingleLiveEvent<DebugLogNavigationEvents>() - val logUploads = bugReportingSettings.uploadHistory.flow .asLiveData(context = dispatcherProvider.Default) @@ -50,26 +49,31 @@ class DebugLogViewModel @AssistedInject constructor( ) }.asLiveData(context = dispatcherProvider.Default) - val errorEvent = SingleLiveEvent<Throwable>() - val shareEvent = SingleLiveEvent<SAFLogExport.Request>() - val logStoreResult = SingleLiveEvent<SAFLogExport.Request.Result>() + val events = SingleLiveEvent<Event>() fun onPrivacyButtonPress() { - routeToScreen.postValue(DebugLogNavigationEvents.NavigateToPrivacyFragment) + events.postValue(Event.NavigateToPrivacyFragment) } fun onShareButtonPress() { - routeToScreen.postValue(DebugLogNavigationEvents.NavigateToShareFragment) + events.postValue(Event.NavigateToUploadFragment) } fun onIdHistoryPress() { - routeToScreen.postValue(DebugLogNavigationEvents.NavigateToUploadHistory) + 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() @@ -86,7 +90,7 @@ class DebugLogViewModel @AssistedInject constructor( Timber.d("storeLog()") val snapshot = logSnapshotter.snapshot() val shareRequest = safLogExport.createSAFRequest(snapshot) - shareEvent.postValue(shareRequest) + events.postValue(Event.LocalExport(shareRequest)) } fun processSAFResult(requestCode: Int, safPath: Uri?) = launchWithProgress { @@ -101,10 +105,14 @@ class DebugLogViewModel @AssistedInject constructor( return@launchWithProgress } - val storageResult = request.storeSnapshot(contentResolver, safPath) - Timber.i("Log stored %s", storageResult) - - logStoreResult.postValue(storageResult) + 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)) + } } private fun launchWithProgress( @@ -119,7 +127,7 @@ class DebugLogViewModel @AssistedInject constructor( block() } catch (e: Throwable) { Timber.e(e, "launchWithProgress() failed.") - errorEvent.postValue(e) + events.postValue(Event.Error(e)) } finally { val duration = System.currentTimeMillis() - startTime Timber.v("launchWithProgress() took ${duration}ms") @@ -135,6 +143,18 @@ class DebugLogViewModel @AssistedInject constructor( 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/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index ff29781c4f705364b86ac526fe26b4c939a38586..ce0bebb41c5db8158339ab7dd75f467ff4ae8fc7 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -899,8 +899,16 @@ <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> @@ -1620,6 +1628,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/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index 60b7b297a475ed3cd145c2a0ab60b35a11504227..315ee7ea00279280d352ddce2e884c6bd7c27135 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -918,6 +918,16 @@ <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> @@ -1638,6 +1648,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/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 index 0b3e7884a89177d57fe9771c6ba46a1e417e4e69..44a2d6e19db22d8fc66b3051676818880435a606 100644 --- 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 @@ -44,7 +44,7 @@ class DebugLogStorageCheckTest : BaseTest() { @Test fun `normal not low storage case`() { val instance = createInstance() - instance.checkLowStorage() shouldBe false + instance.isLowStorage() shouldBe false verify { logWriter wasNot Called } } @@ -58,7 +58,7 @@ class DebugLogStorageCheckTest : BaseTest() { every { logWriter.write(capture(logSlot)) } just Runs val instance = createInstance() - instance.checkLowStorage() shouldBe true + instance.isLowStorage() shouldBe true logSlot.captured.throwable shouldBe unexpectedError } @@ -67,11 +67,11 @@ class DebugLogStorageCheckTest : BaseTest() { fun `low storage default is 200MB`() { every { targetPath.usableSpace } returns 199 * 1000 * 1024L val instance = createInstance() - instance.checkLowStorage() shouldBe true - instance.checkLowStorage() shouldBe true + instance.isLowStorage() shouldBe true + instance.isLowStorage() shouldBe true currentTime += 60 * 1000L - instance.checkLowStorage() shouldBe true + instance.isLowStorage() shouldBe true // We only write the warning once verify(exactly = 1) { logWriter.write(any()) } @@ -80,21 +80,21 @@ class DebugLogStorageCheckTest : BaseTest() { @Test fun `checks happen at most every 5 seconds`() { val instance = createInstance() - instance.checkLowStorage() shouldBe false + instance.isLowStorage() shouldBe false every { targetPath.usableSpace } returns 1024L - instance.checkLowStorage() shouldBe false + instance.isLowStorage() shouldBe false verify(exactly = 1) { targetPath.usableSpace } currentTime += 5000L - instance.checkLowStorage() shouldBe true + instance.isLowStorage() shouldBe true every { targetPath.usableSpace } returns 250 * 1000 * 1024L - instance.checkLowStorage() shouldBe true + instance.isLowStorage() shouldBe true verify(exactly = 2) { targetPath.usableSpace } }