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

Add debug log feature for non-tester builds (EXPOSUREAPP-4451) (#2029)


* Initial draft for user accessible debug logs in production.

TODO: Sharing, Tests, Cleanup FileLoggerTree?

* Ready strings for translation.

* Log file compression for sharing (zip).

* Add log sharing

* Change how DebugLogger is initialized to make sure we can record issues that happen very early in the apps lifecycle.

* Add missing toolbar back arrow navigation.

* Fix initial delay for ui state emissions.

* Censor registration token.

* Adjust warning regarding sensitive data in debug logs.

* Use property injection instead of component getters.

* Hide option until greenlit.

* Add test for RegistrationTokenCensor

* Unit tests.

* Make unit test without triggerfile more specific.

* LINTs

* Fix missing injection provider in release mode.

* Fix regtoken censor if condition.

* Fix typos.

* Remove empty manifest specific to deviceForTesters build.

* Wait until log job is canceled before deleting log files.
Otherwise a race condition could lead to file creation after job cancellation.

* Replace runBlockingTest with runBlocking we don't need scheduler control and runBlockingTest is sometimes unreliable.

* Refactor FileSharing to use compat builder from androidx.

* Handle exceptions on debug log start()

* Print device infos when log is started.

* Text changes requested by UA

Co-authored-by: default avatarralfgehrer <mail@ralfgehrer.com>
parent f1537e7c
No related branches found
No related tags found
No related merge requests found
Showing
with 613 additions and 39 deletions
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="LockedOrientationActivity"
package="de.rki.coronawarnapp">
<application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
</application>
</manifest>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="share" path="share/" />
</paths>
\ No newline at end of file
......@@ -80,9 +80,19 @@
android:name=".contactdiary.ui.ContactDiaryActivity"
android:exported="false"
android:screenOrientation="portrait"
android:launchMode= "singleTop"
android:launchMode="singleTop"
android:theme="@style/AppTheme.ContactDiary"
android:windowSoftInputMode="adjustResize"/>
android:windowSoftInputMode="adjustResize" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
......
......@@ -66,6 +66,8 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
Timber.v("onCreate(): Initializing Dagger")
AppInjector.init(this)
CWADebug.initAfterInjection(component)
Timber.plant(rollingLogHistory)
Timber.v("onCreate(): WorkManager setup done: $workManager")
......
package de.rki.coronawarnapp.bugreporting
import dagger.Module
import dagger.Provides
import de.rki.coronawarnapp.bugreporting.censors.BugCensor
import de.rki.coronawarnapp.bugreporting.censors.RegistrationTokenCensor
import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger
import javax.inject.Singleton
@Module
class BugReportingSharedModule {
@Singleton
@Provides
fun debugLogger() = DebugLogger
@Singleton
@Provides
fun censors(
registrationTokenCensor: RegistrationTokenCensor
): List<BugCensor> = listOf(registrationTokenCensor)
}
package de.rki.coronawarnapp.bugreporting.censors
import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
interface BugCensor {
/**
* If there is something to censor a new log line is returned, otherwise returns null
*/
fun checkLog(entry: LogLine): LogLine?
}
package de.rki.coronawarnapp.bugreporting.censors
import dagger.Reusable
import de.rki.coronawarnapp.bugreporting.debuglog.LogLine
import de.rki.coronawarnapp.storage.LocalData
import de.rki.coronawarnapp.util.CWADebug
import javax.inject.Inject
import kotlin.math.min
@Reusable
class RegistrationTokenCensor @Inject constructor() : BugCensor {
override fun checkLog(entry: LogLine): LogLine? {
val token = LocalData.registrationToken() ?: return null
if (!entry.message.contains(token)) return null
val replacement = if (CWADebug.isDeviceForTestersBuild) {
token
} else {
token.substring(0, min(4, token.length)) + "###-####-####-####-############"
}
return entry.copy(message = entry.message.replace(token, replacement))
}
}
package de.rki.coronawarnapp.bugreporting.debuglog
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import timber.log.Timber
class DebugLogTree : Timber.DebugTree() {
private val logLinesPub = MutableSharedFlow<LogLine>(
replay = 128,
extraBufferCapacity = 1024,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val logLines: Flow<LogLine> = logLinesPub
init {
Timber.tag(TAG).d("init()")
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
LogLine(
timestamp = System.currentTimeMillis(),
priority = priority,
tag = tag,
message = message,
throwable = t
).also { logLinesPub.tryEmit(it) }
}
companion object {
private const val TAG = "DebugLogTree"
}
}
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.util.di.ApplicationComponent
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.yield
import timber.log.Timber
import java.io.File
@SuppressLint("LogNotTimber")
@Suppress("BlockingMethodInNonBlockingContext")
object DebugLogger : 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 mutex = Mutex()
private var logJob: Job? = null
private var logTree: DebugLogTree? = null
private var isDaggerReady = false
fun init(application: Application) {
context = application
try {
if (triggerFile.exists()) {
Timber.tag(TAG).i("Trigger file exists, starting debug log.")
runBlocking { start() }
}
} catch (e: Exception) {
// This is called from Application.onCreate() never crash here.
Timber.tag(TAG).e(e, "DebugLogger init(%s) failed.", application)
}
}
/**
* To censor unique data, we need to actually know what to censor.
* So we buffer log statements until Dagger is ready
*/
fun setInjectionIsReady(component: ApplicationComponent) {
Timber.tag(TAG).i("setInjectionIsReady()")
component.inject(this)
isDaggerReady = true
}
val isLogging: Boolean
get() = logJob?.isActive == true
suspend fun start(): Unit = mutex.withLock {
Timber.tag(TAG).d("start()")
if (isLogging) {
Timber.tag(TAG).w("Ignoring start(), already running.")
return@withLock
}
logJob?.cancel()
logTree?.let { Timber.uproot(it) }
DebugLogTree().apply {
Timber.plant(this)
logTree = this
if (!runningLog.exists()) {
runningLog.parentFile?.mkdirs()
if (runningLog.createNewFile()) {
Timber.tag(TAG).i("Log file didn't exist and was created.")
}
}
logJob = scope.launch {
try {
logLines.collect { rawLine ->
while (!isDaggerReady) {
yield()
}
val censoredLine = bugCensors.get().mapNotNull { it.checkLog(rawLine) }.firstOrNull()
appendLogLine(censoredLine ?: rawLine)
}
} catch (e: CancellationException) {
Timber.tag(TAG).i("Logging was canceled.")
} catch (e: Exception) {
Log.e(TAG, "Failed to call appendLogLine(...)", e)
}
}
}
if (!triggerFile.exists()) {
Timber.tag(TAG).i("Trigger file created.")
triggerFile.createNewFile()
}
}
suspend fun stop() = mutex.withLock {
Timber.tag(TAG).i("stop()")
if (triggerFile.exists() && triggerFile.delete()) {
Timber.tag(TAG).d("Trigger file deleted.")
}
logTree?.let {
Timber.tag(TAG).d("LogTree uprooted.")
Timber.uproot(it)
}
logTree = null
logJob?.let {
Timber.tag(TAG).d("LogJob canceled.")
it.cancel()
it.join()
}
logJob = null
if (runningLog.exists() && runningLog.delete()) {
Timber.tag(TAG).d("Log file was deleted.")
}
clearSharedFiles()
}
private fun appendLogLine(line: LogLine) {
val formattedLine = line.format(context)
runningLog.appendText(formattedLine, Charsets.UTF_8)
}
fun getLogSize(): Long = runningLog.length()
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)
}
}
}
private const val TAG = "DebugLogger"
}
package de.rki.coronawarnapp.bugreporting.debuglog
import de.rki.coronawarnapp.bugreporting.censors.BugCensor
import javax.inject.Inject
/**
* Workaround for dagger injection into kotlin objects
*/
@Suppress("UnnecessaryAbstractClass")
abstract class DebugLoggerBase {
@Inject internal lateinit var bugCensors: dagger.Lazy<List<BugCensor>>
}
package de.rki.coronawarnapp.bugreporting.debuglog
import de.rki.coronawarnapp.util.threads.NamedThreadFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors
import javax.inject.Qualifier
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
@Singleton
object DebugLoggerScope : CoroutineScope {
val dispatcher = Executors.newSingleThreadExecutor(
NamedThreadFactory("DebugLogger")
).asCoroutineDispatcher()
override val coroutineContext: CoroutineContext = SupervisorJob() + dispatcher
}
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class AppScope
package de.rki.coronawarnapp.bugreporting.debuglog
import android.content.Context
import android.util.Log
import org.joda.time.Instant
data class LogLine(
val timestamp: Long,
val priority: Int,
val tag: String?,
val message: String,
val throwable: Throwable?
) {
fun format(context: Context): String {
val time = Instant.ofEpochMilli(timestamp)
return "$time ${priorityLabel(priority)}/$tag: $message\n"
}
private fun priorityLabel(priority: Int): String = when (priority) {
Log.ERROR -> "E"
Log.WARN -> "W"
Log.INFO -> "I"
Log.DEBUG -> "D"
Log.VERBOSE -> "V"
else -> priority.toString()
}
}
package de.rki.coronawarnapp.bugreporting.debuglog.ui
import android.os.Bundle
import android.text.format.Formatter
import android.view.View
import android.widget.Toast
import androidx.core.view.isGone
import androidx.fragment.app.Fragment
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.BugreportingDebuglogFragmentBinding
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 DebugLogFragment : Fragment(R.layout.bugreporting_debuglog_fragment), AutoInject {
@Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
private val vm: DebugLogViewModel by cwaViewModels { viewModelFactory }
private val binding: BugreportingDebuglogFragmentBinding by viewBindingLazy()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
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
)
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
)
shareRecording.isEnabled = it.currentSize > 0L && !it.sharingInProgress
toggleRecording.isEnabled = !it.sharingInProgress
}
}
vm.errorEvent.observe2(this) {
Toast.makeText(requireContext(), it.toString(), Toast.LENGTH_LONG).show()
}
vm.shareEvent.observe2(this) {
startActivity(it.get(requireActivity()))
}
binding.apply {
toggleRecording.setOnClickListener { vm.toggleRecording() }
shareRecording.setOnClickListener { vm.shareRecording() }
toolbar.setNavigationOnClickListener { popBackStack() }
}
}
}
package de.rki.coronawarnapp.bugreporting.debuglog.ui
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 DebugLogFragmentModule {
@Binds
@IntoMap
@CWAViewModelKey(DebugLogViewModel::class)
abstract fun onboardingNotificationsVM(factory: DebugLogViewModel.Factory): CWAViewModelFactory<out CWAViewModel>
@ContributesAndroidInjector
abstract fun debuglogFragment(): DebugLogFragment
}
package de.rki.coronawarnapp.bugreporting.debuglog.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import com.squareup.inject.assisted.AssistedInject
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger
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.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
class DebugLogViewModel @AssistedInject constructor(
private val debugLogger: DebugLogger,
dispatcherProvider: DispatcherProvider,
private val timeStamper: TimeStamper,
private val fileSharing: FileSharing,
private val enfClient: ENFClient
) : 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 ->
State(
isRecording = debugLogger.isLogging,
currentSize = debugLogger.getLogSize() + debugLogger.getShareSize(),
sharingInProgress = sharingInProgress
)
}.asLiveData(context = dispatcherProvider.Default)
val errorEvent = SingleLiveEvent<Throwable>()
val shareEvent = SingleLiveEvent<FileSharing.ShareIntentProvider>()
fun toggleRecording() = launch {
try {
if (debugLogger.isLogging) {
debugLogger.stop()
} else {
debugLogger.start()
printExtendedLogInfos()
}
} 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 shareRecording() {
sharingInProgress.value = true
launch {
try {
debugLogger.clearSharedFiles()
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")
Zipper(zipFile).zip(
listOf(Zipper.Entry(name = "$formattedFileName.txt", path = debugLogger.runningLog))
)
val intentProvider = fileSharing.getIntentProvider(
path = zipFile,
title = zipFile.name,
chooserTitle = R.string.debugging_debuglog_sharing_dialog_title
)
shareEvent.postValue(intentProvider)
} catch (e: Exception) {
Timber.e(e, "Sharing debug log failed.")
errorEvent.postValue(e)
} finally {
sharingInProgress.value = false
}
}
}
data class State(
val isRecording: Boolean,
val sharingInProgress: Boolean = false,
val currentSize: Long = 0
)
@AssistedInject.Factory
interface Factory : SimpleCWAViewModelFactory<DebugLogViewModel>
}
......@@ -5,15 +5,16 @@ import android.os.Bundle
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import androidx.core.view.isGone
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.FragmentInformationBinding
import de.rki.coronawarnapp.ui.doNavigate
import de.rki.coronawarnapp.ui.main.MainActivity
import de.rki.coronawarnapp.util.CWADebug
import de.rki.coronawarnapp.util.ExternalActionHelper
import de.rki.coronawarnapp.util.di.AutoInject
import de.rki.coronawarnapp.util.ui.doNavigate
import de.rki.coronawarnapp.util.ui.observe2
import de.rki.coronawarnapp.util.ui.setGone
import de.rki.coronawarnapp.util.ui.viewBindingLazy
......@@ -55,6 +56,9 @@ class InformationFragment : Fragment(R.layout.fragment_information), AutoInject
setButtonOnClickListener()
setAccessibilityDelegate()
// TODO Hidden until further clarification regarding release schedule is available
binding.informationDebuglog.mainRow.isGone = !CWADebug.isDeviceForTestersBuild
}
override fun onResume() {
......@@ -76,22 +80,22 @@ class InformationFragment : Fragment(R.layout.fragment_information), AutoInject
private fun setButtonOnClickListener() {
binding.informationAbout.mainRow.setOnClickListener {
findNavController().doNavigate(
doNavigate(
InformationFragmentDirections.actionInformationFragmentToInformationAboutFragment()
)
}
binding.informationPrivacy.mainRow.setOnClickListener {
findNavController().doNavigate(
doNavigate(
InformationFragmentDirections.actionInformationFragmentToInformationPrivacyFragment()
)
}
binding.informationTerms.mainRow.setOnClickListener {
findNavController().doNavigate(
doNavigate(
InformationFragmentDirections.actionInformationFragmentToInformationTermsFragment()
)
}
binding.informationContact.mainRow.setOnClickListener {
findNavController().doNavigate(
doNavigate(
InformationFragmentDirections.actionInformationFragmentToInformationContactFragment()
)
}
......@@ -99,15 +103,20 @@ class InformationFragment : Fragment(R.layout.fragment_information), AutoInject
ExternalActionHelper.openUrl(this, requireContext().getString(R.string.main_about_link))
}
binding.informationLegal.mainRow.setOnClickListener {
findNavController().doNavigate(
doNavigate(
InformationFragmentDirections.actionInformationFragmentToInformationLegalFragment()
)
}
binding.informationTechnical.mainRow.setOnClickListener {
findNavController().doNavigate(
doNavigate(
InformationFragmentDirections.actionInformationFragmentToInformationTechnicalFragment()
)
}
binding.informationDebuglog.mainRow.setOnClickListener {
doNavigate(
InformationFragmentDirections.actionInformationFragmentToDebuglogFragment()
)
}
binding.informationHeader.headerButtonBack.buttonIcon.setOnClickListener {
(activity as MainActivity).goBack()
}
......
......@@ -4,11 +4,12 @@ import dagger.Binds
import dagger.Module
import dagger.android.ContributesAndroidInjector
import dagger.multibindings.IntoMap
import de.rki.coronawarnapp.bugreporting.debuglog.ui.DebugLogFragmentModule
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
@Module
@Module(includes = [DebugLogFragmentModule::class])
abstract class InformationFragmentModule {
@Binds
@IntoMap
......
......@@ -3,7 +3,9 @@ package de.rki.coronawarnapp.util
import android.app.Application
import android.os.Build
import de.rki.coronawarnapp.BuildConfig
import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger
import de.rki.coronawarnapp.util.debug.FileLogger
import de.rki.coronawarnapp.util.di.ApplicationComponent
import timber.log.Timber
object CWADebug {
......@@ -19,9 +21,13 @@ object CWADebug {
fileLogger = FileLogger(application)
}
Timber.i("CWA version: %s (%s)", BuildConfig.VERSION_CODE, BuildConfig.GIT_COMMIT_SHORT_HASH)
Timber.i("CWA flavor: %s (%s)", BuildConfig.FLAVOR, BuildConfig.BUILD_TYPE)
Timber.i("Build.FINGERPRINT: %s", Build.FINGERPRINT)
DebugLogger.init(application)
logDeviceInfos()
}
fun initAfterInjection(component: ApplicationComponent) {
DebugLogger.setInjectionIsReady(component)
}
val isDebugBuildOrMode: Boolean
......@@ -45,4 +51,10 @@ object CWADebug {
false
}
}
fun logDeviceInfos() {
Timber.i("CWA version: %s (%s)", BuildConfig.VERSION_CODE, BuildConfig.GIT_COMMIT_SHORT_HASH)
Timber.i("CWA flavor: %s (%s)", BuildConfig.FLAVOR, BuildConfig.BUILD_TYPE)
Timber.i("Build.FINGERPRINT: %s", Build.FINGERPRINT)
}
}
package de.rki.coronawarnapp.util.compression
import timber.log.Timber
import java.io.File
import java.io.IOException
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
class Zipper(private val zipPath: File) {
fun zip(toZip: List<Entry>) {
if (zipPath.exists()) throw IOException("$zipPath already exists")
Timber.tag(TAG).d("Creating ZIP file: %s", zipPath)
zipPath.parentFile?.mkdirs()
zipPath.createNewFile()
if (!zipPath.exists()) throw IOException("Could not create $zipPath")
ZipOutputStream(zipPath.outputStream().buffered()).use { output ->
for (i in toZip.indices) {
Timber.tag(TAG).v("Compressing ${toZip[i]} into $zipPath")
val item = toZip[i]
Timber.tag(TAG).v("Reading %s (size=%d)", item.path, item.path.length())
item.path.inputStream().buffered().use { input ->
output.putNextEntry(ZipEntry(item.name))
input.copyTo(output)
}
}
}
Timber.tag(TAG).i("ZipFile finished: %s", zipPath)
}
data class Entry(
val path: File,
val name: String = path.name
)
companion object {
private const val TAG = "ZipFile"
}
}
......@@ -9,6 +9,8 @@ import de.rki.coronawarnapp.appconfig.AppConfigModule
import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.bugreporting.BugReporter
import de.rki.coronawarnapp.bugreporting.BugReportingModule
import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule
import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger
import de.rki.coronawarnapp.contactdiary.ContactDiaryRootModule
import de.rki.coronawarnapp.diagnosiskeys.DiagnosisKeysModule
import de.rki.coronawarnapp.diagnosiskeys.DownloadDiagnosisKeysTaskModule
......@@ -64,6 +66,7 @@ import javax.inject.Singleton
TaskModule::class,
DeviceForTestersModule::class,
BugReportingModule::class,
BugReportingSharedModule::class,
SerializationModule::class,
WorkerBinder::class,
ContactDiaryRootModule::class
......@@ -88,6 +91,8 @@ interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> {
val bugReporter: BugReporter
fun inject(logger: DebugLogger)
@Component.Factory
interface Factory {
fun create(@BindsInstance app: CoronaWarnApplication): ApplicationComponent
......
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