diff --git a/app/src/main/java/com/onemoresecret/CrashReportActivity.kt b/app/src/main/java/com/onemoresecret/CrashReportActivity.kt index 8d2ff23..5d2610c 100644 --- a/app/src/main/java/com/onemoresecret/CrashReportActivity.kt +++ b/app/src/main/java/com/onemoresecret/CrashReportActivity.kt @@ -1,8 +1,14 @@ package com.onemoresecret -import android.content.ActivityNotFoundException +import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipboardManager +import android.content.Context import android.content.Intent +import android.os.Build import android.os.Bundle +import android.os.PersistableBundle +import android.view.WindowManager import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -14,38 +20,43 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import com.onemoresecret.CrashReportData -import com.onemoresecret.OmsFileProvider -import com.onemoresecret.R import com.onemoresecret.composable.OneMoreSecretTheme import java.io.IOException import java.nio.charset.StandardCharsets import java.nio.file.Files import kotlin.system.exitProcess +private const val MAX_INLINE_REPORT_LENGTH = 128 * 1024 class CrashReportActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + //prohibit screenshots + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + Thread { OmsFileProvider.purgeTmp(this) }.start() @Suppress("DEPRECATION") val crashReportData = intent.getSerializableExtra(OmsUncaughtExceptionHandler.EXTRA_CRASH_REPORT) as? CrashReportData - + + if (crashReportData == null) { + finish() + return + } + setContent { OneMoreSecretTheme { - if (crashReportData != null) { - CrashReport( - crashReportData = crashReportData, - onDismiss = { - finish() - exitProcess(0) - } - ) - } + CrashReport( + crashReportData = crashReportData, + onDismiss = { + finish() + exitProcess(0) + } + ) } } } @@ -105,60 +116,64 @@ fun CrashReport( Text("Dismiss") } Spacer(modifier = Modifier.width(8.dp)) + TextButton(onClick = { + copyToClipboard(context, reportText) + }) { + Text(stringResource(R.string.copy)) + } + Spacer(modifier = Modifier.width(8.dp)) Button(onClick = { - sendEmail(context, crashReportData, includeLogcat, onDismiss) + shareReport(context, reportText) }) { - Text("Send") + Text(stringResource(R.string.share)) } } } } -// Extracted logic for sending the email -private fun sendEmail( - context: android.content.Context, - crashReportData: CrashReportData, - includeLogcat: Boolean, - onComplete: () -> Unit -) { - val contactEmail = context.getString(R.string.contact_email) +private fun copyToClipboard(context: Context, report: String) { + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText("oneMoreSecret", report) - fun createBaseIntent(action: String): Intent { - return Intent(action).apply { - data = "mailto:".toUri() - putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.crash_email_subject)) - putExtra(Intent.EXTRA_EMAIL, arrayOf(contactEmail)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + clipData.description.extras = PersistableBundle().apply { + putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) } } - try { - val crashReport = crashReportData.toString(includeLogcat) - val fileRecord = OmsFileProvider.create(context, "crash_report.txt", false) - Files.write(fileRecord.path, crashReport!!.toByteArray(StandardCharsets.UTF_8)) + clipboardManager.setPrimaryClip(clipData) + Toast.makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show() +} - val intentSend = createBaseIntent(Intent.ACTION_SEND).apply { - putExtra(Intent.EXTRA_STREAM, fileRecord.uri) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - putExtra(Intent.EXTRA_TEXT, context.getString(R.string.crash_email_body)) - } +// Extracted logic for sharing the report +private fun shareReport(context: Context, report: String) { + val sendIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.crash_email_subject)) + putExtra(Intent.EXTRA_TITLE, context.getString(R.string.crash_email_subject)) + putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.contact_email))) + } - try { - context.startActivity(intentSend) - onComplete() - } catch (_: ActivityNotFoundException) { - try { - val intentSendTo = createBaseIntent(Intent.ACTION_SENDTO).apply { - putExtra(Intent.EXTRA_TEXT, crashReport) - } - context.startActivity(intentSendTo) - onComplete() - } catch (_: ActivityNotFoundException) { - Toast.makeText(context, - context.getString(R.string.could_not_send_email), Toast.LENGTH_LONG).show() - onComplete() - } - } + val attached = try { + val fileRecord = OmsFileProvider.create(context, "crash_report.txt", false, "crash") + Files.write(fileRecord.path, report.toByteArray(StandardCharsets.UTF_8)) + sendIntent.putExtra(Intent.EXTRA_STREAM, fileRecord.uri) + sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + true } catch (ex: IOException) { - ex.printStackTrace() + Util.printStackTrace(ex) + false } -} \ No newline at end of file + + val body = context.getString(R.string.crash_email_body) + sendIntent.putExtra( + Intent.EXTRA_TEXT, + when { + report.length <= MAX_INLINE_REPORT_LENGTH -> "$body\n\n$report" + attached -> body + else -> "$body\n\n" + report.take(MAX_INLINE_REPORT_LENGTH) + } + ) + + context.startActivity(Intent.createChooser(sendIntent, null)) +} diff --git a/app/src/main/java/com/onemoresecret/CrashReportData.kt b/app/src/main/java/com/onemoresecret/CrashReportData.kt index b44a678..2c2719f 100644 --- a/app/src/main/java/com/onemoresecret/CrashReportData.kt +++ b/app/src/main/java/com/onemoresecret/CrashReportData.kt @@ -10,8 +10,9 @@ import java.io.PrintWriter import java.io.Serializable import java.io.StringWriter -class CrashReportData(private val throwable: Throwable?) : Serializable { - private val logcat: String? = getLogcat() +class CrashReportData(throwable: Throwable?, logcatTailLines: Int? = null) : Serializable { + private val stackTrace: String? = throwable?.let { renderStackTrace(it) } + private val logcat: String? = getLogcat(logcatTailLines) fun toString(includeLogcat: Boolean): String? { try { @@ -24,9 +25,9 @@ class CrashReportData(private val throwable: Throwable?) : Serializable { BuildConfig.FLAVOR ) ) - if (throwable != null) { + if (stackTrace != null) { pw.println("\n----- STACK TRACE -----") - throwable.printStackTrace(pw) + pw.println(stackTrace) } pw.println("\n----- DEVICE -----") pw.println("Brand: " + Build.BRAND) @@ -55,8 +56,19 @@ class CrashReportData(private val throwable: Throwable?) : Serializable { companion object { private val TAG: String = CrashReportData::class.java.simpleName - fun getLogcat(): String? { - return getProcessOutput("logcat", "-b", "all", "-d") + private fun renderStackTrace(t: Throwable): String = try { + StringWriter().use { sw -> + PrintWriter(sw).use { pw -> t.printStackTrace(pw) } + sw.toString() + } + } catch (_: Throwable) { + "(stack trace unavailable)" + } + + fun getLogcat(tailLines: Int? = null): String? = if (tailLines == null) { + getProcessOutput("logcat", "-b", "all", "-d") + } else { + getProcessOutput("logcat", "-b", "all", "-d", "-t", tailLines.toString()) } fun getProcessOutput(vararg sArr: String?): String? { diff --git a/app/src/main/java/com/onemoresecret/OmsFileProvider.kt b/app/src/main/java/com/onemoresecret/OmsFileProvider.kt index c9d7653..66160ac 100644 --- a/app/src/main/java/com/onemoresecret/OmsFileProvider.kt +++ b/app/src/main/java/com/onemoresecret/OmsFileProvider.kt @@ -18,8 +18,13 @@ class OmsFileProvider : FileProvider() { @JvmStatic @Throws(IOException::class) - fun create(ctx: Context, filename: String?, deleteOnExit: Boolean): FileRecord { - val dir = File(ctx.cacheDir, "tmp") + fun create( + ctx: Context, + filename: String?, + deleteOnExit: Boolean, + subdir: String? = null + ): FileRecord { + val dir = if (subdir == null) File(ctx.cacheDir, "tmp") else File(ctx.cacheDir, "tmp/$subdir") assert( dir.exists() || dir.mkdirs() //otherwise something went wrong ) diff --git a/app/src/main/java/com/onemoresecret/OmsUncaughtExceptionHandler.kt b/app/src/main/java/com/onemoresecret/OmsUncaughtExceptionHandler.kt index 5aee64e..635680d 100644 --- a/app/src/main/java/com/onemoresecret/OmsUncaughtExceptionHandler.kt +++ b/app/src/main/java/com/onemoresecret/OmsUncaughtExceptionHandler.kt @@ -9,7 +9,7 @@ class OmsUncaughtExceptionHandler(private val activity: Activity) : private val existingHandler = Thread.getDefaultUncaughtExceptionHandler() override fun uncaughtException(t: Thread, e: Throwable) { - val crashReportData = CrashReportData(e) + val crashReportData = CrashReportData(e, LOGCAT_TAIL_LINES) val intent = Intent(activity.applicationContext, CrashReportActivity::class.java).apply{ putExtra(EXTRA_CRASH_REPORT, crashReportData) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) @@ -25,5 +25,6 @@ class OmsUncaughtExceptionHandler(private val activity: Activity) : companion object { const val EXTRA_CRASH_REPORT: String = "CRASH_REPORT" + private const val LOGCAT_TAIL_LINES = 2000 } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af384d9..e6d86af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -81,6 +81,7 @@ Similar… Encrypt With: Share + Copy Decrypted Message Authentication was successful Cannot apply layout filter - no layout selected @@ -201,7 +202,6 @@ Ready to unlock oms4web ⚠️ this is a BETA version, see Release Notes 🚀 oms4web is here, give it a try! - Could not send email The report file has been attached. Please share the steps to reproduce the issue. OneMoreSecret crash report 4096 bit RSA diff --git a/app/src/standard/java/com/onemoresecret/qr/Analyzer.kt b/app/src/standard/java/com/onemoresecret/qr/Analyzer.kt index 3acf408..f25d836 100644 --- a/app/src/standard/java/com/onemoresecret/qr/Analyzer.kt +++ b/app/src/standard/java/com/onemoresecret/qr/Analyzer.kt @@ -17,36 +17,46 @@ class Analyzer { @OptIn(ExperimentalGetImage::class) fun analyze(imageProxy: ImageProxy, onQRCodeFound: Consumer) { - if (mlTask != null) return + if (mlTask != null) { + imageProxy.close() + return + } val mediaImage = imageProxy.image - if (mediaImage != null) { - if (barcodeScanner == null) { - barcodeScanner = BarcodeScanning.getClient( - BarcodeScannerOptions.Builder() - .setBarcodeFormats(Barcode.FORMAT_QR_CODE) - .build() + if (mediaImage == null) { + imageProxy.close() + } else { + try { + if (barcodeScanner == null) { + barcodeScanner = BarcodeScanning.getClient( + BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + ) + } + val inputImage = InputImage.fromMediaImage( + mediaImage, + imageProxy.imageInfo.rotationDegrees ) - } - val inputImage = InputImage.fromMediaImage( - mediaImage, - imageProxy.imageInfo.rotationDegrees - ) - mlTask = barcodeScanner!!.process(inputImage) - .addOnSuccessListener { barcodes -> - mlTask = null - imageProxy.close() - for (barcode in barcodes) { - onQRCodeFound.accept(barcode.rawValue) + mlTask = barcodeScanner!!.process(inputImage) + .addOnSuccessListener { barcodes -> + for (barcode in barcodes) { + onQRCodeFound.accept(barcode.rawValue) + } } - } - .addOnFailureListener { e -> - mlTask = null - imageProxy.close() - e.printStackTrace() - } + .addOnFailureListener { e -> + e.printStackTrace() + } + .addOnCompleteListener { + mlTask = null + imageProxy.close() + } + } catch (t: Throwable) { + imageProxy.close() + throw t + } } } }