fix: 添加 DebugLogger 日志库,修复启动图标缺失及多处崩溃

This commit is contained in:
xiaji
2026-05-20 22:56:17 +08:00
parent cf6a879494
commit 991c330203
13 changed files with 211 additions and 35 deletions

Binary file not shown.

View File

@@ -1,5 +1,11 @@
package com.example.androidruler package com.example.androidruler
import android.app.Application import android.app.Application
import com.example.androidruler.util.DebugLogger
class RulerApplication : Application() class RulerApplication : Application() {
override fun onCreate() {
super.onCreate()
DebugLogger.init(this)
}
}

View File

@@ -25,7 +25,8 @@ fun AndroidRulerTheme(
val view = LocalView.current val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {
SideEffect { SideEffect {
val window = (view.context as Activity).window val activity = view.context as? Activity ?: return@SideEffect
val window = activity.window
window.statusBarColor = colorScheme.primary.toArgb() window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
} }

View File

@@ -31,6 +31,8 @@ fun RulerCanvas(
val textMeasurer = rememberTextMeasurer() val textMeasurer = rememberTextMeasurer()
val isPortrait = LocalConfiguration.current.screenHeightDp > LocalConfiguration.current.screenWidthDp val isPortrait = LocalConfiguration.current.screenHeightDp > LocalConfiguration.current.screenWidthDp
if (pixelPerCm <= 0f) return
val scrollModifier = if (isPortrait) { val scrollModifier = if (isPortrait) {
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View File

@@ -10,6 +10,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -26,8 +30,24 @@ fun RulerScreen(
val context = LocalContext.current val context = LocalContext.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
val dpi = context.resources.displayMetrics.xdpi try {
viewModel.calculatePixelPerCm(dpi) val dpi = context.resources.displayMetrics.xdpi
viewModel.calculatePixelPerCm(dpi)
} catch (e: Exception) {
android.util.Log.e("RulerScreen", "Failed to calculate pixel per cm", e)
}
}
// guard: don't render ruler until pixelPerCm is ready
if (viewModel.pixelPerCm <= 0f) {
Box(modifier = modifier.fillMaxSize()) {
Text(
text = "初始化中...",
modifier = Modifier.align(Alignment.Center),
fontSize = 16.sp
)
}
return
} }
val measuredDistance = viewModel.measuredDistance val measuredDistance = viewModel.measuredDistance

View File

@@ -26,8 +26,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.example.androidruler.util.DebugLogger
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.concurrent.Executors
@Composable @Composable
fun CameraCapture( fun CameraCapture(
@@ -55,20 +55,29 @@ fun CameraCapture(
Button( Button(
onClick = { onClick = {
cameraController.takePicture( try {
ContextCompat.getMainExecutor(context), cameraController.takePicture(
object : ImageCapture.OnImageCapturedCallback() { ContextCompat.getMainExecutor(context),
override fun onCaptureSuccess(image: ImageProxy) { object : ImageCapture.OnImageCapturedCallback() {
val bitmap = imageProxyToBitmap(image) override fun onCaptureSuccess(image: ImageProxy) {
image.close() try {
bitmap?.let { onPhotoTaken(it) } val bitmap = imageProxyToBitmap(image)
} bitmap?.let { onPhotoTaken(it) }
} catch (e: Exception) {
DebugLogger.e("CameraCapture", "Failed to process captured image", e)
} finally {
image.close()
}
}
override fun onError(exception: ImageCaptureException) { override fun onError(exception: ImageCaptureException) {
exception.printStackTrace() DebugLogger.e("CameraCapture", "Capture error", exception)
}
} }
} )
) } catch (e: Exception) {
DebugLogger.e("CameraCapture", "takePicture failed", e)
}
}, },
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
@@ -83,13 +92,25 @@ fun CameraCapture(
} }
private fun imageProxyToBitmap(image: ImageProxy): Bitmap? { private fun imageProxyToBitmap(image: ImageProxy): Bitmap? {
val buffer: ByteBuffer = image.planes[0].buffer return try {
val bytes = ByteArray(buffer.remaining()) if (image.planes.isEmpty()) {
buffer.get(bytes) DebugLogger.w("CameraCapture", "ImageProxy has no planes")
return null
}
val buffer: ByteBuffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return null val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: run {
val matrix = Matrix().apply { DebugLogger.w("CameraCapture", "Failed to decode image bytes")
postRotate(image.imageInfo.rotationDegrees.toFloat()) return null
}
val matrix = Matrix().apply {
postRotate(image.imageInfo.rotationDegrees.toFloat())
}
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
} catch (e: Exception) {
DebugLogger.e("CameraCapture", "imageProxyToBitmap failed", e)
null
} }
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
} }

View File

@@ -0,0 +1,115 @@
package com.example.androidruler.util
import android.content.Context
import android.os.Build
import android.os.Environment
import java.io.File
import java.io.FileWriter
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.system.exitProcess
object DebugLogger {
private var logFile: File? = null
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
private var initialized = false
fun init(context: Context) {
if (initialized) return
initialized = true
try {
val logDir = File(context.getExternalFilesDir(null), "logs")
logDir.mkdirs()
logFile = File(logDir, "ruler_debug_${System.currentTimeMillis()}.log")
logFile?.createNewFile()
d("DebugLogger", "Logger initialized. SDK=${Build.VERSION.SDK_INT}, Model=${Build.MODEL}, Manufacturer=${Build.MANUFACTURER}")
// global crash handler
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
e("CrashHandler", "Uncaught exception in thread '${thread.name}'", throwable)
flush()
// pass to default handler
defaultHandler?.uncaughtException(thread, throwable)
// if no default handler, kill process
exitProcess(1)
}
} catch (e: Exception) {
android.util.Log.e("DebugLogger", "Failed to initialize logger", e)
}
}
@JvmStatic
fun d(tag: String, message: String) {
log("DEBUG", tag, message, null)
}
@JvmStatic
fun i(tag: String, message: String) {
log("INFO", tag, message, null)
}
@JvmStatic
fun w(tag: String, message: String, throwable: Throwable? = null) {
log("WARN", tag, message, throwable)
}
@JvmStatic
fun e(tag: String, message: String, throwable: Throwable? = null) {
log("ERROR", tag, message, throwable)
}
private fun log(level: String, tag: String, message: String, throwable: Throwable?) {
val timestamp = synchronized(dateFormat) { dateFormat.format(Date()) }
val sb = StringBuilder()
sb.append("$timestamp [$level] $tag: $message")
if (throwable != null) {
sb.append("\n")
val sw = StringWriter()
throwable.printStackTrace(PrintWriter(sw))
sb.append(sw.toString())
}
val line = sb.toString()
// write to android logcat
when (level) {
"DEBUG" -> android.util.Log.d(tag, line)
"INFO" -> android.util.Log.i(tag, line)
"WARN" -> android.util.Log.w(tag, line)
"ERROR" -> android.util.Log.e(tag, line)
}
// write to file
try {
logFile?.let { file ->
FileWriter(file, true).use { writer ->
writer.appendLine(line)
}
}
} catch (_: Exception) {
// best effort
}
}
fun flush() {
// file writes are already flushed via use{}
}
fun getLogFilePath(): String? = logFile?.absolutePath
fun getRecentLogs(maxLines: Int = 200): String {
return try {
logFile?.readLines()?.takeLast(maxLines)?.joinToString("\n") ?: "No logs available"
} catch (e: Exception) {
"Failed to read logs: ${e.message}"
}
}
}

View File

@@ -9,6 +9,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.androidruler.data.SettingsRepository import com.example.androidruler.data.SettingsRepository
import com.example.androidruler.util.DebugLogger
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.abs import kotlin.math.abs
@@ -36,10 +37,15 @@ class RulerViewModel(application: Application) : AndroidViewModel(application) {
init { init {
viewModelScope.launch { viewModelScope.launch {
cmCorrectionFactor = repo.correctionFactor.first() try {
isCalibrated = repo.isCalibrated.first() cmCorrectionFactor = repo.correctionFactor.first()
defaultOrientation = repo.defaultOrientation.first() isCalibrated = repo.isCalibrated.first()
overlayRulerLength = repo.defaultRulerLength.first() defaultOrientation = repo.defaultOrientation.first()
overlayRulerLength = repo.defaultRulerLength.first()
DebugLogger.i("RulerViewModel", "Settings loaded: correctionFactor=$cmCorrectionFactor, calibrated=$isCalibrated")
} catch (e: Exception) {
DebugLogger.e("RulerViewModel", "Failed to load settings, using defaults", e)
}
} }
} }
@@ -77,13 +83,18 @@ class RulerViewModel(application: Application) : AndroidViewModel(application) {
correctionFactor: Float correctionFactor: Float
) { ) {
viewModelScope.launch { viewModelScope.launch {
defaultOrientation = orientation try {
overlayRulerLength = rulerLength defaultOrientation = orientation
cmCorrectionFactor = correctionFactor overlayRulerLength = rulerLength
repo.setDefaultOrientation(orientation) cmCorrectionFactor = correctionFactor
repo.setDefaultRulerLength(rulerLength) repo.setDefaultOrientation(orientation)
repo.setCorrectionFactor(correctionFactor) repo.setDefaultRulerLength(rulerLength)
isCalibrated = true repo.setCorrectionFactor(correctionFactor)
isCalibrated = true
DebugLogger.i("RulerViewModel", "Settings saved: orientation=$orientation, length=$rulerLength, factor=$correctionFactor")
} catch (e: Exception) {
DebugLogger.e("RulerViewModel", "Failed to save settings", e)
}
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB