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 7645f1b8aa8750ba5f04b387618888d32508c1ad..00ccf795235bae3fc73efa24865fafd46731301e 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 @@ -35,7 +35,6 @@ class DebugLogger( ) : DebugLoggerBase() { private val triggerFile = File(debugDir, "debug.trigger") - val sharedDirectory = File(debugDir, "shared") internal val runningLog: File get() = logWriter.logFile @@ -146,8 +145,6 @@ class DebugLogger( logJob = null logWriter.teardown() - - clearSharedFiles() } private fun startNewLogJob(logLines: Flow<LogLine>) = scope.launch { @@ -175,22 +172,6 @@ class DebugLogger( } } - fun getShareSize(): Long = sharedDirectory.listFiles() - ?.fold(0L) { prev, file -> prev + file.length() } - ?: 0L - - fun clearSharedFiles() { - if (!sharedDirectory.exists()) return - - 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) - } - } - } - companion object { internal const val TAG = "DebugLogger" } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotter.kt new file mode 100644 index 0000000000000000000000000000000000000000..1cb67b8deb4352f0fcbd6c54db2722ed38e80ace --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotter.kt @@ -0,0 +1,58 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.sharing + +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 { + Timber.tag(TAG).w("Deleting 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/sharing/SAFLogSharing.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharing.kt new file mode 100644 index 0000000000000000000000000000000000000000..31df4941d7ce7521bfc216685519d1509e9e867f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharing.kt @@ -0,0 +1,61 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.sharing + +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri +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 SAFLogSharing @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/ui/DebugLogFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/debuglog/ui/DebugLogFragment.kt index f4d9c0c415347204523485fe1ae585cbf8619dc5..7e1135d109d24be2ae74d2b837bf25ac1f18e608 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,5 +1,7 @@ package de.rki.coronawarnapp.bugreporting.debuglog.ui +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.text.format.Formatter import android.view.View @@ -18,6 +20,7 @@ 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 { @@ -30,6 +33,8 @@ class DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), Auto super.onViewCreated(view, savedInstanceState) binding.apply { + toolbar.setNavigationOnClickListener { popBackStack() } + if (explanationSectionTwo.text == getString(R.string.debugging_debuglog_intro_explanation_section_two) ) { @@ -39,6 +44,7 @@ class DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), Auto R.string.debugging_debuglog_intro_explanation_section_two_faq_link ) } + debugLogPrivacyInformation.setOnClickListener { vm.onPrivacyButtonPress() } } vm.state.observe2(this) { @@ -66,24 +72,45 @@ class DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), Auto toggleRecording.apply { isActivated = it.isRecording - isEnabled = !it.sharingInProgress + 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.toggleRecording() } + setOnClickListener { vm.onToggleRecording() } } toggleSendErrorLog.apply { isGone = !it.isRecording - isEnabled = it.currentSize > 0L && !it.sharingInProgress - setOnClickListener { vm.shareRecording() } + isEnabled = it.currentSize > 0L && !it.isActionInProgress + setOnClickListener { vm.onUploadLog() } } - toggleStoreLog.isGone = !it.isRecording + toggleStoreLog.apply { + isGone = !it.isRecording + isEnabled = it.currentSize > 0L && !it.isActionInProgress + setOnClickListener { vm.onStoreLog() } + } } } + 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.logUploads.observe2(this@DebugLogFragment) { + binding.debugLogHistoryContainer.setGone(it.logs.isEmpty()) + } + binding.debugLogHistoryContainer.setOnClickListener { vm.onIdHistoryPress() } + vm.routeToScreen.observe2(this) { when (it) { DebugLogNavigationEvents.NavigateToPrivacyFragment -> doNavigate( @@ -98,19 +125,13 @@ class DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), Auto vm.errorEvent.observe2(this) { Toast.makeText(requireContext(), it.toString(), Toast.LENGTH_LONG).show() } + } - vm.shareEvent.observe2(this) { - startActivity(it.get(requireActivity())) - } - - vm.logUploads.observe2(this@DebugLogFragment) { - binding.debugLogHistoryContainer.setGone(it.logs.isEmpty()) - } - - binding.apply { - debugLogHistoryContainer.setOnClickListener { vm.onIdHistoryPress() } - debugLogPrivacyInformation.setOnClickListener { vm.onPrivacyButtonPress() } - 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 + ) } } 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 4db122764569d8307f7471e72a847a8869841834..542d5ff9f8f0e03b152bb797e2e5c9332043dfd5 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,57 +1,58 @@ 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.sharing.LogSnapshotter +import de.rki.coronawarnapp.bugreporting.debuglog.sharing.SAFLogSharing 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.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine -import org.joda.time.format.DateTimeFormat import timber.log.Timber -import java.io.File class DebugLogViewModel @AssistedInject constructor( private val debugLogger: DebugLogger, dispatcherProvider: DispatcherProvider, - private val timeStamper: TimeStamper, - private val fileSharing: FileSharing, private val enfClient: ENFClient, - bugReportingSettings: BugReportingSettings + bugReportingSettings: BugReportingSettings, + private val logSnapshotter: LogSnapshotter, + private val safLogSharing: SAFLogSharing, + private val contentResolver: ContentResolver, ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { - val logUploads = bugReportingSettings.uploadHistory.flow - .asLiveData(context = dispatcherProvider.Default) - - private val sharingInProgress = MutableStateFlow(false) + private val isActionInProgress = MutableStateFlow(false) val routeToScreen = SingleLiveEvent<DebugLogNavigationEvents>() + val logUploads = bugReportingSettings.uploadHistory.flow + .asLiveData(context = dispatcherProvider.Default) + val state: LiveData<State> = combine( - sharingInProgress, + isActionInProgress, debugLogger.logState - ) { sharingInProgress, logState -> + ) { isActionInProgress, logState -> State( isRecording = logState.isLogging, isLowStorage = logState.isLowStorage, - currentSize = logState.logSize + debugLogger.getShareSize(), - sharingInProgress = sharingInProgress + currentSize = logState.logSize, + isActionInProgress = isActionInProgress ) }.asLiveData(context = dispatcherProvider.Default) val errorEvent = SingleLiveEvent<Throwable>() - val shareEvent = SingleLiveEvent<FileSharing.ShareIntentProvider>() + val shareEvent = SingleLiveEvent<SAFLogSharing.Request>() + val logStoreResult = SingleLiveEvent<SAFLogSharing.Request.Result>() fun onPrivacyButtonPress() { routeToScreen.postValue(DebugLogNavigationEvents.NavigateToPrivacyFragment) @@ -61,56 +62,69 @@ class DebugLogViewModel @AssistedInject constructor( routeToScreen.postValue(DebugLogNavigationEvents.NavigateToUploadHistory) } - fun toggleRecording() = launch { - try { - if (debugLogger.isLogging.value) { - debugLogger.stop() - } else { - debugLogger.start() - printExtendedLogInfos() + fun onToggleRecording() = launchWithProgress { + if (debugLogger.isLogging.value) { + debugLogger.stop() + } else { + 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) } } - 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 onUploadLog() = launchWithProgress { + Timber.d("uploadLog()") + throw NotImplementedError("TODO") } - fun shareRecording() { - sharingInProgress.value = true - launch { - try { - debugLogger.clearSharedFiles() + fun onStoreLog() = launchWithProgress(finishProgressAction = false) { + Timber.d("storeLog()") + val snapshot = logSnapshotter.snapshot() + val shareRequest = safLogSharing.createSAFRequest(snapshot) + shareEvent.postValue(shareRequest) + } - 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") + fun processSAFResult(requestCode: Int, safPath: Uri?) = launchWithProgress { + if (safPath == null) { + Timber.i("No SAF path available.") + return@launchWithProgress + } - Zipper(zipFile).zip( - listOf(Zipper.Entry(name = "$formattedFileName.txt", path = debugLogger.runningLog)) - ) + val request = safLogSharing.getRequest(requestCode) + if (request == null) { + Timber.w("Unknown request with code $requestCode") + return@launchWithProgress + } - val intentProvider = fileSharing.getIntentProvider( - path = zipFile, - title = zipFile.name, - chooserTitle = R.string.debugging_debuglog_sharing_dialog_title - ) + val storageResult = request.storeSnapshot(contentResolver, safPath) + Timber.i("Log stored %s", storageResult) - shareEvent.postValue(intentProvider) - } catch (e: Exception) { - Timber.e(e, "Sharing debug log failed.") + logStoreResult.postValue(storageResult) + } + + private fun launchWithProgress( + finishProgressAction: Boolean = true, + block: suspend CoroutineScope.() -> Unit + ) { + val startTime = System.currentTimeMillis() + isActionInProgress.value = true + + launch { + try { + block() + } catch (e: Throwable) { + Timber.e(e, "launchWithProgress() failed.") errorEvent.postValue(e) } finally { - sharingInProgress.value = false + val duration = System.currentTimeMillis() - startTime + Timber.v("launchWithProgress() took ${duration}ms") + if (finishProgressAction) isActionInProgress.value = false } } } @@ -118,7 +132,7 @@ class DebugLogViewModel @AssistedInject constructor( data class State( val isRecording: Boolean, val isLowStorage: Boolean, - val sharingInProgress: Boolean = false, + val isActionInProgress: Boolean = false, val currentSize: Long = 0 ) 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/test/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/DebugLoggerTest.kt index 83495f172e2d50f4dccd7317b15e79a20257ad76..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 @@ -23,7 +23,6 @@ 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() { @@ -35,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") @@ -140,24 +138,17 @@ class DebugLoggerTest : BaseIOTest() { Timber.forest().none { it is DebugLogTree } shouldBe true - File(sharedDir, "1").apply { - parentFile?.mkdirs() - appendBytes(Random.nextBytes(10)) - } - instance.start() instance.start() instance.start() Timber.forest().single { it is DebugLogTree } shouldNotBe null - sharedDir.listFiles()!!.size shouldBe 1 instance.stop() instance.stop() Timber.forest().none { it is DebugLogTree } shouldBe true instance.isLogging.value shouldBe false - sharedDir.listFiles()!!.size shouldBe 0 } @Test @@ -180,17 +171,6 @@ class DebugLoggerTest : BaseIOTest() { runningLog.exists() shouldBe false } - @Test - fun `shared size aggregates shared folder size`() = runBlockingTest { - sharedDir.mkdirs() - File(sharedDir, "1").apply { appendBytes(Random.nextBytes(10)) } - File(sharedDir, "2").apply { appendBytes(Random.nextBytes(15)) } - createInstance(scope = this).apply { - init() - getShareSize() shouldBe 25 - } - } - @Test fun `logwriter is setup and used`() = runBlockingTest { val instance = createInstance(scope = this).apply { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotterTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotterTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f823097df42336229e965cff9951b9a82ef3263c --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/LogSnapshotterTest.kt @@ -0,0 +1,76 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.sharing + +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/sharing/SAFLogSharingTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharingTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..eb323f351d0bdd530bb575518706fbf5ddbe731f --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/debuglog/sharing/SAFLogSharingTest.kt @@ -0,0 +1,71 @@ +package de.rki.coronawarnapp.bugreporting.debuglog.sharing + +import android.content.ContentResolver +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 SAFLogSharingTest : 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() = SAFLogSharing() + + @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 + } +}