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