开发者
This commit is contained in:
parent
a87aac59ce
commit
3302225e74
|
|
@ -47,11 +47,17 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.StandAPP" />
|
android:theme="@style/Theme.StandAPP" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".DeveloperActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="开发者模式"
|
||||||
|
android:theme="@style/Theme.StandAPP" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".printer.PrintTestActivity"
|
android:name=".printer.PrintTestActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="打印测试"
|
android:label="打印测试"
|
||||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
|
android:theme="@style/Theme.StandAPP" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.example.kingway.ptt.pttActivity"
|
android:name="com.example.kingway.ptt.pttActivity"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.stand.standapp
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
|
class DeveloperActivity : AppCompatActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_developer)
|
||||||
|
|
||||||
|
// 左上角退出
|
||||||
|
findViewById<ImageView>(R.id.btn_close).setOnClickListener {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PTT 测试
|
||||||
|
findViewById<LinearLayout>(R.id.item_ptt).setOnClickListener {
|
||||||
|
startActivity(Intent(this, com.example.kingway.ptt.pttActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印测试
|
||||||
|
findViewById<LinearLayout>(R.id.item_printer).setOnClickListener {
|
||||||
|
startActivity(Intent(this, com.stand.standapp.printer.PrintTestActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 崩溃模拟
|
||||||
|
findViewById<LinearLayout>(R.id.item_crash_test).setOnClickListener {
|
||||||
|
androidx.appcompat.app.AlertDialog.Builder(this)
|
||||||
|
.setTitle("确认崩溃测试?")
|
||||||
|
.setMessage("应用将立即崩溃并尝试记录日志,确定继续吗?")
|
||||||
|
.setPositiveButton("确定") { _, _ ->
|
||||||
|
throw RuntimeException("这是一次开发者手动触发的模拟崩溃测试")
|
||||||
|
}
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载调试信息
|
||||||
|
loadDebugInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDebugInfo() {
|
||||||
|
val tvInfo = findViewById<TextView>(R.id.tv_debug_info)
|
||||||
|
val info = StringBuilder()
|
||||||
|
info.append("设备型号: ${Build.MODEL}\n")
|
||||||
|
info.append("系统版本: Android ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})\n")
|
||||||
|
info.append("架构(ABI): ${Build.SUPPORTED_ABIS.joinToString(", ")}\n")
|
||||||
|
info.append("运行模式: ${if (android.os.Process.is64Bit()) "64位" else "32位兼容模式"}\n")
|
||||||
|
info.append("服务器地址: ${AppConfig.getServerUrl(this)}\n")
|
||||||
|
info.append("串口路径: ${AppConfig.getSerialPort(this)}")
|
||||||
|
|
||||||
|
tvInfo.text = info.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
package com.stand.standapp
|
package com.stand.standapp
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import timber.log.Timber
|
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.stand.standapp.printer.PrinterManager
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
@ -23,9 +24,9 @@ class LoginActivity : AppCompatActivity() {
|
||||||
val etPassword = findViewById<EditText>(R.id.et_password)
|
val etPassword = findViewById<EditText>(R.id.et_password)
|
||||||
val cbRemember = findViewById<CheckBox>(R.id.cb_remember)
|
val cbRemember = findViewById<CheckBox>(R.id.cb_remember)
|
||||||
val btnLogin = findViewById<Button>(R.id.btn_login)
|
val btnLogin = findViewById<Button>(R.id.btn_login)
|
||||||
val btnPttExample = findViewById<Button>(R.id.btn_ptt_example)
|
val btnExit = findViewById<Button>(R.id.btn_exit)
|
||||||
val btnPrinterTest = findViewById<Button>(R.id.btn_printer_test)
|
|
||||||
val ivSettings = findViewById<ImageView>(R.id.iv_settings)
|
val ivSettings = findViewById<ImageView>(R.id.iv_settings)
|
||||||
|
val tvCopyright = findViewById<TextView>(R.id.tv_copyright)
|
||||||
|
|
||||||
// 1. 加载保存的状态和凭据
|
// 1. 加载保存的状态和凭据
|
||||||
val isRemember = AppConfig.isRememberLogin(this)
|
val isRemember = AppConfig.isRememberLogin(this)
|
||||||
|
|
@ -54,34 +55,37 @@ class LoginActivity : AppCompatActivity() {
|
||||||
performLogin(username, password, cbRemember.isChecked)
|
performLogin(username, password, cbRemember.isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. PTT 示例按钮点击事件
|
// 4. 退出按钮
|
||||||
btnPttExample.setOnClickListener {
|
btnExit.setOnClickListener {
|
||||||
try {
|
finish()
|
||||||
val intent = Intent(this, com.example.kingway.ptt.pttActivity::class.java)
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(this, "打开 PTT 示例失败: ${e.message}", Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 打印测试按钮点击事件
|
// 5. 彩蛋:点击底部版权 10 次进入开发者模式
|
||||||
btnPrinterTest.setOnClickListener {
|
var clickCount = 0
|
||||||
try {
|
var lastClickTime = 0L
|
||||||
val intent = Intent(this, com.stand.standapp.printer.PrintTestActivity::class.java)
|
tvCopyright.setOnClickListener {
|
||||||
startActivity(intent)
|
val currentTime = System.currentTimeMillis()
|
||||||
} catch (e: Exception) {
|
// 连续点击判定间隔为 500ms
|
||||||
Toast.makeText(this, "打开打印测试失败: ${e.message}", Toast.LENGTH_SHORT).show()
|
if (currentTime - lastClickTime < 500) {
|
||||||
|
clickCount++
|
||||||
|
} else {
|
||||||
|
clickCount = 1
|
||||||
|
}
|
||||||
|
lastClickTime = currentTime
|
||||||
|
|
||||||
|
if (clickCount >= 10) {
|
||||||
|
clickCount = 0
|
||||||
|
Toast.makeText(this, "开发者模式已激活", Toast.LENGTH_SHORT).show()
|
||||||
|
startActivity(Intent(this, DeveloperActivity::class.java))
|
||||||
|
} else if (clickCount > 5) {
|
||||||
|
Toast.makeText(this, "再点击 ${10 - clickCount} 次进入调试模式", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行真实接口登录
|
|
||||||
*/
|
|
||||||
private fun performLogin(username: String, password: String, remember: Boolean) {
|
private fun performLogin(username: String, password: String, remember: Boolean) {
|
||||||
val serverUrl = AppConfig.getServerUrl(this)
|
val serverUrl = AppConfig.getServerUrl(this)
|
||||||
val loginUrl = "$serverUrl/api/auth/login"
|
val loginUrl = "$serverUrl/api/auth/login"
|
||||||
val bearerToken = "eyJraWQiOiJzLXYxIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhZG1pbiIsInR5cCI6ImFjY2VzcyIsImV4cCI6MTc3NjQwMTU2MSwianRpIjoiMjM0NjdiZGMtNTY0Zi00NmY0LWJhZTgtNzJhZmQ1MzhmYmZhIiwiZmdwIjoiY2FhNzYzOTI4YWRhYWZiYjgzMjE3NjExYmVjYTc3ZjMiLCJraWQiOiJzLXYxIn0.4Zf1eiKcUjGNoFylEzmZ2no1kJY-44CQ8QWmMA_UxvY"
|
|
||||||
|
|
||||||
val body = MultipartBody.Builder()
|
val body = MultipartBody.Builder()
|
||||||
.setType(MultipartBody.FORM)
|
.setType(MultipartBody.FORM)
|
||||||
|
|
@ -92,7 +96,6 @@ class LoginActivity : AppCompatActivity() {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(loginUrl)
|
.url(loginUrl)
|
||||||
.post(body)
|
.post(body)
|
||||||
.addHeader("Authorization", "Bearer $bearerToken")
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val loadingDialog = AlertDialog.Builder(this)
|
val loadingDialog = AlertDialog.Builder(this)
|
||||||
|
|
@ -117,7 +120,6 @@ class LoginActivity : AppCompatActivity() {
|
||||||
val json = JSONObject(responseBody ?: "")
|
val json = JSONObject(responseBody ?: "")
|
||||||
val code = json.optInt("code", -1)
|
val code = json.optInt("code", -1)
|
||||||
if (code == 0) {
|
if (code == 0) {
|
||||||
// 登录成功
|
|
||||||
AppConfig.setRememberLogin(this@LoginActivity, remember)
|
AppConfig.setRememberLogin(this@LoginActivity, remember)
|
||||||
if (remember) {
|
if (remember) {
|
||||||
AppConfig.setSavedUsername(this@LoginActivity, username)
|
AppConfig.setSavedUsername(this@LoginActivity, username)
|
||||||
|
|
@ -127,12 +129,12 @@ class LoginActivity : AppCompatActivity() {
|
||||||
AppConfig.setSavedPassword(this@LoginActivity, "")
|
AppConfig.setSavedPassword(this@LoginActivity, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
// 初始化打印机 (自动重连)
|
// 初始化打印机 (异步)
|
||||||
com.stand.standapp.printer.PrinterManager.init(this@LoginActivity)
|
PrinterManager.init(this@LoginActivity)
|
||||||
|
|
||||||
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
val msg = json.optString("msg", "登录失败")
|
val msg = json.optString("msg", "登录失败")
|
||||||
|
|
@ -153,7 +155,6 @@ class LoginActivity : AppCompatActivity() {
|
||||||
val etSerialPort = dialogView.findViewById<EditText>(R.id.et_serial_port)
|
val etSerialPort = dialogView.findViewById<EditText>(R.id.et_serial_port)
|
||||||
val btnReset = dialogView.findViewById<TextView>(R.id.btn_reset)
|
val btnReset = dialogView.findViewById<TextView>(R.id.btn_reset)
|
||||||
|
|
||||||
// 初始化数据
|
|
||||||
etServerUrl.setText(AppConfig.getServerUrl(this))
|
etServerUrl.setText(AppConfig.getServerUrl(this))
|
||||||
etSerialPort.setText(AppConfig.getSerialPort(this))
|
etSerialPort.setText(AppConfig.getSerialPort(this))
|
||||||
|
|
||||||
|
|
@ -190,10 +191,9 @@ class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
// 强制设置对话框宽度
|
|
||||||
dialog.window?.let { window ->
|
dialog.window?.let { window ->
|
||||||
val params = window.attributes
|
val params = window.attributes
|
||||||
params.width = (resources.displayMetrics.widthPixels * 0.75).toInt() // 占据屏幕 75% 宽度
|
params.width = (resources.displayMetrics.widthPixels * 0.95).toInt()
|
||||||
window.attributes = params
|
window.attributes = params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,10 +202,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle("提示")
|
.setTitle("提示")
|
||||||
.setMessage("确定要退出应用吗?")
|
.setMessage("确定要退出应用吗?")
|
||||||
.setPositiveButton("确定") { _, _ ->
|
.setPositiveButton("确定") { _, _ -> finish() }
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
super.onBackPressed()
|
|
||||||
}
|
|
||||||
.setNegativeButton("取消", null)
|
.setNegativeButton("取消", null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,58 +24,99 @@ object LogManager {
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logDir = File(context.getExternalFilesDir(null), "logs")
|
// 1. 路径确定
|
||||||
if (logDir?.exists() == false) logDir?.mkdirs()
|
val baseDir = context.getExternalFilesDir(null)
|
||||||
|
logDir = if (baseDir != null) {
|
||||||
|
File(baseDir, "logs")
|
||||||
|
} else {
|
||||||
|
File(context.filesDir, "logs")
|
||||||
|
}
|
||||||
|
|
||||||
// 立即清理旧日志
|
if (logDir?.exists() == false) {
|
||||||
logDir?.let { cleanOldLogs(it) }
|
val created = logDir?.mkdirs()
|
||||||
|
android.util.Log.i("LogManager", "Directory created: $created at ${logDir?.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
// 挂载文件日志树
|
// 2. 强行创建一个“自检”日志文件,不使用异步线程池
|
||||||
logDir?.let { Timber.plant(FileLoggingTree(it)) }
|
logDir?.let { dir ->
|
||||||
|
val testFile = File(dir, "startup_check.txt")
|
||||||
|
try {
|
||||||
|
val fos = FileOutputStream(testFile, true)
|
||||||
|
fos.write("Logging started at ${Date()}\n".toByteArray())
|
||||||
|
fos.close()
|
||||||
|
android.util.Log.i("LogManager", "Startup check file written successfully")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("LogManager", "FAILED to write startup check file", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logDir?.let {
|
||||||
|
cleanOldLogs(it)
|
||||||
|
Timber.plant(FileLoggingTree(it))
|
||||||
|
}
|
||||||
|
|
||||||
// 【核心增强】接管全局异常,确保崩溃信息能写进文件
|
|
||||||
setupCrashHandler()
|
setupCrashHandler()
|
||||||
|
|
||||||
Timber.tag("LogManager").i("Logging system initialized and CrashHandler attached")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("LogManager", "Init failed", e)
|
android.util.Log.e("LogManager", "Init global failure", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupCrashHandler() {
|
private fun setupCrashHandler() {
|
||||||
|
// 1. 捕获所有普通线程、子线程的未捕获异常
|
||||||
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||||
// 强制同步写入崩溃日志
|
saveCrashToFile(thread, throwable)
|
||||||
val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())
|
defaultHandler?.uncaughtException(thread, throwable) ?: exitProcess(1)
|
||||||
val crashInfo = "\n\n=== FATAL CRASH ===\n" +
|
}
|
||||||
"Time: $timestamp\n" +
|
|
||||||
"Thread: ${thread.name}\n" +
|
|
||||||
"Stacktrace:\n${throwable.stackTraceToString()}\n" +
|
|
||||||
"====================\n\n"
|
|
||||||
|
|
||||||
logDir?.let { dir ->
|
// 2. 捕获主线程 (UI 线程) 的所有异常
|
||||||
val logFile = File(dir, SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) + ".log")
|
// 这种方式能捕获点击事件、生命周期回调中发生的所有崩溃
|
||||||
|
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
||||||
|
while (true) {
|
||||||
try {
|
try {
|
||||||
FileOutputStream(logFile, true).use { it.write(crashInfo.toByteArray()) }
|
android.os.Looper.loop()
|
||||||
} catch (e: Exception) {
|
} catch (e: Throwable) {
|
||||||
android.util.Log.e("LogManager", "Failed to write crash log", e)
|
saveCrashToFile(android.os.Looper.getMainLooper().thread, e)
|
||||||
|
// 如果你希望应用在崩溃后不闪退而是尝试恢复,可以不抛出,但建议这里还是交还给系统处理比较稳妥
|
||||||
|
// 这里我们为了记录完日志后让系统正常走关闭流程
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 交还给系统处理(弹出“应用已停止”对话框)
|
|
||||||
defaultHandler?.uncaughtException(thread, throwable) ?: exitProcess(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanOldLogs(logDir: File) {
|
private fun saveCrashToFile(thread: Thread, throwable: Throwable) {
|
||||||
|
// 使用同步写入,确保磁盘 IO 优先
|
||||||
|
val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())
|
||||||
|
val crashInfo = "\n\nCRASH_ERROR_REPORT\n" +
|
||||||
|
"Time: $timestamp\n" +
|
||||||
|
"Thread: ${thread.name}\n" +
|
||||||
|
"Cause: ${throwable.cause}\n" +
|
||||||
|
"Message: ${throwable.message}\n" +
|
||||||
|
"Full Stacktrace:\n${throwable.stackTraceToString()}\n" +
|
||||||
|
"-------------------\n\n"
|
||||||
|
|
||||||
|
logDir?.let { dir ->
|
||||||
|
try {
|
||||||
|
val logFile = File(dir, SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) + ".log")
|
||||||
|
val fos = FileOutputStream(logFile, true)
|
||||||
|
fos.write(crashInfo.toByteArray())
|
||||||
|
fos.flush()
|
||||||
|
fos.fd.sync()
|
||||||
|
fos.close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("LogManager", "Failed to save crash log", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanOldLogs(dir: File) {
|
||||||
executor.execute {
|
executor.execute {
|
||||||
try {
|
try {
|
||||||
val calendar = Calendar.getInstance()
|
val thresholdDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -MAX_DAYS) }.time
|
||||||
calendar.add(Calendar.DAY_OF_YEAR, -MAX_DAYS)
|
dir.listFiles()?.forEach { file ->
|
||||||
val thresholdDate = calendar.time
|
if (file.isFile && file.name.endsWith(".log") && Date(file.lastModified()).before(thresholdDate)) {
|
||||||
logDir.listFiles()?.forEach { file ->
|
file.delete()
|
||||||
if (file.isFile && file.name.endsWith(".log")) {
|
|
||||||
if (Date(file.lastModified()).before(thresholdDate)) file.delete()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {}
|
} catch (e: Exception) {}
|
||||||
|
|
@ -84,7 +125,6 @@ object LogManager {
|
||||||
|
|
||||||
fun zipLogs(context: Context): String? {
|
fun zipLogs(context: Context): String? {
|
||||||
val dir = logDir ?: return null
|
val dir = logDir ?: return null
|
||||||
if (!dir.exists() || dir.listFiles().isNullOrEmpty()) return null
|
|
||||||
val zipFile = File(context.getExternalFilesDir(null), "logs_${System.currentTimeMillis()}.zip")
|
val zipFile = File(context.getExternalFilesDir(null), "logs_${System.currentTimeMillis()}.zip")
|
||||||
return try {
|
return try {
|
||||||
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
|
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
|
||||||
|
|
@ -97,48 +137,29 @@ object LogManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
zipFile.absolutePath
|
zipFile.absolutePath
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) { null }
|
||||||
Timber.e(e, "Zip logs failed")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uploadLogs(context: Context, callback: (Boolean, String) -> Unit) {
|
fun uploadLogs(context: Context, callback: (Boolean, String) -> Unit) {
|
||||||
executor.execute {
|
executor.execute {
|
||||||
val zipPath = zipLogs(context)
|
val zipPath = zipLogs(context) ?: return@execute callback(false, "打包失败")
|
||||||
if (zipPath == null) {
|
|
||||||
callback(false, "打包日志失败")
|
|
||||||
return@execute
|
|
||||||
}
|
|
||||||
|
|
||||||
val file = File(zipPath)
|
|
||||||
val serverUrl = AppConfig.getServerUrl(context)
|
val serverUrl = AppConfig.getServerUrl(context)
|
||||||
val uploadUrl = "$serverUrl/api/attachment/upload"
|
val uploadUrl = "$serverUrl/api/attachment/upload"
|
||||||
val bearerToken = "eyJraWQiOiJzLXYxIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhZG1pbiIsInR5cCI6ImFjY2VzcyIsImV4cCI6MTc3NjQ5MDczNSwianRpIjoiMjYzNjljYzQtYzJiMy00YTg0LWFiYTMtZDU4NmNiZDRmMmM0IiwiZmdwIjoiY2FhNzYzOTI4YWRhYWZiYjgzMjE3NjExYmVjYTc3ZjMiLCJraWQiOiJzLXYxIn0.AyuzNAe-R_GOBC65STH5qpHkmmUngX8rMBPI4CdIIhk"
|
val bearerToken = "eyJraWQiOiJzLXYxIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhZG1pbiIsInR5cCI6ImFjY2VzcyIsImV4cCI6MTc3NjQ5MDczNSwianRpIjoiMjYzNjljYzQtYzJiMy00YTg0LWFiYTMtZDU4NmNiZDRmMmM0IiwiZmdwIjoiY2FhNzYzOTI4YWRhYWZiYjgzMjE3NjExYmVjYTc3ZjMiLCJraWQiOiJzLXYxIn0.AyuzNAe-R_GOBC65STH5qpHkmmUngX8rMBPI4CdIIhk"
|
||||||
|
|
||||||
val requestBody = MultipartBody.Builder()
|
val body = MultipartBody.Builder().setType(MultipartBody.FORM)
|
||||||
.setType(MultipartBody.FORM)
|
.addFormDataPart("file", File(zipPath).name, File(zipPath).asRequestBody("application/zip".toMediaTypeOrNull()))
|
||||||
.addFormDataPart("file", file.name, file.asRequestBody("application/zip".toMediaTypeOrNull()))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(uploadUrl)
|
|
||||||
.header("Authorization", "Bearer $bearerToken")
|
|
||||||
.post(requestBody)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
val request = Request.Builder().url(uploadUrl).header("Authorization", "Bearer $bearerToken").post(body).build()
|
||||||
try {
|
try {
|
||||||
client.newCall(request).execute().use { response ->
|
client.newCall(request).execute().use { response ->
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
callback(true, "上传成功")
|
callback(true, "上传成功")
|
||||||
file.delete()
|
File(zipPath).delete()
|
||||||
} else {
|
} else callback(false, "错误: ${response.code}")
|
||||||
callback(false, "服务器返回错误: ${response.code}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) { callback(false, "失败: ${e.message}") }
|
||||||
callback(false, "网络连接失败: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,18 +169,12 @@ object LogManager {
|
||||||
|
|
||||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||||
executor.execute {
|
executor.execute {
|
||||||
val logFile = File(logDir, "${fileNameFormat.format(Date())}.log")
|
try {
|
||||||
val priorityStr = when (priority) {
|
val logFile = File(logDir, "${fileNameFormat.format(Date())}.log")
|
||||||
2 -> "V"
|
val priorityStr = when (priority) { 2->"V" 3->"D" 4->"I" 5->"W" 6->"E" 7->"A" else->"U" }
|
||||||
3 -> "D"
|
val entry = "${dateFormat.format(Date())} $priorityStr/[$tag]: $message\n${t?.stackTraceToString() ?: ""}\n"
|
||||||
4 -> "I"
|
FileWriter(logFile, true).use { it.write(entry) }
|
||||||
5 -> "W"
|
} catch (e: Exception) {}
|
||||||
6 -> "E"
|
|
||||||
7 -> "A"
|
|
||||||
else -> "U"
|
|
||||||
}
|
|
||||||
val entry = "${dateFormat.format(Date())} $priorityStr/[$tag]: $message\n${t?.stackTraceToString() ?: ""}\n"
|
|
||||||
try { FileWriter(logFile, true).use { it.write(entry) } } catch (e: Exception) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="#10000000">
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="#FFFFFF" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="#F1F5F9">
|
||||||
|
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:background="#FFFFFF"
|
||||||
|
android:elevation="2dp"
|
||||||
|
android:paddingHorizontal="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/btn_close"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:src="@android:drawable/ic_menu_revert"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="退出" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:text="调试控制台"
|
||||||
|
android:textColor="#1E293B"
|
||||||
|
android:textSize="17sp"
|
||||||
|
android:fontFamily="sans-serif-medium" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="核心模块测试"
|
||||||
|
android:textColor="#64748B"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:letterSpacing="0.05"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
|
<!-- PTT 卡片 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/item_ptt"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:background="@drawable/bg_developer_item"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:src="@android:drawable/ic_btn_speak_now"
|
||||||
|
android:tint="#004A8F" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="PTT 核心指令测试"
|
||||||
|
android:textColor="#1E293B"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Native 引擎交互及语音对讲模拟"
|
||||||
|
android:textColor="#94A3B8"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:src="@android:drawable/ic_media_next"
|
||||||
|
android:tint="#CBD5E1" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 打印机卡片 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/item_printer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:background="@drawable/bg_developer_item"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:src="@android:drawable/ic_menu_save"
|
||||||
|
android:tint="#10B981" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="打印机串口测试"
|
||||||
|
android:textColor="#1E293B"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="硬件指令调试及纸张状态检查"
|
||||||
|
android:textColor="#94A3B8"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:src="@android:drawable/ic_media_next"
|
||||||
|
android:tint="#CBD5E1" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 崩溃测试卡片 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/item_crash_test"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:background="@drawable/bg_developer_item"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:src="@android:drawable/ic_delete"
|
||||||
|
android:tint="#EF4444" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="异常崩溃模拟"
|
||||||
|
android:textColor="#1E293B"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="强制抛出运行时异常测试日志记录"
|
||||||
|
android:textColor="#94A3B8"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="系统信息"
|
||||||
|
android:textColor="#64748B"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginStart="8dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_developer_item"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_debug_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="加载设备详细信息..."
|
||||||
|
android:textColor="#475569"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:lineSpacingExtra="4dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Version 1.0.10 (Internal Build)\nStandalone Dispatch Terminal SDK"
|
||||||
|
android:textColor="#94A3B8"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="40dp"
|
||||||
|
android:layout_marginBottom="20dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
@ -130,29 +130,20 @@
|
||||||
android:textSize="18sp" />
|
android:textSize="18sp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_ptt_example"
|
android:id="@+id/btn_exit"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
android:layout_marginBottom="10dp"
|
android:background="@android:color/transparent"
|
||||||
android:background="@drawable/shape_button_login"
|
android:text="退出应用"
|
||||||
android:text="PTT 示例"
|
android:textColor="#CCFFFFFF"
|
||||||
android:textColor="#FFFFFF"
|
android:textSize="14sp" />
|
||||||
android:textSize="16sp" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btn_printer_test"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
android:background="@drawable/shape_button_login"
|
|
||||||
android:text="打印测试"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:textSize="16sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<!-- 底部版权 -->
|
<!-- 底部版权 -->
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/tv_copyright"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
|
|
@ -160,6 +151,9 @@
|
||||||
android:layout_marginBottom="20dp"
|
android:layout_marginBottom="20dp"
|
||||||
android:text="Copyright © 2020-2026 Telewave. All Rights Reserved."
|
android:text="Copyright © 2020-2026 Telewave. All Rights Reserved."
|
||||||
android:textColor="#88FFFFFF"
|
android:textColor="#88FFFFFF"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue