开发者

This commit is contained in:
fengge 2026-04-23 11:11:49 +08:00
parent a87aac59ce
commit 3302225e74
7 changed files with 447 additions and 130 deletions

View File

@ -47,11 +47,17 @@
android:label="@string/app_name"
android:theme="@style/Theme.StandAPP" />
<activity
android:name=".DeveloperActivity"
android:exported="false"
android:label="开发者模式"
android:theme="@style/Theme.StandAPP" />
<activity
android:name=".printer.PrintTestActivity"
android:exported="false"
android:label="打印测试"
android:theme="@style/Theme.AppCompat.Light.DarkActionBar" />
android:theme="@style/Theme.StandAPP" />
<activity
android:name="com.example.kingway.ptt.pttActivity"

View File

@ -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()
}
}

View File

@ -1,14 +1,15 @@
package com.stand.standapp
import android.content.Context
import android.content.Intent
import android.os.Bundle
import timber.log.Timber
import android.widget.*
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.stand.standapp.printer.PrinterManager
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import org.json.JSONObject
import timber.log.Timber
import java.io.IOException
class LoginActivity : AppCompatActivity() {
@ -23,9 +24,9 @@ class LoginActivity : AppCompatActivity() {
val etPassword = findViewById<EditText>(R.id.et_password)
val cbRemember = findViewById<CheckBox>(R.id.cb_remember)
val btnLogin = findViewById<Button>(R.id.btn_login)
val btnPttExample = findViewById<Button>(R.id.btn_ptt_example)
val btnPrinterTest = findViewById<Button>(R.id.btn_printer_test)
val btnExit = findViewById<Button>(R.id.btn_exit)
val ivSettings = findViewById<ImageView>(R.id.iv_settings)
val tvCopyright = findViewById<TextView>(R.id.tv_copyright)
// 1. 加载保存的状态和凭据
val isRemember = AppConfig.isRememberLogin(this)
@ -54,35 +55,38 @@ class LoginActivity : AppCompatActivity() {
performLogin(username, password, cbRemember.isChecked)
}
// 4. PTT 示例按钮点击事件
btnPttExample.setOnClickListener {
try {
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();
}
// 4. 退出按钮
btnExit.setOnClickListener {
finish()
}
// 5. 打印测试按钮点击事件
btnPrinterTest.setOnClickListener {
try {
val intent = Intent(this, com.stand.standapp.printer.PrintTestActivity::class.java)
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, "打开打印测试失败: ${e.message}", Toast.LENGTH_SHORT).show()
// 5. 彩蛋:点击底部版权 10 次进入开发者模式
var clickCount = 0
var lastClickTime = 0L
tvCopyright.setOnClickListener {
val currentTime = System.currentTimeMillis()
// 连续点击判定间隔为 500ms
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) {
val serverUrl = AppConfig.getServerUrl(this)
val loginUrl = "$serverUrl/api/auth/login"
val bearerToken = "eyJraWQiOiJzLXYxIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhZG1pbiIsInR5cCI6ImFjY2VzcyIsImV4cCI6MTc3NjQwMTU2MSwianRpIjoiMjM0NjdiZGMtNTY0Zi00NmY0LWJhZTgtNzJhZmQ1MzhmYmZhIiwiZmdwIjoiY2FhNzYzOTI4YWRhYWZiYjgzMjE3NjExYmVjYTc3ZjMiLCJraWQiOiJzLXYxIn0.4Zf1eiKcUjGNoFylEzmZ2no1kJY-44CQ8QWmMA_UxvY"
val body = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("username", username)
@ -92,7 +96,6 @@ class LoginActivity : AppCompatActivity() {
val request = Request.Builder()
.url(loginUrl)
.post(body)
.addHeader("Authorization", "Bearer $bearerToken")
.build()
val loadingDialog = AlertDialog.Builder(this)
@ -117,7 +120,6 @@ class LoginActivity : AppCompatActivity() {
val json = JSONObject(responseBody ?: "")
val code = json.optInt("code", -1)
if (code == 0) {
// 登录成功
AppConfig.setRememberLogin(this@LoginActivity, remember)
if (remember) {
AppConfig.setSavedUsername(this@LoginActivity, username)
@ -127,12 +129,12 @@ class LoginActivity : AppCompatActivity() {
AppConfig.setSavedPassword(this@LoginActivity, "")
}
Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show()
// 初始化打印机 (自动重连)
com.stand.standapp.printer.PrinterManager.init(this@LoginActivity)
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show()
// 初始化打印机 (异步)
PrinterManager.init(this@LoginActivity)
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
finish()
} else {
val msg = json.optString("msg", "登录失败")
@ -153,7 +155,6 @@ class LoginActivity : AppCompatActivity() {
val etSerialPort = dialogView.findViewById<EditText>(R.id.et_serial_port)
val btnReset = dialogView.findViewById<TextView>(R.id.btn_reset)
// 初始化数据
etServerUrl.setText(AppConfig.getServerUrl(this))
etSerialPort.setText(AppConfig.getSerialPort(this))
@ -190,10 +191,9 @@ class LoginActivity : AppCompatActivity() {
dialog.show()
// 强制设置对话框宽度
dialog.window?.let { window ->
val params = window.attributes
params.width = (resources.displayMetrics.widthPixels * 0.75).toInt() // 占据屏幕 75% 宽度
params.width = (resources.displayMetrics.widthPixels * 0.95).toInt()
window.attributes = params
}
}
@ -202,10 +202,7 @@ class LoginActivity : AppCompatActivity() {
AlertDialog.Builder(this)
.setTitle("提示")
.setMessage("确定要退出应用吗?")
.setPositiveButton("确定") { _, _ ->
@Suppress("DEPRECATION")
super.onBackPressed()
}
.setPositiveButton("确定") { _, _ -> finish() }
.setNegativeButton("取消", null)
.show()
}

View File

@ -24,58 +24,99 @@ object LogManager {
Timber.plant(Timber.DebugTree())
try {
logDir = File(context.getExternalFilesDir(null), "logs")
if (logDir?.exists() == false) logDir?.mkdirs()
// 1. 路径确定
val baseDir = context.getExternalFilesDir(null)
logDir = if (baseDir != null) {
File(baseDir, "logs")
} else {
File(context.filesDir, "logs")
}
if (logDir?.exists() == false) {
val created = logDir?.mkdirs()
android.util.Log.i("LogManager", "Directory created: $created at ${logDir?.absolutePath}")
}
// 立即清理旧日志
logDir?.let { cleanOldLogs(it) }
// 2. 强行创建一个“自检”日志文件,不使用异步线程池
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))
}
// 挂载文件日志树
logDir?.let { Timber.plant(FileLoggingTree(it)) }
// 【核心增强】接管全局异常,确保崩溃信息能写进文件
setupCrashHandler()
Timber.tag("LogManager").i("Logging system initialized and CrashHandler attached")
} catch (e: Exception) {
android.util.Log.e("LogManager", "Init failed", e)
android.util.Log.e("LogManager", "Init global failure", e)
}
}
private fun setupCrashHandler() {
// 1. 捕获所有普通线程、子线程的未捕获异常
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
// 强制同步写入崩溃日志
val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()).format(Date())
val crashInfo = "\n\n=== FATAL CRASH ===\n" +
"Time: $timestamp\n" +
"Thread: ${thread.name}\n" +
"Stacktrace:\n${throwable.stackTraceToString()}\n" +
"====================\n\n"
logDir?.let { dir ->
val logFile = File(dir, SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date()) + ".log")
saveCrashToFile(thread, throwable)
defaultHandler?.uncaughtException(thread, throwable) ?: exitProcess(1)
}
// 2. 捕获主线程 (UI 线程) 的所有异常
// 这种方式能捕获点击事件、生命周期回调中发生的所有崩溃
android.os.Handler(android.os.Looper.getMainLooper()).post {
while (true) {
try {
FileOutputStream(logFile, true).use { it.write(crashInfo.toByteArray()) }
} catch (e: Exception) {
android.util.Log.e("LogManager", "Failed to write crash log", e)
android.os.Looper.loop()
} catch (e: Throwable) {
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 {
try {
val calendar = Calendar.getInstance()
calendar.add(Calendar.DAY_OF_YEAR, -MAX_DAYS)
val thresholdDate = calendar.time
logDir.listFiles()?.forEach { file ->
if (file.isFile && file.name.endsWith(".log")) {
if (Date(file.lastModified()).before(thresholdDate)) file.delete()
val thresholdDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -MAX_DAYS) }.time
dir.listFiles()?.forEach { file ->
if (file.isFile && file.name.endsWith(".log") && Date(file.lastModified()).before(thresholdDate)) {
file.delete()
}
}
} catch (e: Exception) {}
@ -84,7 +125,6 @@ object LogManager {
fun zipLogs(context: Context): String? {
val dir = logDir ?: return null
if (!dir.exists() || dir.listFiles().isNullOrEmpty()) return null
val zipFile = File(context.getExternalFilesDir(null), "logs_${System.currentTimeMillis()}.zip")
return try {
ZipOutputStream(FileOutputStream(zipFile)).use { zos ->
@ -97,48 +137,29 @@ object LogManager {
}
}
zipFile.absolutePath
} catch (e: Exception) {
Timber.e(e, "Zip logs failed")
null
}
} catch (e: Exception) { null }
}
fun uploadLogs(context: Context, callback: (Boolean, String) -> Unit) {
executor.execute {
val zipPath = zipLogs(context)
if (zipPath == null) {
callback(false, "打包日志失败")
return@execute
}
val file = File(zipPath)
val zipPath = zipLogs(context) ?: return@execute callback(false, "打包失败")
val serverUrl = AppConfig.getServerUrl(context)
val uploadUrl = "$serverUrl/api/attachment/upload"
val bearerToken = "eyJraWQiOiJzLXYxIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhZG1pbiIsInR5cCI6ImFjY2VzcyIsImV4cCI6MTc3NjQ5MDczNSwianRpIjoiMjYzNjljYzQtYzJiMy00YTg0LWFiYTMtZDU4NmNiZDRmMmM0IiwiZmdwIjoiY2FhNzYzOTI4YWRhYWZiYjgzMjE3NjExYmVjYTc3ZjMiLCJraWQiOiJzLXYxIn0.AyuzNAe-R_GOBC65STH5qpHkmmUngX8rMBPI4CdIIhk"
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", file.name, file.asRequestBody("application/zip".toMediaTypeOrNull()))
.build()
val request = Request.Builder()
.url(uploadUrl)
.header("Authorization", "Bearer $bearerToken")
.post(requestBody)
val body = MultipartBody.Builder().setType(MultipartBody.FORM)
.addFormDataPart("file", File(zipPath).name, File(zipPath).asRequestBody("application/zip".toMediaTypeOrNull()))
.build()
val request = Request.Builder().url(uploadUrl).header("Authorization", "Bearer $bearerToken").post(body).build()
try {
client.newCall(request).execute().use { response ->
if (response.isSuccessful) {
callback(true, "上传成功")
file.delete()
} else {
callback(false, "服务器返回错误: ${response.code}")
}
File(zipPath).delete()
} else callback(false, "错误: ${response.code}")
}
} catch (e: IOException) {
callback(false, "网络连接失败: ${e.message}")
}
} catch (e: IOException) { callback(false, "失败: ${e.message}") }
}
}
@ -148,18 +169,12 @@ object LogManager {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
executor.execute {
val logFile = File(logDir, "${fileNameFormat.format(Date())}.log")
val priorityStr = when (priority) {
2 -> "V"
3 -> "D"
4 -> "I"
5 -> "W"
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) {}
try {
val logFile = File(logDir, "${fileNameFormat.format(Date())}.log")
val priorityStr = when (priority) { 2->"V" 3->"D" 4->"I" 5->"W" 6->"E" 7->"A" else->"U" }
val entry = "${dateFormat.format(Date())} $priorityStr/[$tag]: $message\n${t?.stackTraceToString() ?: ""}\n"
FileWriter(logFile, true).use { it.write(entry) }
} catch (e: Exception) {}
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -130,29 +130,20 @@
android:textSize="18sp" />
<Button
android:id="@+id/btn_ptt_example"
android:id="@+id/btn_exit"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="10dp"
android:background="@drawable/shape_button_login"
android:text="PTT 示例"
android:textColor="#FFFFFF"
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" />
android:background="@android:color/transparent"
android:text="退出应用"
android:textColor="#CCFFFFFF"
android:textSize="14sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 底部版权 -->
<TextView
android:id="@+id/tv_copyright"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
@ -160,6 +151,9 @@
android:layout_marginBottom="20dp"
android:text="Copyright © 2020-2026 Telewave. All Rights Reserved."
android:textColor="#88FFFFFF"
android:textSize="12sp" />
android:textSize="12sp"
android:padding="10dp"
android:clickable="true"
android:focusable="true" />
</RelativeLayout>