fix: 添加 DebugLogger 日志库,修复启动图标缺失及多处崩溃
This commit is contained in:
Binary file not shown.
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
115
app/src/main/java/com/example/androidruler/util/DebugLogger.kt
Normal file
115
app/src/main/java/com/example/androidruler/util/DebugLogger.kt
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 932 B |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Reference in New Issue
Block a user