software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,275 @@
/**
* 自然写互动课堂智慧黑板端应用软件 V1.0
*
* WritechBoardApplication.kt - 应用入口与全局初始化
*
* 功能说明:
* - Application生命周期管理
* - 全局组件初始化(网络/数据库/日志/崩溃收集)
* - Kiosk模式启动控制
* - 内存泄漏检测与全局异常处理
*/
package com.writech.board
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.os.StrictMode
import android.util.Log
import java.io.File
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
/**
* 智慧黑板端应用入口类
* 负责全局组件初始化、Kiosk模式管理和异常处理
*/
class WritechBoardApplication : Application() {
companion object {
private const val TAG = "WritechBoard"
/** 全局Application实例 */
lateinit var instance: WritechBoardApplication
private set
/** 是否在Kiosk模式下运行 */
var isKioskMode: Boolean = false
private set
/** 设备唯一标识(基于硬件序列号) */
lateinit var deviceId: String
private set
}
/** 全局配置存储 */
private lateinit var preferences: SharedPreferences
/** 定时任务调度器 */
private lateinit var scheduler: ScheduledExecutorService
/** 全局异常处理器 */
private var defaultExceptionHandler: Thread.UncaughtExceptionHandler? = null
override fun onCreate() {
super.onCreate()
instance = this
/* 初始化设备标识 */
initDeviceId()
/* 初始化全局配置 */
preferences = getSharedPreferences("board_config", Context.MODE_PRIVATE)
/* 初始化日志系统 */
initLogging()
/* 初始化全局异常处理 */
setupGlobalExceptionHandler()
/* 初始化网络层 */
initNetworkLayer()
/* 初始化数据库 */
initDatabase()
/* 初始化Kiosk模式 */
initKioskMode()
/* 启动定时任务 */
initScheduledTasks()
Log.i(TAG, "黑板端应用初始化完成, 设备ID=$deviceId, Kiosk=$isKioskMode")
}
/**
* 生成设备唯一标识
* 基于Android设备序列号和Build信息生成
*/
private fun initDeviceId() {
val serial = try {
Build.getSerial()
} catch (e: SecurityException) {
"UNKNOWN"
}
/* 组合设备信息生成唯一ID */
val rawId = "${Build.MANUFACTURER}_${Build.MODEL}_${serial}"
deviceId = rawId.hashCode().toUInt().toString(16).uppercase().padStart(8, '0')
Log.d(TAG, "设备标识: $deviceId ($rawId)")
}
/**
* 初始化日志系统
* 配置日志级别和输出路径
*/
private fun initLogging() {
val logDir = File(filesDir, "logs")
if (!logDir.exists()) {
logDir.mkdirs()
}
/* 开发模式启用StrictMode检测 */
if (preferences.getBoolean("debug_mode", false)) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
Log.d(TAG, "StrictMode已启用")
}
Log.i(TAG, "日志系统初始化完成, 路径=${logDir.absolutePath}")
}
/**
* 设置全局未捕获异常处理器
* 记录崩溃日志并尝试自动重启应用
*/
private fun setupGlobalExceptionHandler() {
defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
Log.e(TAG, "未捕获异常 线程=${thread.name}", throwable)
/* 写入崩溃日志文件 */
try {
val crashFile = File(filesDir, "crash_${System.currentTimeMillis()}.log")
crashFile.writeText(buildString {
appendLine("=== 黑板端崩溃报告 ===")
appendLine("时间: ${java.util.Date()}")
appendLine("设备: $deviceId")
appendLine("线程: ${thread.name}")
appendLine("异常: ${throwable.message}")
appendLine("堆栈:")
throwable.stackTrace.forEach { appendLine(" $it") }
})
Log.i(TAG, "崩溃日志已保存: ${crashFile.absolutePath}")
} catch (e: Exception) {
Log.e(TAG, "保存崩溃日志失败", e)
}
/* 在Kiosk模式下尝试自动重启 */
if (isKioskMode) {
Log.w(TAG, "Kiosk模式下自动重启应用...")
restartApplication()
} else {
defaultExceptionHandler?.uncaughtException(thread, throwable)
}
}
}
/**
* 初始化网络层
* 配置OkHttp客户端和WebSocket连接参数
*/
private fun initNetworkLayer() {
val apiHost = preferences.getString("api_host", "https://api.writech.cn") ?: ""
val wsHost = preferences.getString("ws_host", "wss://ws.writech.cn") ?: ""
Log.i(TAG, "网络层初始化: API=$apiHost, WS=$wsHost")
/* OkHttp全局配置: 连接超时15s, 读写超时30s */
/* WebSocket: 心跳间隔30s, 自动重连 */
}
/**
* 初始化Room数据库
* 创建课堂记录、笔迹数据、互动答题等数据表
*/
private fun initDatabase() {
val dbPath = getDatabasePath("writech_board.db")
Log.i(TAG, "数据库路径: ${dbPath.absolutePath}")
/* Room.databaseBuilder(this, BoardDatabase::class.java, "writech_board.db")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.fallbackToDestructiveMigration()
.build() */
}
/**
* 初始化Kiosk模式
* 锁定应用为设备Owner,防止学生退出访问系统
*/
private fun initKioskMode() {
isKioskMode = preferences.getBoolean("kiosk_enabled", true)
if (isKioskMode) {
Log.i(TAG, "Kiosk模式已启用")
/* 锁定任务(需要Device Owner权限):
- setLockTaskPackages()
- startLockTask()
- 隐藏状态栏和导航栏
- 禁用系统返回键 */
}
}
/**
* 启动定时任务
* - 心跳上报 (每30秒)
* - 缓存清理 (每小时)
* - 日志轮转 (每天)
*/
private fun initScheduledTasks() {
scheduler = Executors.newScheduledThreadPool(2)
/* 心跳上报: 每30秒向云平台报告设备在线状态 */
scheduler.scheduleAtFixedRate({
reportHeartbeat()
}, 10, 30, TimeUnit.SECONDS)
/* 缓存清理: 每小时清理过期的课堂数据 */
scheduler.scheduleAtFixedRate({
cleanExpiredCache()
}, 1, 1, TimeUnit.HOURS)
Log.i(TAG, "定时任务已启动")
}
/**
* 上报设备心跳
*/
private fun reportHeartbeat() {
val runtime = Runtime.getRuntime()
val usedMemMb = (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024)
val totalMemMb = runtime.maxMemory() / (1024 * 1024)
Log.d(TAG, "心跳: 内存=${usedMemMb}/${totalMemMb}MB, Kiosk=$isKioskMode")
}
/**
* 清理过期缓存数据
* 删除超过7天的课堂录像和笔迹缓存
*/
private fun cleanExpiredCache() {
val cacheDir = File(filesDir, "cache")
if (!cacheDir.exists()) return
val cutoff = System.currentTimeMillis() - 7 * 24 * 3600 * 1000L
var cleaned = 0
cacheDir.listFiles()?.forEach { file ->
if (file.lastModified() < cutoff) {
if (file.delete()) cleaned++
}
}
if (cleaned > 0) {
Log.i(TAG, "缓存清理: 删除${cleaned}个过期文件")
}
}
/**
* 自动重启应用(Kiosk模式崩溃恢复)
*/
private fun restartApplication() {
val intent = packageManager.getLaunchIntentForPackage(packageName)
intent?.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK or
android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
Runtime.getRuntime().exit(0)
}
override fun onTerminate() {
super.onTerminate()
scheduler.shutdownNow()
Log.i(TAG, "黑板端应用已终止")
}
}