diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 73834c1..baec481 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" ndk { - abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")) + abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a")) } } @@ -45,6 +45,7 @@ android { } dependencies { + implementation("com.jakewharton.timber:timber:5.0.1") implementation("io.github.justson:agentweb-core:v5.1.1-androidx") implementation("com.github.Justson:Downloader:v5.0.4-androidx") implementation("com.google.android.material:material:1.12.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce5ae38..5002e62 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ (适用于蓝牙/USB 小票机,传输原始 JSON)

- - + + +
@@ -255,6 +256,15 @@ } } + // 上传日志 + function callUploadLogs() { + if (window.android && window.android.uploadLogs) { + window.android.uploadLogs(); + } else { + alert("上传日志接口不可用"); + } + } + function callExit() { if (window.android && window.android.exitApp) { window.android.exitApp(); diff --git a/app/src/main/java/com/example/kingway/ptt/CallBackResolution.java b/app/src/main/java/com/example/kingway/ptt/CallBackResolution.java index bc9c56b..58e94e1 100644 --- a/app/src/main/java/com/example/kingway/ptt/CallBackResolution.java +++ b/app/src/main/java/com/example/kingway/ptt/CallBackResolution.java @@ -1,12 +1,12 @@ package com.example.kingway.ptt; -import android.util.Log; +import timber.log.Timber; import java.util.ArrayList; import java.util.List; /**解析接收到的AT指令*/ public class CallBackResolution { - private static String TAG = "CallBackResolution"; + private static String TAG = "PTT_RX"; private static String indexGroupId = "";//用户所在群组Id private static String indexGroupName = "";//用户所在群组名称 private static List memberInfoDtos = new ArrayList<>(); @@ -26,10 +26,10 @@ public class CallBackResolution { String ngx = "\\u"; try { - Log.i(TAG, "lisHex-->" + lisHex.toString()); + Timber.tag(TAG).i("lisHex-->" + lisHex.toString()); String firstHex = lisHex.get(0); String errorNum = lisHex.get(1); - Log.d(TAG, "at_OEM_AT_cb firstHex: " + firstHex + ", errorNum: " + errorNum); + Timber.tag(TAG).d("at_OEM_AT_cb firstHex: " + firstHex + ", errorNum: " + errorNum); switch (firstHex){ case "82"://登录状态通知指令 String loginId = lisHex.get(2) + "" + lisHex.get(3) + "" + lisHex.get(4) + "" + lisHex.get(5); @@ -54,17 +54,17 @@ public class CallBackResolution { break; case "0b"://发起讲话返回状态 - Log.d(TAG, "at_OEM_AT_cb ptt speak..."); + Timber.tag(TAG).d("at_OEM_AT_cb ptt speak..."); CallBackUtil.callBackSpeakPlaySuccess(errorNum.equals("00")? true : false); break; case "0c"://结束讲话返回状态 - Log.d(TAG, "at_OEM_AT_cb speak end..."); + Timber.tag(TAG).d("at_OEM_AT_cb speak end..."); CallBackUtil.callBackSpeakReleaseSuccess(errorNum.equals("00")? true : false); break; case "80"://返回群组信息 - Log.d(TAG, "at_OEM_AT_cb get group info: " + lisHex); + Timber.tag(TAG).d("at_OEM_AT_cb get group info: " + lisHex); String strGroup = ""; String groupId = lisHex.get(6) + "" + lisHex.get(7) + "" + lisHex.get(8) + "" + lisHex.get(9); int groupNo = Integer.valueOf((lisHex.get(4) + "" + lisHex.get(5)), 16); @@ -87,9 +87,9 @@ public class CallBackResolution { } finally { String tempTxt = ""; tempTxt = unicodeToString(strGroup); - Log.i(TAG, " 组名称 " + tempTxt); + Timber.tag(TAG).i(" 组名称 " + tempTxt); CallBackUtil.callBackGroupInfo(groupId, tempTxt, groupNo, groupCount); - Log.d(TAG, "at_OEM_AT_cb group info groupId: " + groupId + ", groupNo: " + groupNo+", groupCount: " +groupCount); + Timber.tag(TAG).d("at_OEM_AT_cb group info groupId: " + groupId + ", groupNo: " + groupNo+", groupCount: " +groupCount); if (1 == groupNo) groupInfos.clear(); GroupInfoDto groupInfo = new GroupInfoDto(groupId, tempTxt, groupNo, groupCount); groupInfos.add(groupInfo); @@ -197,7 +197,7 @@ public class CallBackResolution { for (int i = 0; i < stringsTalk.size(); i++) { tempTxt += unicode(stringsTalk.get(i)); } - Log.i(TAG, " 临时讲话 信息通知的指令 " + tempTxt); + Timber.tag(TAG).i(" 临时讲话 信息通知的指令 " + tempTxt); CallBackUtil.callBackTempCallNotify(tempTxt); } break; @@ -230,7 +230,7 @@ public class CallBackResolution { for (int i = 0; i < stringsJumpGroup.size(); i++) { userGroupName += unicode(stringsJumpGroup.get(i)); } - Log.i(TAG,"进入群组 " + userGroupName); + Timber.tag(TAG).i("进入群组 " + userGroupName); indexGroupId = userGroupId; indexGroupName = userGroupName; CallBackUtil.callBackNotifyUpdateGroup(userGroupId, userGroupName); @@ -257,7 +257,7 @@ public class CallBackResolution { String time = strLis[2]; CallBackUtil.callBackNotifyLocationStatus(0, locationMemberId, latitude, longitude, time); } catch (Exception e) { - Log.e(TAG, "获取定位Exception-->" + e.getMessage()); + Timber.tag(TAG).e(e, "获取定位Exception"); } } else { CallBackUtil.callBackNotifyLocationStatus(1, "", 0.0, 0.0, ""); @@ -311,14 +311,14 @@ public class CallBackResolution { List strName = new ArrayList<>(); double latitude = 0.0, longitude = 0.0; try { - String string = ""; - for (int i = 4; i < lisHex.size(); i++) { - string += lisHex.get(i).toString(); - } - String address = hexString2String(string); - Log.i(TAG, "address-->" + address); + String string = ""; + for (int i = 4; i < lisHex.size(); i++) { + string += lisHex.get(i).toString(); + } + String address = hexString2String(string); + Timber.tag(TAG).i("address-->" + address); - String[] strLis = address.split(","); + String[] strLis = address.split(","); latitude = Double.parseDouble(strLis[0]); longitude = Double.parseDouble(strLis[1]); int size = lisHex.size() - (8 + strLis[0].length() + strLis[1].length()); @@ -358,7 +358,7 @@ public class CallBackResolution { break; } }catch (Exception e){ - Log.e(TAG,"at_OEM_AT_cb Exception " + e.getMessage()); + Timber.tag(TAG).e(e, "at_OEM_AT_cb Exception"); } } diff --git a/app/src/main/java/com/example/kingway/ptt/SendAtUtil.java b/app/src/main/java/com/example/kingway/ptt/SendAtUtil.java index e0be413..e25bd09 100644 --- a/app/src/main/java/com/example/kingway/ptt/SendAtUtil.java +++ b/app/src/main/java/com/example/kingway/ptt/SendAtUtil.java @@ -1,10 +1,17 @@ package com.example.kingway.ptt; +import timber.log.Timber; + /** * 指令发送类 * */ public class SendAtUtil { + private static void send(String cmd) { + Timber.tag("PTT_TX").i("CMD: %s", cmd); + MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd(cmd); + } + /** * 账号登录接口 * account 账号 @@ -20,55 +27,55 @@ public class SendAtUtil { String locationHex = str2HexStr("gps=" + location + ";"); String tempCallHex = str2HexStr("inv=" + tempCall + ";"); String loginAt = "010000" + ipHex + "" + accountHex + "" + pwdHex + locationHex + tempCallHex + "\r\n"; - MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd(loginAt); - MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("00000000\r\n"); + send(loginAt); + send("00000000\r\n"); } /** * 按下PTT开始呼叫 * */ public static void speakPlay(){ - MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("0B0000"); + send("0B0000"); } /** 松开PTT结束呼叫*/ public static void speakRelease(){ AudioRecordManager.getInstance().stopRecord(); - MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("0C0000"); + send("0C0000"); } /** 发送TCP包 登录成功后需要定时发送TCP包*/ public static void sendTCP(){ - MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("570000\r\n"); + send("570000\r\n"); } /** 发送UDP包 登录后需要定时发送UDP包*/ public static void sendUDP(){ - MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("580000\r\n"); + send("580000\r\n"); } /** 请求群组信息 */ public static void getGroupInfo(){ - MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("0D0000"); + send("0D0000"); } /** 请求群组成员信息 * groupId 十六进制群组Id * */ public static void getGroupMemberInfo(String groupId){ - MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("0E0000" + groupId); + send("0E0000" + groupId); } /** 切换群组 * groupId 十六进制群组Id * */ public static void jumpGroup(String groupId){ - MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("090000" + groupId); + send("090000" + groupId); } /** 请求好友信息 */ public static void getFriendInfo(){ - MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("0E000000000000\r\n"); + send("0E000000000000\r\n"); } diff --git a/app/src/main/java/com/stand/standapp/AndroidInterface.kt b/app/src/main/java/com/stand/standapp/AndroidInterface.kt index c4369ba..915568f 100644 --- a/app/src/main/java/com/stand/standapp/AndroidInterface.kt +++ b/app/src/main/java/com/stand/standapp/AndroidInterface.kt @@ -3,15 +3,49 @@ package com.stand.standapp import android.content.Context import android.os.Handler import android.os.Looper -import android.util.Log import android.webkit.JavascriptInterface import android.widget.Toast import android.print.PrintAttributes import android.print.PrintManager import com.stand.standapp.R +import com.stand.standapp.utils.LogManager +import timber.log.Timber +import org.json.JSONObject class AndroidInterface(private val activity: MainActivity) { + /** + * JS 调用:压缩并导出日志 + * 返回压缩后的文件路径 + */ + @JavascriptInterface + fun exportLogs(): String { + val zipPath = LogManager.zipLogs(activity) + if (zipPath != null) { + Handler(Looper.getMainLooper()).post { + Toast.makeText(activity, "日志已打包: $zipPath", Toast.LENGTH_LONG).show() + } + return zipPath + } else { + Handler(Looper.getMainLooper()).post { + Toast.makeText(activity, "暂无日志或打包失败", Toast.LENGTH_SHORT).show() + } + return "" + } + } + + /** + * JS 调用:直接上传日志到后台 + */ + @JavascriptInterface + fun uploadLogs() { + LogManager.uploadLogs(activity) { success, message -> + Handler(Looper.getMainLooper()).post { + Toast.makeText(activity, if (success) "日志上传成功" else "上传失败: $message", Toast.LENGTH_SHORT).show() + } + } + } + /** * JS 调用:标准系统打印 (PDF/网络打印机/AirPrint) * 适用于打印精美的 A4 单据或页面 @@ -33,7 +67,7 @@ class AndroidInterface(private val activity: MainActivity) { printManager.print(jobName, printAdapter, PrintAttributes.Builder().build()) } catch (e: Exception) { - Log.e("Print", "系统打印失败", e) + Timber.tag("Print").e(e, "系统打印失败") Toast.makeText(activity, "打印服务启动失败", Toast.LENGTH_SHORT).show() } } @@ -47,17 +81,17 @@ class AndroidInterface(private val activity: MainActivity) { @JavascriptInterface fun printRawData(data: String) { try { - Log.d("Print", "收到 H5 原始打印数据: $data") + Timber.tag("Print").d("收到 H5 原始打印数据: $data") Handler(Looper.getMainLooper()).post { try { // 这里可以弹出一个选择框让用户选择打印机,或者根据配置自动连接 Toast.makeText(activity, "H5指令打印: $data", Toast.LENGTH_SHORT).show() } catch (e: Exception) { - Log.e("Print", "Toast显示失败", e) + Timber.tag("Print").e(e, "Toast显示失败") } } } catch (e: Exception) { - Log.e("Print", "指令解析失败", e) + Timber.tag("Print").e(e, "指令解析失败") } } @@ -70,7 +104,7 @@ class AndroidInterface(private val activity: MainActivity) { try { UpdateManager(activity).checkUpdate() } catch (e: Exception) { - Log.e("Update", "启动更新检查失败", e) + Timber.tag("Update").e(e, "启动更新检查失败") } } } @@ -84,7 +118,7 @@ class AndroidInterface(private val activity: MainActivity) { try { activity.showExitDialog() } catch (e: Exception) { - Log.e("App", "显示退出弹窗失败", e) + Timber.tag("App").e(e, "显示退出弹窗失败") activity.finish() } } @@ -110,7 +144,7 @@ class AndroidInterface(private val activity: MainActivity) { fun getAboutInfo(): String { return try { val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0) - val json = org.json.JSONObject() + val json = JSONObject() json.put("appName", activity.getString(R.string.app_name)) json.put("versionName", packageInfo.versionName) json.put("model", android.os.Build.MODEL) @@ -134,7 +168,7 @@ class AndroidInterface(private val activity: MainActivity) { val list = updatedMsgs.split("|---|").take(5000) prefs.edit().putString("history", list.joinToString("|---|")).apply() } catch (e: Exception) { - Log.e("Storage", "消息保存异常", e) + Timber.tag("Storage").e(e, "消息保存异常") } } @@ -162,7 +196,7 @@ class AndroidInterface(private val activity: MainActivity) { Toast.makeText(activity, "记录已清空", Toast.LENGTH_SHORT).show() } } catch (e: Exception) { - Log.e("Storage", "清空记录异常", e) + Timber.tag("Storage").e(e, "清空记录异常") } } @@ -176,7 +210,7 @@ class AndroidInterface(private val activity: MainActivity) { activity.cancelTimeoutTimer() activity.findViewById(R.id.loading_layout)?.visibility = android.view.View.GONE } catch (e: Exception) { - Log.e("UI", "隐藏加载布局失败", e) + Timber.tag("UI").e(e, "隐藏加载布局失败") } } } diff --git a/app/src/main/java/com/stand/standapp/LoginActivity.kt b/app/src/main/java/com/stand/standapp/LoginActivity.kt index 0957fc3..8a96042 100644 --- a/app/src/main/java/com/stand/standapp/LoginActivity.kt +++ b/app/src/main/java/com/stand/standapp/LoginActivity.kt @@ -2,7 +2,7 @@ package com.stand.standapp import android.content.Intent import android.os.Bundle -import android.util.Log +import timber.log.Timber import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity @@ -68,7 +68,8 @@ class LoginActivity : AppCompatActivity() { * 执行真实接口登录 */ private fun performLogin(username: String, password: String, remember: Boolean) { - val loginUrl = "https://template.gyouzhe.com/api/auth/login" + val serverUrl = AppConfig.getServerUrl(this) + val loginUrl = "$serverUrl/api/auth/login" val bearerToken = "eyJraWQiOiJzLXYxIiwidHlwIjoiSldUIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiJhZG1pbiIsInR5cCI6ImFjY2VzcyIsImV4cCI6MTc3NjQwMTU2MSwianRpIjoiMjM0NjdiZGMtNTY0Zi00NmY0LWJhZTgtNzJhZmQ1MzhmYmZhIiwiZmdwIjoiY2FhNzYzOTI4YWRhYWZiYjgzMjE3NjExYmVjYTc3ZjMiLCJraWQiOiJzLXYxIn0.4Zf1eiKcUjGNoFylEzmZ2no1kJY-44CQ8QWmMA_UxvY" val body = MultipartBody.Builder() @@ -123,7 +124,7 @@ class LoginActivity : AppCompatActivity() { Toast.makeText(this@LoginActivity, msg, Toast.LENGTH_SHORT).show() } } catch (e: Exception) { - Log.e("Login", "解析响应失败", e) + Timber.tag("Login").e(e, "解析响应失败") Toast.makeText(this@LoginActivity, "服务器响应异常", Toast.LENGTH_SHORT).show() } } diff --git a/app/src/main/java/com/stand/standapp/MainActivity.kt b/app/src/main/java/com/stand/standapp/MainActivity.kt index 8b0512a..079f4d8 100644 --- a/app/src/main/java/com/stand/standapp/MainActivity.kt +++ b/app/src/main/java/com/stand/standapp/MainActivity.kt @@ -3,12 +3,20 @@ package com.stand.standapp import android.os.Bundle import android.view.KeyEvent import android.view.ViewGroup +import android.webkit.ConsoleMessage +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebView import android.widget.LinearLayout -import android.util.Log -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import timber.log.Timber import com.just.agentweb.AgentWeb +import com.just.agentweb.WebChromeClient +import com.just.agentweb.WebViewClient import com.stand.standapp.R +import com.stand.standapp.AppConfig +import com.stand.standapp.AndroidInterface +import com.stand.standapp.UpdateManager class MainActivity : AppCompatActivity() { @@ -38,11 +46,31 @@ class MainActivity : AppCompatActivity() { // 从配置中获取服务器地址 val serverUrl = AppConfig.getServerUrl(this) val finalUrl = "$serverUrl/example/print_demo.html?t=${System.currentTimeMillis()}" - Log.d("MainActivity", "Loading URL: $finalUrl") + Timber.tag("MainActivity").d("Loading URL: $finalUrl") mAgentWeb = AgentWeb.with(this) .setAgentWebParent(container, LinearLayout.LayoutParams(-1, -1)) .useDefaultIndicator() + .setWebChromeClient(object : WebChromeClient() { + override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { + Timber.tag("WebConsole").d("${consoleMessage?.messageLevel()}: ${consoleMessage?.message()} -- From line ${consoleMessage?.lineNumber()} of ${consoleMessage?.sourceId()}") + return super.onConsoleMessage(consoleMessage) + } + }) + .setWebViewClient(object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + cancelTimeoutTimer() + } + + override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) { + Timber.tag("WebError").e("URL: ${request?.url}, Error: ${error?.description} (Code: ${error?.errorCode})") + if (request?.isForMainFrame == true) { + showErrorPage() + } + super.onReceivedError(view, request, error) + } + }) .addJavascriptInterface("android", AndroidInterface(this)) .createAgentWeb() .ready() @@ -66,7 +94,7 @@ class MainActivity : AppCompatActivity() { settings.domStorageEnabled = true settings.javaScriptEnabled = true } catch (e: Exception) { - Log.e("MainActivity", "WebView settings error: ${e.message}") + Timber.tag("MainActivity").e(e, "WebView settings error") } } // .go("http://152.32.186.199/local/print_demo.html") @@ -75,8 +103,7 @@ class MainActivity : AppCompatActivity() { UpdateManager(this).checkUpdate() } catch (e: Exception) { - Log.e("MainActivity", "Fatal Crash: ${e.message}") - e.printStackTrace() + Timber.tag("MainActivity").e(e, "Fatal Crash") } } diff --git a/app/src/main/java/com/stand/standapp/MyApplication.kt b/app/src/main/java/com/stand/standapp/MyApplication.kt new file mode 100644 index 0000000..75901c8 --- /dev/null +++ b/app/src/main/java/com/stand/standapp/MyApplication.kt @@ -0,0 +1,20 @@ +package com.stand.standapp + +import android.app.Application +import com.stand.standapp.utils.LogManager +import com.example.kingway.ptt.MyApplication as PTTMyApplication +import timber.log.Timber + +class MyApplication : Application() { + + override fun onCreate() { + super.onCreate() + // Initialize Timber and File Logging + LogManager.init(this) + + // Initialize PTT Module + PTTMyApplication.init(this) + + Timber.tag("MyApplication").i("Application onCreate - Logging system and PTT started") + } +} diff --git a/app/src/main/java/com/stand/standapp/UpdateManager.kt b/app/src/main/java/com/stand/standapp/UpdateManager.kt index a9c8352..580c368 100644 --- a/app/src/main/java/com/stand/standapp/UpdateManager.kt +++ b/app/src/main/java/com/stand/standapp/UpdateManager.kt @@ -10,7 +10,7 @@ import android.os.Environment import android.os.Handler import android.os.Looper import android.provider.MediaStore -import android.util.Log +import timber.log.Timber import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity @@ -63,7 +63,7 @@ class UpdateManager(private val context: Context) { client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { - Log.e("UpdateManager", "检查更新失败: ${e.message}") + Timber.tag("UpdateManager").e(e, "检查更新失败") } override fun onResponse(call: Call, response: Response) { @@ -104,7 +104,7 @@ class UpdateManager(private val context: Context) { handler.post { Toast.makeText(context, "更新服务提示: $msg", Toast.LENGTH_SHORT).show() } } } catch (e: Exception) { - Log.e("UpdateManager", "解析或处理更新结果失败", e) + Timber.tag("UpdateManager").e(e, "解析或处理更新结果失败") handler.post { Toast.makeText(context, "更新信息解析失败", Toast.LENGTH_SHORT).show() } } } @@ -266,7 +266,7 @@ class UpdateManager(private val context: Context) { intent.setDataAndType(installUri, "application/vnd.android.package-archive") context.startActivity(intent) } catch (e: Exception) { - Log.e("UpdateManager", "安装失败", e) + Timber.tag("UpdateManager").e(e, "安装失败") Toast.makeText(context, "无法自动调起安装,请在系统下载目录中手动安装", Toast.LENGTH_LONG).show() } } diff --git a/app/src/main/java/com/stand/standapp/utils/LogManager.kt b/app/src/main/java/com/stand/standapp/utils/LogManager.kt new file mode 100644 index 0000000..70643ec --- /dev/null +++ b/app/src/main/java/com/stand/standapp/utils/LogManager.kt @@ -0,0 +1,135 @@ +package com.stand.standapp.utils + +import android.content.Context +import com.stand.standapp.AppConfig +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.asRequestBody +import timber.log.Timber +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.FileWriter +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.* +import java.util.concurrent.Executors +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +object LogManager { + private val executor = Executors.newSingleThreadExecutor() + private const val MAX_DAYS = 15 + private val client = OkHttpClient() + + fun init(context: Context) { + Timber.plant(Timber.DebugTree()) + val logDir = File(context.getExternalFilesDir(null), "logs") + if (!logDir.exists()) logDir.mkdirs() + cleanOldLogs(logDir) + Timber.plant(FileLoggingTree(logDir)) + } + + private fun cleanOldLogs(logDir: 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() + } + } + } catch (e: Exception) {} + } + } + + fun zipLogs(context: Context): String? { + val logDir = File(context.getExternalFilesDir(null), "logs") + if (!logDir.exists() || logDir.listFiles().isNullOrEmpty()) return null + val zipFile = File(context.getExternalFilesDir(null), "logs_${System.currentTimeMillis()}.zip") + return try { + ZipOutputStream(FileOutputStream(zipFile)).use { zos -> + logDir.listFiles()?.forEach { file -> + if (file.isFile && file.name.endsWith(".log")) { + zos.putNextEntry(ZipEntry(file.name)) + FileInputStream(file).use { it.copyTo(zos) } + zos.closeEntry() + } + } + } + zipFile.absolutePath + } catch (e: Exception) { + Timber.e(e, "Zip logs failed") + 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 serverUrl = AppConfig.getServerUrl(context) + val uploadUrl = "$serverUrl/api/attachment/upload" + + // 使用固定的 Token (根据用户提供) + 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) + .build() + + try { + client.newCall(request).execute().use { response -> + if (response.isSuccessful) { + callback(true, "上传成功") + // 上传成功后删除临时 zip 文件 + file.delete() + } else { + callback(false, "服务器返回错误: ${response.code}") + } + } + } catch (e: IOException) { + callback(false, "网络连接失败: ${e.message}") + } + } + } + + private class FileLoggingTree(private val logDir: File) : Timber.Tree() { + private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) + private val fileNameFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + + 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) {} + } + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9bd964a..51d8518 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - StandAPP + 消防值守台 \ No newline at end of file