diff options
Diffstat (limited to 'app/src/main/java')
5 files changed, 64 insertions, 1 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt index c3809b36..05dc6b65 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsDestination.kt @@ -38,6 +38,7 @@ import androidx.core.net.toUri import androidx.documentfile.provider.DocumentFile import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.lifecycleScope import app.passwordstore.BuildConfig import app.passwordstore.R import app.passwordstore.ui.compose.theme.SpacingLarge @@ -52,6 +53,7 @@ import app.passwordstore.util.services.PasswordExportService import app.passwordstore.util.settings.PreferenceKeys import com.github.androidpasswordstore.autofillparser.BrowserAutofillSupportLevel import com.github.androidpasswordstore.autofillparser.getInstalledBrowsersWithAutofillSupportLevel +import kotlinx.coroutines.launch private enum class SettingsDialog { Autofill, @@ -125,6 +127,16 @@ fun SettingsSectionDestination( ) } + val exportLog = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri -> + if (uri != null) + activity.lifecycleScope.launch { + val message = + if (viewModel.exportLog(uri)) R.string.log_exported else R.string.log_export_failed + activity.snackbar(message = activity.getString(message)) + } + } + // One screen lock prompt at a time: a double tap must not open two. A recreation resets it // (remember), and the old activity's prompt is gone with it. var prompting by remember { mutableStateOf(false) } @@ -206,6 +218,7 @@ fun SettingsSectionDestination( SettingsAction.ClearSecret -> dialog = SettingsDialog.ClearSecret SettingsAction.DeleteRepository -> dialog = SettingsDialog.DeleteRepository SettingsAction.Export -> export.launch(null) + SettingsAction.ExportLog -> exportLog.launch("hardenedpass-log.txt") else -> onOpen(action) } }, diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsGraph.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsGraph.kt index ba4d158c..e45166e0 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsGraph.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsGraph.kt @@ -70,7 +70,8 @@ fun NavGraphBuilder.settingsGraph(activity: MainActivity, nav: NavHostController // Handled by the destination itself. SettingsAction.ClearSecret, SettingsAction.DeleteRepository, - SettingsAction.Export -> Unit + SettingsAction.Export, + SettingsAction.ExportLog -> Unit } }, onDeleted = { diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsModel.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsModel.kt index eca6770c..54250f74 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsModel.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsModel.kt @@ -9,6 +9,7 @@ import app.passwordstore.util.extensions.hash import app.passwordstore.util.extensions.time import app.passwordstore.util.git.GitCommit import java.io.IOException +import java.io.OutputStream import logcat.LogPriority.ERROR import logcat.asLog import logcat.logcat @@ -77,3 +78,17 @@ fun gitLog(repo: Repository?): List<GitCommit> { emptyList() } } + +/** + * Writes [header], then what logcat holds for this app: Android shows an app only its own lines, + * and those of its earlier runs and crashes until the buffer rotates. + */ +fun writeAppLog(header: String, out: OutputStream) { + out.write(header.toByteArray()) + val logcat = + ProcessBuilder("logcat", "-d", "-b", "main,crash", "-v", "threadtime") + .redirectErrorStream(true) + .start() + logcat.inputStream.use { it.copyTo(out) } + logcat.waitFor() +} diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt index a7bbe30a..2e7840ad 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsScreen.kt @@ -63,6 +63,7 @@ enum class SettingsAction { ClearSecret, DeleteRepository, Export, + ExportLog, } /** Top bar with [title] and back, then [content] in one grouped surface, scrolling. */ @@ -324,6 +325,11 @@ fun SettingsSectionScreen( onCheckedChange = { onBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, it) }, summary = stringResource(R.string.pref_debug_logging_summary), ) + PreferenceAction( + title = stringResource(R.string.pref_export_log_title), + summary = stringResource(R.string.pref_export_log_summary), + onClick = { onAction(SettingsAction.ExportLog) }, + ) } SettingsSection.Pgp -> { PreferenceAction( diff --git a/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt b/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt index cfc576ae..90aecb2a 100644 --- a/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/app/passwordstore/ui/settings/SettingsViewModel.kt @@ -8,6 +8,8 @@ package app.passwordstore.ui.settings import android.app.Application import android.content.SharedPreferences import android.content.pm.ShortcutManager +import android.net.Uri +import android.os.Build import androidx.annotation.StringRes import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -16,6 +18,7 @@ import androidx.core.content.edit import androidx.core.content.getSystemService import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import app.passwordstore.BuildConfig import app.passwordstore.R import app.passwordstore.data.crypto.PGPPassphraseCache import app.passwordstore.data.repo.PasswordRepository @@ -31,12 +34,16 @@ import app.passwordstore.util.settings.GitSettings import app.passwordstore.util.settings.PasswordSortOrder import app.passwordstore.util.settings.PreferenceKeys import dagger.hilt.android.lifecycle.HiltViewModel +import java.io.IOException import java.nio.file.Files import java.nio.file.LinkOption import javax.inject.Inject import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import logcat.LogPriority.ERROR +import logcat.asLog +import logcat.logcat /** What the settings sections show: stored values, and the state rows depend on. */ data class SettingsState( @@ -111,6 +118,27 @@ constructor( state = read() } + /** Saves the app's log to [uri]; false when that failed. */ + suspend fun exportLog(uri: Uri): Boolean = + withContext(dispatcherProvider.io()) { + val header = + "HardenedPass ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}), " + + "Android ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT}), ${Build.MODEL}\n\n" + try { + val out = + getApplication<Application>().contentResolver.openOutputStream(uri) + ?: throw IOException("No stream for the chosen file") + out.use { writeAppLog(header, it) } + true + } catch (e: IOException) { + logcat(ERROR) { e.asLog() } + false + } catch (e: SecurityException) { + logcat(ERROR) { e.asLog() } + false + } + } + fun putBoolean(key: String, value: Boolean) { prefs.edit { putBoolean(key, value) } } |
