276 lines
8.6 KiB
Kotlin
276 lines
8.6 KiB
Kotlin
/**
|
|
* 自然写互动课堂智慧黑板端应用软件 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, "黑板端应用已终止")
|
|
}
|
|
}
|