diff --git a/app/src/main/java/com/sw/dualscreen/MyApp.kt b/app/src/main/java/com/sw/dualscreen/MyApp.kt index 5610913..0856516 100644 --- a/app/src/main/java/com/sw/dualscreen/MyApp.kt +++ b/app/src/main/java/com/sw/dualscreen/MyApp.kt @@ -1,5 +1,6 @@ package com.sw.dualscreen +import com.sw.dualscreen.utils.CrashHandler import com.sw.plate.App import com.sw.plate.utils.AppUtil import timber.log.Timber @@ -16,5 +17,8 @@ class MyApp : App() { Timber.d("UDID = ${AppUtil.getUDID(this)}") deviceId = "3ea47dc0-3cf0-3c2f-909c-265a9a65572e" GlobalData.deviceId = deviceId + + // 初始化崩溃处理器 + CrashHandler.init(this) } } \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/sdk/SensorScaleUtils.kt b/app/src/main/java/com/sw/dualscreen/sdk/SensorScaleUtils.kt index 3a6cdcd..5304d2f 100644 --- a/app/src/main/java/com/sw/dualscreen/sdk/SensorScaleUtils.kt +++ b/app/src/main/java/com/sw/dualscreen/sdk/SensorScaleUtils.kt @@ -114,9 +114,9 @@ object SensorScaleUtils { * 零位标定 */ fun zero() { - Timber.d("zero isZero = $isZero") - if (isZero) return - isZero = true + Timber.d("zero isOpened = $isOpened") + if (!isOpened) return +// isZero = true mSensorScale?.zero { Timber.d("零位标定操作成功") ToastUtils.showToast("零位标定操作成功") diff --git a/app/src/main/java/com/sw/dualscreen/utils/CrashHandler.kt b/app/src/main/java/com/sw/dualscreen/utils/CrashHandler.kt new file mode 100644 index 0000000..d6a5331 --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/utils/CrashHandler.kt @@ -0,0 +1,246 @@ +package com.sw.dualscreen.utils + +import android.app.ActivityManager +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.os.Process +import com.sw.dualscreen.activity.MainActivity +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.PrintWriter +import java.io.StringWriter +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.apply +import kotlin.collections.forEach +import kotlin.io.bufferedReader +import kotlin.io.readLines +import kotlin.io.use +import kotlin.jvm.java +import kotlin.run +import kotlin.system.exitProcess +import kotlin.text.endsWith +import kotlin.text.toByteArray + +/** + * 崩溃处理 + */ +class CrashHandler private constructor(private val context: Context) : + Thread.UncaughtExceptionHandler { + + companion object { + private const val TAG = "CrashHandler" + private const val CRASH_REPORTS_DIR = "crash_reports" + private const val LOG_LINES = 500 // 收集最近500行日志 + + @Volatile + private var instance: CrashHandler? = null + + fun init(context: Context) { + if (instance == null) { + synchronized(CrashHandler::class.java) { + if (instance == null) { + instance = CrashHandler(context.applicationContext) + } + } + } + } + + fun getCrashReportFiles(context: Context): Array { + val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR) + return if (crashDir.exists() && crashDir.isDirectory) { + crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray() + } else { + emptyArray() + } + } + + fun clearCrashReports(context: Context) { + getCrashReportFiles(context).forEach { it.delete() } + } + } + + private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() + + init { + Thread.setDefaultUncaughtExceptionHandler(this) + } + + override fun uncaughtException(thread: Thread, ex: Throwable) { + handleException(thread, ex) + + // 如果系统提供了默认的异常处理器,则交给系统去结束程序 + // 否则自己结束程序 + defaultHandler?.uncaughtException(thread, ex) ?: run { + Process.killProcess(Process.myPid()) + exitProcess(1) + } + } + + /** + * 自动重启app + */ + private fun restartApp() { + // 延迟1秒后重启应用 + Handler(Looper.getMainLooper()).postDelayed({ + val intent = Intent(context, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + val pendingIntent = PendingIntent.getActivity( + context, 0, intent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent) + + Process.killProcess(Process.myPid()) + exitProcess(1) + }, 1000) + } + + private fun handleException(thread: Thread, ex: Throwable) { + // 收集设备信息和异常信息 + val crashInfo = collectCrashInfo(thread, ex) + + // 保存日志文件 + saveCrashInfoToFile(crashInfo) + + // 这里可以添加其他处理逻辑,比如上传到服务器等 + } + + private fun collectCrashInfo(thread: Thread, ex: Throwable): String { + return buildString { + // 收集设备信息 + collectDeviceInfo(this) + + // 收集应用日志 + append("\n\n").append(collectLogs()) + + // 收集线程和异常信息 + append("\n\n========== Thread & Exception Info ==========\n") + append("Thread: ${thread.name}\n") + append("Stack Trace:\n") + + val sw = StringWriter() + val pw = PrintWriter(sw) + ex.printStackTrace(pw) + var cause: Throwable? = ex.cause + while (cause != null) { + cause.printStackTrace(pw) + cause = cause.cause + } + pw.close() + append(sw.toString()) + } + } + + private fun collectDeviceInfo(sb: StringBuilder) { + sb.append("========== Device Info ==========\n") + try { + // 应用信息 + val pm = context.packageManager + val pi = pm.getPackageInfo(context.packageName, 0) + + sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n") + + // Android 版本信息 + sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n") + + // 设备信息 + sb.append("Vendor: ${Build.MANUFACTURER}\n") + sb.append("Model: ${Build.MODEL}\n") + sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n") + + // 其他信息 + sb.append("Locale: ${Locale.getDefault()}\n") + sb.append( + "Current Time: ${ + SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss", + Locale.getDefault() + ).format(Date()) + }\n" + ) + + // 内存信息 + val memoryInfo = ActivityManager.MemoryInfo() + val activityManager = + context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + activityManager.getMemoryInfo(memoryInfo) + sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n") + sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n") + sb.append("Low Memory: ${memoryInfo.lowMemory}\n") + + } catch (e: Exception) { + Timber.e(e, "Error while collecting device info") + sb.append("Error while collecting device info: ${e.message}\n") + } + } + + private fun collectLogs(): String { + return buildString { + append("========== Application Logs ==========\n") + try { + val process = Runtime.getRuntime().exec("logcat -d -v threadtime") + val reader = process.inputStream.bufferedReader() + + val logLines = reader.readLines() + val start = kotlin.comparisons.maxOf(0, logLines.size - LOG_LINES) + + logLines.subList(start, logLines.size).forEach { + append(it).append("\n") + } + } catch (e: IOException) { + Timber.e(e, "Error collecting logs") + append("Error collecting logs: ${e.message}\n") + } + } + } + + private fun saveCrashInfoToFile(crashInfo: String) { + try { + val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date()) + val fileName = "crash_$time.log" + + val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR) + if (!crashDir.exists() && !crashDir.mkdirs()) { + Timber.tag(TAG).e("Failed to create crash report directory") + return + } + + val crashFile = File(crashDir, fileName) + FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) } + + Timber.tag(TAG).d("Crash info saved to: ${crashFile.absolutePath}") + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Error saving crash info to file") + } + } + + /** + * 清理旧的崩溃日志 + */ + fun cleanupOldCrashReports(maxAgeDays: Int = 7) { + val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR) + if (!crashDir.exists() || !crashDir.isDirectory) return + + val now = System.currentTimeMillis() + val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L + + crashDir.listFiles()?.forEach { file -> + if (file.lastModified() < now - maxAgeMillis) { + file.delete() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/utils/KeyboardUtils.kt b/app/src/main/java/com/sw/dualscreen/utils/KeyboardUtils.kt new file mode 100644 index 0000000..ef233e6 --- /dev/null +++ b/app/src/main/java/com/sw/dualscreen/utils/KeyboardUtils.kt @@ -0,0 +1,59 @@ +package com.sw.dualscreen.utils + +import android.app.Activity +import android.content.Context +import android.view.View +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.EditText + +/** + * 键盘工具类 + */ +object KeyboardUtils { + + /** + * 显示键盘 + */ + fun showKeyboard(editText: EditText) { + editText.requestFocus() + val imm = editText.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) + } + + /** + * 隐藏键盘 + */ + fun hideKeyboard(activity: Activity) { + val view = activity.currentFocus ?: activity.window.decorView + hideKeyboard(view) + } + + /** + * 隐藏键盘 + */ + fun hideKeyboard(view: View) { + val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) + } + + /** + * 为EditText设置EditorAction监听并隐藏键盘 + */ + fun setupEditorAction(editText: EditText, onAction: (() -> Unit)? = null) { + editText.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE || + actionId == EditorInfo.IME_ACTION_SEARCH || + actionId == EditorInfo.IME_ACTION_GO || + actionId == EditorInfo.IME_ACTION_SEND || + actionId == EditorInfo.IME_ACTION_NEXT) { + + hideKeyboard(editText) + onAction?.invoke() + true + } else { + false + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sw/dualscreen/view/CustomBottomSheetDialog.kt b/app/src/main/java/com/sw/dualscreen/view/CustomBottomSheetDialog.kt index 1b2a4cc..0e6d904 100644 --- a/app/src/main/java/com/sw/dualscreen/view/CustomBottomSheetDialog.kt +++ b/app/src/main/java/com/sw/dualscreen/view/CustomBottomSheetDialog.kt @@ -20,6 +20,7 @@ import com.sw.dualscreen.databinding.BottomSheetDialogBinding import com.sw.dualscreen.databinding.ItemSearchFoodInfoBinding import com.sw.dualscreen.model.response.FoodInfo import com.sw.dualscreen.utils.Debouncer +import com.sw.dualscreen.utils.KeyboardUtils import com.sw.dualscreen.viewmodel.UserViewModel import kotlinx.coroutines.launch import timber.log.Timber @@ -95,13 +96,8 @@ class CustomBottomSheetDialog( binding.ivSearch.setOnClickListener { searchInfo() } - binding.etSearch.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - searchInfo() - true - } else { - false - } + KeyboardUtils.setupEditorAction(binding.etSearch) { + searchInfo() } registerDataChange() } diff --git a/app/src/main/res/drawable/bottom_sheet_background.xml b/app/src/main/res/drawable/bottom_sheet_background.xml index aa48aef..0c88412 100644 --- a/app/src/main/res/drawable/bottom_sheet_background.xml +++ b/app/src/main/res/drawable/bottom_sheet_background.xml @@ -2,6 +2,6 @@ + android:topLeftRadius="60dp" + android:topRightRadius="60dp" /> \ No newline at end of file