浏览代码

1. 新增日志系统
2. 支持打印输出为错误级别的日志

bjb 4 月之前
父节点
当前提交
fe18cd3c3d

+ 1 - 1
.idea/deploymentTargetSelector.xml

@@ -4,7 +4,7 @@
     <selectionStates>
       <SelectionState runConfigName="app">
         <option name="selectionMode" value="DROPDOWN" />
-        <DropdownSelection timestamp="2026-01-19T07:37:04.874216600Z">
+        <DropdownSelection timestamp="2026-01-21T03:54:14.252565200Z">
           <Target type="DEFAULT_BOOT">
             <handle>
               <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\ISCS\.android\avd\Resizable.avd" />

+ 3 - 2
app/src/main/java/com/iscs/bozzys/Entry.kt

@@ -3,9 +3,8 @@ package com.iscs.bozzys
 import android.app.Application
 import com.alibaba.sdk.android.push.noonesdk.PushInitConfig
 import com.alibaba.sdk.android.push.noonesdk.PushServiceFactory
-import com.iscs.bozzys.utils.BiometricKeyStore
+import com.iscs.bozzys.utils.LogUtil
 import com.iscs.bozzys.utils.Storage
-import com.iscs.bozzys.utils.SystemUtil.isBiometricCanUse
 
 /**
  * App主入口
@@ -14,6 +13,8 @@ class Entry : Application() {
 
     override fun onCreate() {
         super.onCreate()
+        // 初始化日志工具
+        LogUtil.init(this)
         // 初始化轻量级存储相关
         Storage.init(this)
         // 初始化消息推送

+ 0 - 1
app/src/main/java/com/iscs/bozzys/api/ApiBean.kt

@@ -519,7 +519,6 @@ data class Node(
      * 获取node图标,暂时本地通过type获取
      */
     fun getIcon(): Int {
-        // Log.d("xiaoming", "节点类型:$type")
         return when (type) {
             // 开始/创建
             "createJob" -> R.drawable.node_icon_create_job

+ 1 - 1
app/src/main/java/com/iscs/bozzys/ui/pages/PageSplash.kt

@@ -79,7 +79,7 @@ class PageSplash : PageBase() {
     private fun initPush() {
         val pushService = PushServiceFactory.getCloudPushService()
         // 设置输出调试日志
-        pushService.setLogLevel(CloudPushService.LOG_DEBUG)
+        pushService.setLogLevel(CloudPushService.LOG_OFF)
         // 设置消息服务
         pushService.setPushIntentService(AliPushService::class.java)
         // 初始化消息推送

+ 1 - 1
app/src/main/java/com/iscs/bozzys/ui/pages/home/TasksCompose.kt

@@ -224,7 +224,7 @@ private fun TaskList(vm: VMHome) {
     // 处理加载更多数据
     LaunchedEffect(shouldLoadMore) {
         if (shouldLoadMore && state.taskList.isNotEmpty() && !state.taskPage.noMore) {
-            vm.getJobList(state.taskPage.copy(page = state.taskPage.page + 1, isRefresh = false))
+            vm.getTaskList(state.taskPage.copy(page = state.taskPage.page + 1, isRefresh = false))
         }
     }
     Box {

+ 42 - 38
app/src/main/java/com/iscs/bozzys/ui/pages/login/PageLogin.kt

@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
@@ -43,6 +44,7 @@ import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.text.input.PasswordVisualTransformation
 import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.lifecycle.viewmodel.compose.viewModel
@@ -51,12 +53,14 @@ import com.iscs.bozzys.ui.common.PageBase
 import com.iscs.bozzys.ui.pages.home.openPageHome
 import com.iscs.bozzys.ui.pages.vm.VMLogin
 import com.iscs.bozzys.ui.theme.Main
+import com.iscs.bozzys.ui.theme.SubMain
 import com.iscs.bozzys.ui.theme.Text
 import com.iscs.bozzys.ui.theme.TextDesc
 import com.iscs.bozzys.utils.BiometricKeyStore
 import com.iscs.bozzys.utils.LogUtil
 import com.iscs.bozzys.utils.Storage
 import com.iscs.bozzys.utils.SystemUtil
+import com.iscs.bozzys.utils.SystemUtil.getAppVersionInfo
 
 /**
  * 打开登录页面
@@ -138,41 +142,41 @@ class PageLogin : PageBase() {
         ) {
             Text("欢迎登录", fontSize = 20.sp, fontWeight = FontWeight.SemiBold, color = Text, modifier = Modifier.padding(top = 30.dp))
             Spacer(Modifier.weight(1f))
-//            Row(
-//                Modifier
-//                    .padding(top = 24.dp)
-//                    .size(335.dp, 47.dp)
-//                    .border(1.dp, color = Color(0xFFD1D5DB), shape = RoundedCornerShape(12.dp))
-//                    .clip(RoundedCornerShape(12.dp)),
-//                verticalAlignment = Alignment.CenterVertically
-//            ) {
-//                Text(
-//                    "短信登录",
-//                    Modifier
-//                        .weight(1f)
-//                        .fillMaxHeight()
-//                        .background(if (state.loginType == 0) SubMain else Color.White)
-//                        .clickable(onClick = { vm.updateLoginType(0) }),
-//                    color = if (state.loginType == 0) Main else TextDesc,
-//                    lineHeight = 47.sp,
-//                    fontSize = 14.sp,
-//                    fontWeight = if (state.loginType == 0) FontWeight.SemiBold else FontWeight.Normal,
-//                    textAlign = TextAlign.Center
-//                )
-//                Text(
-//                    "密码登录",
-//                    Modifier
-//                        .weight(1f)
-//                        .fillMaxHeight()
-//                        .background(if (state.loginType == 1) SubMain else Color.White)
-//                        .clickable(onClick = { vm.updateLoginType(1) }),
-//                    color = if (state.loginType == 1) Main else TextDesc,
-//                    lineHeight = 47.sp,
-//                    fontSize = 14.sp,
-//                    fontWeight = if (state.loginType == 1) FontWeight.SemiBold else FontWeight.Normal,
-//                    textAlign = TextAlign.Center,
-//                )
-//            }
+            Row(
+                Modifier
+                    .padding(top = 24.dp)
+                    .size(335.dp, 47.dp)
+                    .border(1.dp, color = Color(0xFFD1D5DB), shape = RoundedCornerShape(12.dp))
+                    .clip(RoundedCornerShape(12.dp)),
+                verticalAlignment = Alignment.CenterVertically
+            ) {
+                Text(
+                    "短信登录",
+                    Modifier
+                        .weight(1f)
+                        .fillMaxHeight()
+                        .background(if (state.loginType == 0) SubMain else Color.White)
+                        .clickable(onClick = { vm.updateLoginType(0) }),
+                    color = if (state.loginType == 0) Main else TextDesc,
+                    lineHeight = 47.sp,
+                    fontSize = 14.sp,
+                    fontWeight = if (state.loginType == 0) FontWeight.SemiBold else FontWeight.Normal,
+                    textAlign = TextAlign.Center
+                )
+                Text(
+                    "密码登录",
+                    Modifier
+                        .weight(1f)
+                        .fillMaxHeight()
+                        .background(if (state.loginType == 1) SubMain else Color.White)
+                        .clickable(onClick = { vm.updateLoginType(1) }),
+                    color = if (state.loginType == 1) Main else TextDesc,
+                    lineHeight = 47.sp,
+                    fontSize = 14.sp,
+                    fontWeight = if (state.loginType == 1) FontWeight.SemiBold else FontWeight.Normal,
+                    textAlign = TextAlign.Center,
+                )
+            }
             Row(
                 Modifier
                     .padding(top = 24.dp)
@@ -304,8 +308,8 @@ class PageLogin : PageBase() {
                         .clickable {
                             val cipher = BiometricKeyStore.decryptCipher(Storage.readTokenIv())
                             SystemUtil.showFingerAuth(this@PageLogin, cipher, Storage.readTokenIvValue()) {
-                                LogUtil.i("xiaoming", "当前 token -> ${Storage.readToken()}")
-                                LogUtil.i("xiaoming", "解密 token -> ${String(it)}")
+                                // LogUtil.i("xiaoming", "当前 token -> ${Storage.readToken()}")
+                                // LogUtil.i("xiaoming", "解密 token -> ${String(it)}")
                             }
                         }
                         .background(Color(0xFFF5F5F5))
@@ -345,7 +349,7 @@ class PageLogin : PageBase() {
                         .background(Color(0xFFEEEEEE))
                 )
                 Text("LOTO智能锁控·能量安全隔离", Modifier.padding(top = 20.dp, bottom = 4.dp), fontSize = 14.sp, color = Color(0xFF666666))
-                Text("版本 v1.0.0", Modifier.padding(bottom = 20.dp), fontSize = 14.sp, color = Color(0xFF666666))
+                Text("版本 v${ctx.getAppVersionInfo().second}", Modifier.padding(bottom = 20.dp), fontSize = 14.sp, color = Color(0xFF666666))
             }
             Spacer(Modifier.weight(1f))
         }

+ 67 - 4
app/src/main/java/com/iscs/bozzys/utils/LogUtil.kt

@@ -1,37 +1,100 @@
 package com.iscs.bozzys.utils
 
+import android.app.Application
 import android.util.Log
+import com.iscs.bozzys.utils.DateUtil.format
+import java.io.File
 
+/**
+ * 日志输出工具,后续这里输出日志到文件中
+ */
 object LogUtil {
 
+    private const val TAG = "BozzysLogs"
+
     var showLog = true
 
+    // 日志文件目录
+    private const val DIR_NAME = "logs"
+
+    // 限制最大日志占用空间,单位MB
+    private const val MAX_FILE_SIZE = 10 * 1024 * 1024
+
+    // 日志写锁保护
+    private val syncLock = Any()
+
+    private lateinit var app: Application
+
+    /**
+     * 日志工具初始化
+     */
+    fun init(app: Application) {
+        this.app = app
+    }
+
     /**
      * INFO级别日志输出
      */
     fun i(tag: String, msg: String) {
-        if (showLog) Log.i("Bozzys $tag", msg)
+        if (showLog) Log.i("$TAG $tag", msg)
     }
 
     /**
      * DEBUG级别日志输出
      */
     fun d(tag: String, msg: String) {
-        if (showLog) Log.d("Bozzys $tag", msg)
+        if (showLog) Log.d("$TAG $tag", msg)
     }
 
     /**
      * WARNING级别日志输出
      */
     fun w(tag: String, msg: String) {
-        if (showLog) Log.w("Bozzys $tag", msg)
+        if (showLog) Log.w("$TAG $tag", msg)
     }
 
     /**
      * ERROR级别日志输出
      */
     fun e(tag: String, msg: String) {
-        if (showLog) Log.e("Bozzys $tag", msg)
+        write("$TAG $tag", msg)
+        if (showLog) Log.e("$TAG $tag", msg)
+    }
+
+    /**
+     * 写日志到本地文件中
+     *
+     * @param tag   写入的标记
+     * @param msg   写入的数据
+     */
+    private fun write(tag: String, msg: String) {
+        val time = System.currentTimeMillis().format("yyyy-MM-dd HH:mm:ss.SSS")
+        val line = "$time [$tag] $msg\n"
+        synchronized(syncLock) {
+            try {
+                val dir = app.getExternalFilesDir(DIR_NAME) ?: return
+                if (!dir.exists()) dir.mkdirs()
+
+                val file = File(dir, "log_${System.currentTimeMillis().format("yyyyMMdd")}.txt")
+                rotateIfNeed(file)
+
+                file.appendText(line)
+            } catch (e: Exception) {
+                e.printStackTrace()
+            }
+        }
+    }
+
+    /**
+     * 超出大小后做文件备份处理
+     *
+     * @param file
+     */
+    private fun rotateIfNeed(file: File) {
+        if (file.exists() && file.length() > MAX_FILE_SIZE) {
+            val bak = File(file.parent, file.name + ".bak")
+            file.renameTo(bak)
+        }
     }
 
 }

+ 15 - 0
app/src/main/java/com/iscs/bozzys/utils/SystemUtil.kt

@@ -20,6 +20,21 @@ import javax.crypto.spec.IvParameterSpec
  */
 object SystemUtil {
 
+    /**
+     * 获取App版本信息
+     */
+    fun Context.getAppVersionInfo(): Pair<Long, String> {
+        val pm = this.packageManager
+        val pi = pm.getPackageInfo(this.packageName, 0)
+        val code = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            pi.longVersionCode
+        } else {
+            @Suppress("DEPRECATION")
+            pi.versionCode.toLong()
+        }
+        val name = pi.versionName ?: ""
+        return code to name
+    }
 
     /**
      * 指纹组件是否可用

+ 93 - 2
app/src/main/java/com/iscs/bozzys/utils/network/Request.kt

@@ -1,10 +1,16 @@
 package com.iscs.bozzys.utils.network
 
 import android.annotation.SuppressLint
+import com.iscs.bozzys.utils.DateUtil.format
+import com.iscs.bozzys.utils.LogUtil
 import okhttp3.OkHttpClient
-import okhttp3.logging.HttpLoggingInterceptor
+import okhttp3.Request
+import okio.Buffer
 import retrofit2.Retrofit
 import retrofit2.converter.gson.GsonConverterFactory
+import java.io.EOFException
+import java.nio.charset.Charset
+import java.nio.charset.StandardCharsets.UTF_8
 import java.security.SecureRandom
 import java.security.cert.X509Certificate
 import javax.net.ssl.SSLContext
@@ -17,6 +23,8 @@ import javax.net.ssl.X509TrustManager
  */
 object Request {
 
+    private const val TAG = "ApiRequest"
+
     // 网络请求的基地址
     // 外网IP "http://120.27.232.27:48080"
     // 本地IP "http://192.168.0.10:48080"
@@ -24,7 +32,22 @@ object Request {
 
     // 构建请求客户端
     private val okClient = OkHttpClient.Builder()
-        .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
+        .addInterceptor {
+            val start = System.currentTimeMillis()
+            // 请求拦截 处理一些需要处理的数据,或者打印
+            val request = it.request()
+            val response: okhttp3.Response
+            try {
+                response = it.proceed(request)
+                val end = System.currentTimeMillis()
+                printLog(request, response, null, start, end)
+            } catch (e: Exception) {
+                val end = System.currentTimeMillis()
+                printLog(request, null, e, start, end)
+                throw e
+            }
+            return@addInterceptor response
+        }
         .sslSocketFactory(getSocketFactory(), createTrustManager())
         .hostnameVerifier { _, _ -> true }
         .build()
@@ -75,4 +98,72 @@ object Request {
     fun getRetrofit(): Retrofit {
         return retrofit
     }
+
+    private fun Buffer.isProbablyUtf8(): Boolean {
+        try {
+            val prefix = Buffer()
+            val byteCount = size.coerceAtMost(64)
+            copyTo(prefix, 0, byteCount)
+            for (i in 0 until 16) {
+                if (prefix.exhausted()) {
+                    break
+                }
+                val codePoint = prefix.readUtf8CodePoint()
+                if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
+                    return false
+                }
+            }
+            return true
+        } catch (_: EOFException) {
+            return false // Truncated UTF-8 sequence.
+        }
+    }
+
+    private fun printLog(request: Request, response: okhttp3.Response?, e: Exception?, start: Long, end: Long) {
+        val sb = StringBuffer("ㅤ\n")
+        sb.append("👉 ${start.format("yyyy-MM-dd HH:mm:ss.SSS")}").append("\n")
+        sb.append("${request.method} ${request.url}").append("\n")
+        // 打印请求头
+        val headers = request.headers
+        for (header in headers) {
+            sb.append("${header.first}: ${header.second}").append("\n")
+        }
+        // 打印请求体
+        request.body?.let { body ->
+            val buffer = Buffer()
+            body.writeTo(buffer)
+
+            val contentType = body.contentType()
+            val charset: Charset = contentType?.charset(UTF_8) ?: UTF_8
+
+            if (buffer.isProbablyUtf8()) {
+                sb.append(buffer.readString(charset)).append("\n")
+            }
+        }
+
+        if (e != null) {
+            sb.append("\n")
+            // 打印响应数据,因为异常了
+            sb.append(e.message ?: "").append("\n")
+            sb.append("👈 ${end.format("yyyy-MM-dd HH:mm:ss.SSS")} (${end - start}ms)")
+            LogUtil.e(TAG, "$sb")
+        } else {
+            // 打印响应数据
+            response?.let {
+                val responseBody = it.body!!
+                val contentLength = responseBody.contentLength()
+                val source = responseBody.source()
+                source.request(Long.MAX_VALUE)
+                val buffer = source.buffer
+                sb.append("\n")
+                val contentType = responseBody.contentType()
+                val charset: Charset = contentType?.charset(UTF_8) ?: UTF_8
+                if (contentLength != 0L) {
+                    sb.append(buffer.clone().readString(charset)).append("\n")
+                }
+                sb.append("👈 ${end.format("yyyy-MM-dd HH:mm:ss.SSS")} (${end - start}ms)")
+            }
+            LogUtil.i(TAG, "$sb")
+        }
+    }
 }