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
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
if (!view.isInEditMode) {
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()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}

View File

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

View File

@@ -10,6 +10,10 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.Modifier
import androidx.compose.ui.graphics.Color
@@ -26,8 +30,24 @@ fun RulerScreen(
val context = LocalContext.current
LaunchedEffect(Unit) {
val dpi = context.resources.displayMetrics.xdpi
viewModel.calculatePixelPerCm(dpi)
try {
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

View File

@@ -26,8 +26,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import com.example.androidruler.util.DebugLogger
import java.nio.ByteBuffer
import java.util.concurrent.Executors
@Composable
fun CameraCapture(
@@ -55,20 +55,29 @@ fun CameraCapture(
Button(
onClick = {
cameraController.takePicture(
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
val bitmap = imageProxyToBitmap(image)
image.close()
bitmap?.let { onPhotoTaken(it) }
}
try {
cameraController.takePicture(
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageCapturedCallback() {
override fun onCaptureSuccess(image: ImageProxy) {
try {
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) {
exception.printStackTrace()
override fun onError(exception: ImageCaptureException) {
DebugLogger.e("CameraCapture", "Capture error", exception)
}
}
}
)
)
} catch (e: Exception) {
DebugLogger.e("CameraCapture", "takePicture failed", e)
}
},
modifier = Modifier
.align(Alignment.BottomCenter)
@@ -83,13 +92,25 @@ fun CameraCapture(
}
private fun imageProxyToBitmap(image: ImageProxy): Bitmap? {
val buffer: ByteBuffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
return try {
if (image.planes.isEmpty()) {
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 matrix = Matrix().apply {
postRotate(image.imageInfo.rotationDegrees.toFloat())
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: run {
DebugLogger.w("CameraCapture", "Failed to decode image bytes")
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.viewModelScope
import com.example.androidruler.data.SettingsRepository
import com.example.androidruler.util.DebugLogger
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlin.math.abs
@@ -36,10 +37,15 @@ class RulerViewModel(application: Application) : AndroidViewModel(application) {
init {
viewModelScope.launch {
cmCorrectionFactor = repo.correctionFactor.first()
isCalibrated = repo.isCalibrated.first()
defaultOrientation = repo.defaultOrientation.first()
overlayRulerLength = repo.defaultRulerLength.first()
try {
cmCorrectionFactor = repo.correctionFactor.first()
isCalibrated = repo.isCalibrated.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
) {
viewModelScope.launch {
defaultOrientation = orientation
overlayRulerLength = rulerLength
cmCorrectionFactor = correctionFactor
repo.setDefaultOrientation(orientation)
repo.setDefaultRulerLength(rulerLength)
repo.setCorrectionFactor(correctionFactor)
isCalibrated = true
try {
defaultOrientation = orientation
overlayRulerLength = rulerLength
cmCorrectionFactor = correctionFactor
repo.setDefaultOrientation(orientation)
repo.setDefaultRulerLength(rulerLength)
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