diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt index 089820755..e5a8cd1db 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoActivity.kt @@ -4,19 +4,13 @@ package at.bitfire.davdroid.ui -import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ShareCompat -import androidx.core.content.FileProvider import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import at.bitfire.davdroid.BuildConfig -import at.bitfire.davdroid.R import dagger.hilt.android.AndroidEntryPoint -import java.io.File import javax.inject.Inject /** @@ -44,66 +38,13 @@ class DebugInfoActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - model.zipFile.observe(this) { zipFile -> - if (zipFile == null) return@observe - - // ZIP file is ready - shareFile( - zipFile, - subject = "${getString(R.string.app_name)} ${BuildConfig.VERSION_NAME} debug info", - text = getString(R.string.debug_info_attached), - type = "*/*", // application/zip won't show all apps that can manage binary files, like ShareViaHttp - ) - - // only share ZIP file once - model.zipFile.value = null - } - setContent { M2Theme { DebugInfoScreen( model, - onShareFile = { shareFile(it) }, - onViewFile = { viewFile(it) }, onNavUp = { onNavigateUp() } ) } } } - - private fun shareFile( - file: File, - subject: String? = null, - text: String? = null, - type: String = "text/plain" - ) { - val uri = FileProvider.getUriForFile( - this, - getString(R.string.authority_debug_provider), - file - ) - ShareCompat.IntentBuilder(this) - .setSubject(subject) - .setText(text) - .setType(type) - .setStream(uri) - .startChooser() - } - - private fun viewFile( - file: File, - title: String? = null - ) { - val uri = FileProvider.getUriForFile( - this, - getString(R.string.authority_debug_provider), - file - ) - val intent = Intent(Intent.ACTION_VIEW).apply { - setDataAndType(uri, "text/plain") - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - startActivity(Intent.createChooser(intent, title)) - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt index 47be0b988..942a838b9 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt @@ -25,12 +25,16 @@ import android.os.StatFs import android.provider.CalendarContract import android.provider.ContactsContract import android.text.format.DateUtils +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.core.app.NotificationManagerCompat +import androidx.core.app.ShareCompat import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider import androidx.core.content.getSystemService import androidx.core.content.pm.PackageInfoCompat import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.work.WorkManager import androidx.work.WorkQuery @@ -110,16 +114,16 @@ class DebugInfoModel @AssistedInject constructor ( @Inject lateinit var settings: SettingsManager - val cause = MutableLiveData() - var logFile = MutableLiveData() - val localResource = MutableLiveData() - val remoteResource = MutableLiveData() - val debugInfo = MutableLiveData() + var cause by mutableStateOf(null) + var logFile by mutableStateOf(null) + var localResource by mutableStateOf(null) + var remoteResource by mutableStateOf(null) + var debugInfo by mutableStateOf(null) // feedback for UI - val zipProgress = MutableLiveData(false) - val zipFile = MutableLiveData() - val error = MutableLiveData() + var zipProgress by mutableStateOf(false) + var zipFile by mutableStateOf(null) + var error by mutableStateOf(null) init { // create debug info directory @@ -134,22 +138,22 @@ class DebugInfoModel @AssistedInject constructor ( file.writer().buffered().use { writer -> IOUtils.copy(StringReader(logsText), writer) } - logFile.postValue(file) + logFile = file } else Logger.log.warning("Can't write logs to $file") } else Logger.getDebugLogFile()?.let { debugLogFile -> if (debugLogFile.isFile && debugLogFile.canRead()) - logFile.postValue(debugLogFile) + logFile = debugLogFile } val throwable = extras?.getSerializable(EXTRA_CAUSE) as? Throwable - cause.postValue(throwable) + cause = throwable val local = extras?.getString(EXTRA_LOCAL_RESOURCE) - localResource.postValue(local) + localResource = local val remote = extras?.getString(EXTRA_REMOTE_RESOURCE) - remoteResource.postValue(remote) + remoteResource = remote generateDebugInfo( extras?.getParcelable(EXTRA_ACCOUNT), @@ -161,6 +165,52 @@ class DebugInfoModel @AssistedInject constructor ( } } + fun shareZipFile() { + zipFile?.let { + shareFile( + it, + subject = "${context.getString(R.string.app_name)} ${BuildConfig.VERSION_NAME} debug info", + text = context.getString(R.string.debug_info_attached), + type = "*/*", // application/zip won't show all apps that can manage binary files, like ShareViaHttp + ) + } + } + + fun shareFile( + file: File, + subject: String? = null, + text: String? = null, + type: String = "text/plain" + ) { + val uri = FileProvider.getUriForFile( + context, + context.getString(R.string.authority_debug_provider), + file + ) + ShareCompat.IntentBuilder(context) + .setSubject(subject) + .setText(text) + .setType(type) + .setStream(uri) + .startChooser() + } + + fun viewFile( + file: File, + title: String? = null + ) { + val uri = FileProvider.getUriForFile( + context, + context.getString(R.string.authority_debug_provider), + file + ) + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, "text/plain") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(Intent.createChooser(intent, title)) + } + private fun generateDebugInfo(syncAccount: Account?, syncAuthority: String?, cause: Throwable?, localResource: String?, remoteResource: String?) { val debugInfoFile = File(Logger.debugDir(), FILE_DEBUG_INFO) debugInfoFile.writer().buffered().use { writer -> @@ -413,18 +463,18 @@ class DebugInfoModel @AssistedInject constructor ( writer.append("--- END DEBUG INFO ---\n") writer.toString() } - debugInfo.postValue(debugInfoFile) + debugInfo = debugInfoFile } fun generateZip() { try { - zipProgress.postValue(true) + zipProgress = true val file = File(Logger.debugDir(), "davx5-debug.zip") Logger.log.fine("Writing debug info to ${file.absolutePath}") ZipOutputStream(file.outputStream().buffered()).use { zip -> zip.setLevel(9) - debugInfo.value?.let { debugInfo -> + debugInfo?.let { debugInfo -> zip.putNextEntry(ZipEntry("debug-info.txt")) debugInfo.inputStream().use { IOUtils.copy(it, zip) @@ -432,7 +482,7 @@ class DebugInfoModel @AssistedInject constructor ( zip.closeEntry() } - val logs = logFile.value + val logs = logFile if (logs != null) { // verbose logs available zip.putNextEntry(ZipEntry(logs.name)) @@ -454,12 +504,12 @@ class DebugInfoModel @AssistedInject constructor ( } // success, show ZIP file - zipFile.postValue(file) + zipFile = file } catch (e: Exception) { Logger.log.log(Level.SEVERE, "Couldn't generate debug info ZIP", e) - error.postValue(e.localizedMessage) + error = e.localizedMessage } finally { - zipProgress.postValue(false) + zipProgress = false } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoScreen.kt index 48b73f893..341278e95 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoScreen.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoScreen.kt @@ -20,8 +20,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier @@ -35,34 +33,31 @@ import at.bitfire.dav4jvm.exception.HttpException import at.bitfire.davdroid.R import at.bitfire.davdroid.ui.composable.BasicTopAppBar import at.bitfire.davdroid.ui.composable.CardWithImage -import java.io.File import java.io.IOError import java.io.IOException @Composable fun DebugInfoScreen( model: DebugInfoModel, - onShareFile: (File) -> Unit, - onViewFile: (File) -> Unit, onNavUp: () -> Unit ) { - val debugInfo by model.debugInfo.observeAsState() - val zipProgress by model.zipProgress.observeAsState(false) - val modelCause by model.cause.observeAsState() - val localResource by model.localResource.observeAsState() - val remoteResource by model.remoteResource.observeAsState() - val logFile by model.logFile.observeAsState() - val error by model.error.observeAsState() + val debugInfo = model.debugInfo + val zipProgress = model.zipProgress + val modelCause = model.cause + val localResource = model.localResource + val remoteResource = model.remoteResource + val logFile = model.logFile + val error = model.error AppTheme { DebugInfoScreen( - error, - onResetError = { model.error.value = null }, - debugInfo != null, - zipProgress, - modelCause != null, + error = error, + onResetError = { model.error = null }, + showDebugInfo = debugInfo != null, + zipProgress = zipProgress, + showModelCause = modelCause != null, modelCauseTitle = when (modelCause) { - is HttpException -> stringResource(if ((modelCause as HttpException).code / 100 == 5) R.string.debug_info_server_error else R.string.debug_info_http_error) + is HttpException -> stringResource(if (modelCause.code / 100 == 5) R.string.debug_info_server_error else R.string.debug_info_http_error) is DavException -> stringResource(R.string.debug_info_webdav_error) is IOException, is IOError -> stringResource(R.string.debug_info_io_error) else -> modelCause?.let { it::class.java.simpleName } @@ -71,21 +66,23 @@ fun DebugInfoScreen( modelCauseMessage = stringResource( if (modelCause is HttpException) when { - (modelCause as HttpException).code == 403 -> R.string.debug_info_http_403_description - (modelCause as HttpException).code == 404 -> R.string.debug_info_http_404_description - (modelCause as HttpException).code / 100 == 5 -> R.string.debug_info_http_5xx_description + modelCause.code == 403 -> R.string.debug_info_http_403_description + modelCause.code == 404 -> R.string.debug_info_http_404_description + modelCause.code / 100 == 5 -> R.string.debug_info_http_5xx_description else -> R.string.debug_info_unexpected_error } else R.string.debug_info_unexpected_error ), - localResource, - remoteResource, - logFile != null, + localResource = localResource, + remoteResource = remoteResource, + hasLogFile = logFile != null, + onResetZipFile = { model.zipFile = null }, onGenerateZip = { model.generateZip() }, - onShareLogsFile = { logFile?.let { onShareFile(it) } }, - onViewDebugFile = { debugInfo?.let { onViewFile(it) } }, - onNavUp + onShareZipFile = { model.shareZipFile() }, + onShareLogsFile = { logFile?.let { model.shareFile(it) } }, + onViewDebugFile = { debugInfo?.let { model.viewFile(it) } }, + onNavUp = onNavUp ) } } @@ -103,13 +100,25 @@ fun DebugInfoScreen( localResource: String?, remoteResource: String?, hasLogFile: Boolean, + onResetZipFile: () -> Unit, onGenerateZip: () -> Unit, + onShareZipFile: () -> Unit, onShareLogsFile: () -> Unit, onViewDebugFile: () -> Unit, onNavUp: () -> Unit ) { val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(zipProgress) { + if (!zipProgress) return@LaunchedEffect + + // ZIP file is ready + onShareZipFile() + + // only share ZIP file once + onResetZipFile() + } + Scaffold( floatingActionButton = { if (showDebugInfo && !zipProgress) { @@ -285,7 +294,9 @@ fun DebugInfoScreen_Preview() { localResource = "local-resource-string", remoteResource = "remote-resource-string", hasLogFile = true, + onResetZipFile = {}, onGenerateZip = {}, + onShareZipFile = {}, onShareLogsFile = {}, onViewDebugFile = {}, onNavUp = {},