Compare commits
17 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
62cb78167d | |
|
|
d9b65c0e7e | |
|
|
992c18d8dc | |
|
|
4cb6b4ed08 | |
|
|
854e04b28f | |
|
|
eaf169c626 | |
|
|
4465887c96 | |
|
|
4b2bf8f5f4 | |
|
|
7060b99dd9 | |
|
|
58885f69a9 | |
|
|
01dec2de0e | |
|
|
a2c8fd2c1a | |
|
|
414681fa26 | |
|
|
450e04db89 | |
|
|
a982e54eeb | |
|
|
8bddca4996 | |
|
|
397e103596 |
|
|
@ -0,0 +1,16 @@
|
||||||
|
# CodeGraph data files
|
||||||
|
# These are local to each machine and should not be committed
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.db
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
cache/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Hook markers
|
||||||
|
.dirty
|
||||||
|
|
@ -23,7 +23,8 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:theme="@style/Theme.StandAPP">
|
android:theme="@style/Theme.StandAPP"
|
||||||
|
android:hardwareAccelerated="true">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SplashActivity"
|
android:name=".SplashActivity"
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
' setInterval(function() {',
|
' setInterval(function() {',
|
||||||
' var code = prompt("bridge:_poll", "");',
|
' var code = prompt("bridge:_poll", "");',
|
||||||
' if (code) { try { eval(code); } catch(e) {} }',
|
' if (code) { try { eval(code); } catch(e) {} }',
|
||||||
' }, 500);',
|
' }, 2000);',
|
||||||
'})();'
|
'})();'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ import android.util.Log;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
@ -17,17 +20,20 @@ public class CallBackResolution {
|
||||||
private static String TAG = "PTT_RX";
|
private static String TAG = "PTT_RX";
|
||||||
private static String indexGroupId = "";//用户所在群组Id
|
private static String indexGroupId = "";//用户所在群组Id
|
||||||
private static String indexGroupName = "";//用户所在群组名称
|
private static String indexGroupName = "";//用户所在群组名称
|
||||||
private static List<GroupMemberInfoDto> memberInfoDtos = new ArrayList<>();
|
private static final int MAX_GROUP_MEMBERS = 500;
|
||||||
|
private static final int MAX_GROUP_INFOS = 100;
|
||||||
|
private static final Map<String, GroupMemberInfoDto> memberInfoDtos = new LinkedHashMap<>(MAX_GROUP_MEMBERS);
|
||||||
private static int speckType = 1;
|
private static int speckType = 1;
|
||||||
private static String talkId = "";
|
private static String talkId = "";
|
||||||
private static String talkName = "";
|
private static String talkName = "";
|
||||||
private static ArrayList<GroupInfoDto> groupInfos = new ArrayList<>();
|
private static final Map<String, GroupInfoDto> groupInfos = new LinkedHashMap<>(MAX_GROUP_INFOS);
|
||||||
private static String oldLoginSate = "00";
|
private static String oldLoginSate = "00";
|
||||||
private static int logNum = 0;
|
private static int logNum = 0;
|
||||||
private static long offlineTime = 0;
|
private static long offlineTime = 0;
|
||||||
private static boolean loginState = false;
|
private static boolean loginState = false;
|
||||||
private static String pttLoginStatus = "00"; // 00-未登录 01-登录中 02-已登录
|
private static String pttLoginStatus = "00"; // 00-未登录 01-登录中 02-已登录
|
||||||
private static String pttLoginId = "";
|
private static String pttLoginId = "";
|
||||||
|
private static ScheduledExecutorService keepAliveExecutor = null;
|
||||||
|
|
||||||
public static String getPttLoginStatus() {
|
public static String getPttLoginStatus() {
|
||||||
return pttLoginStatus;
|
return pttLoginStatus;
|
||||||
|
|
@ -139,7 +145,7 @@ public class CallBackResolution {
|
||||||
Timber.tag(TAG).d( "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();
|
if (1 == groupNo) groupInfos.clear();
|
||||||
GroupInfoDto groupInfo = new GroupInfoDto(groupId, tempTxt, groupNo, groupCount);
|
GroupInfoDto groupInfo = new GroupInfoDto(groupId, tempTxt, groupNo, groupCount);
|
||||||
groupInfos.add(groupInfo);
|
groupInfos.put(groupId, groupInfo);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -168,7 +174,7 @@ public class CallBackResolution {
|
||||||
String tempTxt = unicodeToString(groupMemberName.toString());
|
String tempTxt = unicodeToString(groupMemberName.toString());
|
||||||
GroupMemberInfoDto infoDto = new GroupMemberInfoDto(memberId, tempTxt, status, memberNo, haveVideo);
|
GroupMemberInfoDto infoDto = new GroupMemberInfoDto(memberId, tempTxt, status, memberNo, haveVideo);
|
||||||
if (memberNo == 1) memberInfoDtos.clear();
|
if (memberNo == 1) memberInfoDtos.clear();
|
||||||
memberInfoDtos.add(infoDto);
|
memberInfoDtos.put(memberId, infoDto);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -543,13 +549,16 @@ public class CallBackResolution {
|
||||||
if (tempTxt.contains("已登录")) {
|
if (tempTxt.contains("已登录")) {
|
||||||
String showName = tempTxt.substring(3, tempTxt.length());
|
String showName = tempTxt.substring(3, tempTxt.length());
|
||||||
CallBackUtil.callBackLogin(true, showName);
|
CallBackUtil.callBackLogin(true, showName);
|
||||||
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
|
if (keepAliveExecutor != null && !keepAliveExecutor.isShutdown()) {
|
||||||
|
keepAliveExecutor.shutdownNow();
|
||||||
|
}
|
||||||
|
keepAliveExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
Runnable task = () -> {
|
Runnable task = () -> {
|
||||||
Timber.tag(TAG).d( "callBackLogin send tcp and udp");
|
Timber.tag(TAG).d( "callBackLogin send tcp and udp");
|
||||||
SendAtUtil.sendUDP();
|
SendAtUtil.sendUDP();
|
||||||
SendAtUtil.sendTCP();
|
SendAtUtil.sendTCP();
|
||||||
};
|
};
|
||||||
executor.scheduleAtFixedRate(task, 0, 40, TimeUnit.SECONDS);
|
keepAliveExecutor.scheduleAtFixedRate(task, 0, 40, TimeUnit.SECONDS);
|
||||||
} else if (tempTxt.contains("账号已更新")) {
|
} else if (tempTxt.contains("账号已更新")) {
|
||||||
Timber.tag(TAG).w("TTS reports account updated: '%s'", tempTxt);
|
Timber.tag(TAG).w("TTS reports account updated: '%s'", tempTxt);
|
||||||
} else if (tempTxt.contains("账号或密码错误")) {
|
} else if (tempTxt.contains("账号或密码错误")) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
package com.example.kingway.ptt;
|
package com.example.kingway.ptt;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**指令返回类
|
/**指令返回类
|
||||||
* 说明:操作UI请在主线程进行
|
* 说明:操作UI请在主线程进行
|
||||||
|
|
@ -11,6 +16,45 @@ import java.util.List;
|
||||||
public class CallBackUtil {
|
public class CallBackUtil {
|
||||||
private static String TAG = "TYT_CallBackUtil";
|
private static String TAG = "TYT_CallBackUtil";
|
||||||
|
|
||||||
|
private static final long THROTTLE_INTERVAL_MS = 100L;
|
||||||
|
private static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
|
||||||
|
private static final ConcurrentHashMap<String, Long> lastPushAt = new ConcurrentHashMap<>();
|
||||||
|
private static final ConcurrentHashMap<String, Runnable> pendingRunnables = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static void pushJsThrottled(String eventName, String script) {
|
||||||
|
long now = SystemClock.uptimeMillis();
|
||||||
|
Long last = lastPushAt.get(eventName);
|
||||||
|
if (last == null || now - last >= THROTTLE_INTERVAL_MS) {
|
||||||
|
lastPushAt.put(eventName, now);
|
||||||
|
cancelPending(eventName);
|
||||||
|
com.stand.standapp.MainActivity.executeJs(script);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Runnable prev = pendingRunnables.get(eventName);
|
||||||
|
if (prev != null) {
|
||||||
|
MAIN_HANDLER.removeCallbacks(prev);
|
||||||
|
}
|
||||||
|
final long scheduledAt = now;
|
||||||
|
Runnable task = () -> {
|
||||||
|
lastPushAt.put(eventName, SystemClock.uptimeMillis());
|
||||||
|
pendingRunnables.remove(eventName);
|
||||||
|
com.stand.standapp.MainActivity.executeJs(script);
|
||||||
|
};
|
||||||
|
pendingRunnables.put(eventName, task);
|
||||||
|
MAIN_HANDLER.postDelayed(task, THROTTLE_INTERVAL_MS - (now - last));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void cancelPending(String eventName) {
|
||||||
|
Runnable r = pendingRunnables.remove(eventName);
|
||||||
|
if (r != null) {
|
||||||
|
MAIN_HANDLER.removeCallbacks(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void pushJsImmediate(String script) {
|
||||||
|
com.stand.standapp.MainActivity.executeJs(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回登录
|
* 返回登录
|
||||||
|
|
@ -68,11 +112,11 @@ public class CallBackUtil {
|
||||||
* 查询群组成员返回
|
* 查询群组成员返回
|
||||||
* state true-成功, false-失败
|
* state true-成功, false-失败
|
||||||
*/
|
*/
|
||||||
public static void callBackOnMemberSuccess(boolean state, List<GroupMemberInfoDto> dtos) {
|
public static void callBackOnMemberSuccess(boolean state, java.util.Map<String, GroupMemberInfoDto> dtos) {
|
||||||
try {
|
try {
|
||||||
org.json.JSONArray arr = new org.json.JSONArray();
|
org.json.JSONArray arr = new org.json.JSONArray();
|
||||||
if (dtos != null && state) {
|
if (dtos != null && state) {
|
||||||
for (GroupMemberInfoDto dto : dtos) {
|
for (GroupMemberInfoDto dto : dtos.values()) {
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
org.json.JSONObject obj = new org.json.JSONObject();
|
||||||
obj.put("memberId", dto.getMemberId());
|
obj.put("memberId", dto.getMemberId());
|
||||||
obj.put("memberName", dto.getGmemberName());
|
obj.put("memberName", dto.getGmemberName());
|
||||||
|
|
@ -80,7 +124,7 @@ public class CallBackUtil {
|
||||||
arr.put(obj);
|
arr.put(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('MemberList', " + arr.toString() + ");}");
|
pushJsThrottled("MemberList", "if(window.onPttEvent){window.onPttEvent('MemberList', " + arr.toString() + ");}");
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,17 +172,17 @@ public class CallBackUtil {
|
||||||
* 返回群组集合信息
|
* 返回群组集合信息
|
||||||
* GroupInfoDto 群组信息
|
* GroupInfoDto 群组信息
|
||||||
* */
|
* */
|
||||||
public static void callBackGroupInfo (ArrayList < GroupInfoDto > groupInfoDtos) {
|
public static void callBackGroupInfo (java.util.Map<String, GroupInfoDto> groupInfoDtos) {
|
||||||
try {
|
try {
|
||||||
org.json.JSONArray arr = new org.json.JSONArray();
|
org.json.JSONArray arr = new org.json.JSONArray();
|
||||||
for (GroupInfoDto dto : groupInfoDtos) {
|
for (GroupInfoDto dto : groupInfoDtos.values()) {
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
org.json.JSONObject obj = new org.json.JSONObject();
|
||||||
obj.put("groupId", dto.getGroupId());
|
obj.put("groupId", dto.getGroupId());
|
||||||
obj.put("groupName", dto.getGroupName());
|
obj.put("groupName", dto.getGroupName());
|
||||||
obj.put("groupNo", dto.getGroupNo());
|
obj.put("groupNo", dto.getGroupNo());
|
||||||
arr.put(obj);
|
arr.put(obj);
|
||||||
}
|
}
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('GroupList', " + arr.toString() + ");}");
|
pushJsThrottled("GroupList", "if(window.onPttEvent){window.onPttEvent('GroupList', " + arr.toString() + ");}");
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,7 +201,7 @@ public class CallBackUtil {
|
||||||
obj.put("groupName", groupName);
|
obj.put("groupName", groupName);
|
||||||
obj.put("talkId", talkId);
|
obj.put("talkId", talkId);
|
||||||
obj.put("talkName", talkName);
|
obj.put("talkName", talkName);
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('SpeakerUpdate', " + obj.toString() + ");}");
|
pushJsThrottled("SpeakerUpdate", "if(window.onPttEvent){window.onPttEvent('SpeakerUpdate', " + obj.toString() + ");}");
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,7 +215,7 @@ public class CallBackUtil {
|
||||||
obj.put("groupName", groupName);
|
obj.put("groupName", groupName);
|
||||||
obj.put("talkId", talkId);
|
obj.put("talkId", talkId);
|
||||||
obj.put("talkName", talkName);
|
obj.put("talkName", talkName);
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('PlayStatus', " + obj.toString() + ");}");
|
pushJsThrottled("PlayStatus", "if(window.onPttEvent){window.onPttEvent('PlayStatus', " + obj.toString() + ");}");
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,7 +255,7 @@ public class CallBackUtil {
|
||||||
obj.put("latitude", latitude);
|
obj.put("latitude", latitude);
|
||||||
obj.put("longitude", longitude);
|
obj.put("longitude", longitude);
|
||||||
obj.put("time", time);
|
obj.put("time", time);
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('LocationStatus', " + obj.toString() + ");}");
|
pushJsThrottled("LocationStatus", "if(window.onPttEvent){window.onPttEvent('LocationStatus', " + obj.toString() + ");}");
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.hjq.permissions.Permission
|
import com.hjq.permissions.Permission
|
||||||
import com.hjq.permissions.XXPermissions
|
import com.hjq.permissions.XXPermissions
|
||||||
|
import com.stand.standapp.net.NetworkModule
|
||||||
import com.stand.standapp.printer.PrinterManager
|
import com.stand.standapp.printer.PrinterManager
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
@ -17,7 +18,7 @@ import java.io.IOException
|
||||||
|
|
||||||
class LoginActivity : AppCompatActivity() {
|
class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val client = OkHttpClient()
|
private val client = NetworkModule.defaultClient
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Volatile
|
@Volatile
|
||||||
private var pendingJsCode: String? = null
|
private var pendingJsCode: String? = null
|
||||||
|
|
||||||
|
private val jsLock = Any()
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun executeJs(script: String) {
|
fun executeJs(script: String) {
|
||||||
Timber.tag("ExecuteJs").d(script)
|
Timber.tag("ExecuteJs").d(script)
|
||||||
synchronized(pendingJsCode ?: Any()) {
|
synchronized(jsLock) {
|
||||||
// 合并多次调用(用换行分隔)
|
// 合并多次调用(用换行分隔)
|
||||||
pendingJsCode = if (pendingJsCode != null) "$pendingJsCode;$script" else script
|
pendingJsCode = if (pendingJsCode != null) "$pendingJsCode;$script" else script
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +121,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
// 获取并清除待执行的 JS 代码(由 bridge.js 轮询)
|
// 获取并清除待执行的 JS 代码(由 bridge.js 轮询)
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun pollPendingJs(): String {
|
fun pollPendingJs(): String {
|
||||||
synchronized(pendingJsCode ?: Any()) {
|
synchronized(jsLock) {
|
||||||
val code = pendingJsCode ?: ""
|
val code = pendingJsCode ?: ""
|
||||||
pendingJsCode = null
|
pendingJsCode = null
|
||||||
return code
|
return code
|
||||||
|
|
@ -472,15 +474,34 @@ class MainActivity : AppCompatActivity() {
|
||||||
return super.onKeyDown(keyCode, event)
|
return super.onKeyDown(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
geckoSession?.setActive(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
geckoSession?.setActive(true)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
cancelTimeoutTimer()
|
cancelTimeoutTimer()
|
||||||
geckoView.releaseSession()
|
geckoView.releaseSession()
|
||||||
geckoSession?.close()
|
geckoSession?.close()
|
||||||
geckoSession = null
|
geckoSession = null
|
||||||
|
clearCompanionState()
|
||||||
instance = null
|
instance = null
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearCompanionState() {
|
||||||
|
synchronized(jsLock) {
|
||||||
|
pendingJsCode = null
|
||||||
|
}
|
||||||
|
pendingDialog = null
|
||||||
|
isConflictDialogShowing = false
|
||||||
|
}
|
||||||
|
|
||||||
private fun startTimeoutTimer() {
|
private fun startTimeoutTimer() {
|
||||||
cancelTimeoutTimer()
|
cancelTimeoutTimer()
|
||||||
mHandler.postDelayed(timeoutRunnable, TIMEOUT_MS)
|
mHandler.postDelayed(timeoutRunnable, TIMEOUT_MS)
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,37 @@ import android.os.Looper
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
||||||
class SplashActivity : AppCompatActivity() {
|
class SplashActivity : AppCompatActivity() {
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
private var checkCount = 0
|
||||||
|
private val maxChecks = 20 // 最多检查20次(2秒)
|
||||||
|
|
||||||
|
private val checkInitRunnable = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
if (MyApplication.pNative != null || checkCount >= maxChecks) {
|
||||||
|
navigateToLogin()
|
||||||
|
} else {
|
||||||
|
checkCount++
|
||||||
|
handler.postDelayed(this, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_splash)
|
setContentView(R.layout.activity_splash)
|
||||||
|
|
||||||
// 延迟 2 秒进入登录界面
|
// 异步检查PTT JNI初始化状态,避免硬等待2秒
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
handler.post(checkInitRunnable)
|
||||||
startActivity(Intent(this, LoginActivity::class.java))
|
}
|
||||||
finish()
|
|
||||||
}, 2000)
|
private fun navigateToLogin() {
|
||||||
|
if (isFinishing || isDestroyed) return
|
||||||
|
startActivity(Intent(this, LoginActivity::class.java))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
handler.removeCallbacks(checkInitRunnable)
|
||||||
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,11 +114,9 @@ class TempCallActivity : AppCompatActivity() {
|
||||||
pulseAnimator?.cancel()
|
pulseAnimator?.cancel()
|
||||||
if (instance == this) {
|
if (instance == this) {
|
||||||
instance = null
|
instance = null
|
||||||
// 如果不是最小化状态,说明是真正结束,清除所有状态
|
|
||||||
if (!isMinimized) {
|
if (!isMinimized) {
|
||||||
currentGroupName = "临时会话"
|
currentGroupName = "临时会话"
|
||||||
}
|
}
|
||||||
// 通知 web 页面状态变化
|
|
||||||
MainActivity.executeJs("onTempCallStateChanged('" + if (isMinimized) "minimized" else "destroyed" + "')")
|
MainActivity.executeJs("onTempCallStateChanged('" + if (isMinimized) "minimized" else "destroyed" + "')")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
|
import com.stand.standapp.net.NetworkModule
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -58,7 +59,7 @@ class UpdateManager(private val context: Context) {
|
||||||
|
|
||||||
val url = "$serverUrl/api/app-version/check-update?versionCode=$currentVersionCode"
|
val url = "$serverUrl/api/app-version/check-update?versionCode=$currentVersionCode"
|
||||||
|
|
||||||
val client = OkHttpClient()
|
val client = NetworkModule.defaultClient
|
||||||
val request = Request.Builder().url(url).build()
|
val request = Request.Builder().url(url).build()
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
|
@ -139,7 +140,7 @@ class UpdateManager(private val context: Context) {
|
||||||
private fun startManualDownload(url: String) {
|
private fun startManualDownload(url: String) {
|
||||||
showProgressDialog()
|
showProgressDialog()
|
||||||
|
|
||||||
val client = OkHttpClient()
|
val client = NetworkModule.defaultClient
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.addHeader("User-Agent", "Mozilla/5.0 (Android)")
|
.addHeader("User-Agent", "Mozilla/5.0 (Android)")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.stand.standapp.net
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
object NetworkModule {
|
||||||
|
private const val DEFAULT_CONNECT_TIMEOUT_SEC = 15L
|
||||||
|
private const val DEFAULT_READ_TIMEOUT_SEC = 30L
|
||||||
|
private const val DEFAULT_WRITE_TIMEOUT_SEC = 30L
|
||||||
|
|
||||||
|
val defaultClient: OkHttpClient by lazy {
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.connectTimeout(DEFAULT_CONNECT_TIMEOUT_SEC, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(DEFAULT_READ_TIMEOUT_SEC, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(DEFAULT_WRITE_TIMEOUT_SEC, TimeUnit.SECONDS)
|
||||||
|
.retryOnConnectionFailure(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun streamingClient(): OkHttpClient {
|
||||||
|
return defaultClient.newBuilder()
|
||||||
|
.readTimeout(0, TimeUnit.MILLISECONDS)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,11 +13,16 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
private const val STREAM_FLUSH_INTERVAL_MS = 50L
|
||||||
|
|
||||||
class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private val repository = AiChatRepository()
|
private val repository = AiChatRepository()
|
||||||
|
|
||||||
|
|
@ -30,14 +35,49 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList())
|
private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList())
|
||||||
val messages: StateFlow<List<ChatMessage>> = _messages.asStateFlow()
|
val messages: StateFlow<List<ChatMessage>> = _messages.asStateFlow()
|
||||||
|
|
||||||
// 👈 声明一个正在加载的状态,保证上一个问题没结束前无法再次发送提问!
|
private val _streamingContent = MutableStateFlow<Map<String, String>>(emptyMap())
|
||||||
|
val streamingContent: StateFlow<Map<String, String>> = _streamingContent.asStateFlow()
|
||||||
|
|
||||||
private val _isLoading = MutableStateFlow(false)
|
private val _isLoading = MutableStateFlow(false)
|
||||||
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
||||||
|
|
||||||
// 👈 额外声明一个当前协程控制作业,用于物理取消/中止 SSE 对话!
|
|
||||||
private var activeChatJob: kotlinx.coroutines.Job? = null
|
private var activeChatJob: kotlinx.coroutines.Job? = null
|
||||||
|
|
||||||
private val prefs = application.getSharedPreferences("ai_chat_cache", Context.MODE_PRIVATE)
|
private val prefs = application.getSharedPreferences("ai_chat_cache", Context.MODE_PRIVATE)
|
||||||
|
private val chunkBuffer = java.util.concurrent.ConcurrentHashMap<String, StringBuilder>()
|
||||||
|
private val lastFlushAt = java.util.concurrent.ConcurrentHashMap<String, Long>()
|
||||||
|
private val flushScope = kotlinx.coroutines.CoroutineScope(
|
||||||
|
kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.Main.immediate
|
||||||
|
)
|
||||||
|
private val flushMutex = kotlinx.coroutines.sync.Mutex()
|
||||||
|
|
||||||
|
private fun scheduleFlush(messageId: String) {
|
||||||
|
val now = android.os.SystemClock.uptimeMillis()
|
||||||
|
val last = lastFlushAt[messageId] ?: 0
|
||||||
|
val delta = now - last
|
||||||
|
if (delta >= STREAM_FLUSH_INTERVAL_MS) {
|
||||||
|
flushStreamingMessage(messageId)
|
||||||
|
} else {
|
||||||
|
flushScope.launch {
|
||||||
|
kotlinx.coroutines.delay(STREAM_FLUSH_INTERVAL_MS - delta)
|
||||||
|
flushStreamingMessage(messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flushStreamingMessage(messageId: String) {
|
||||||
|
flushScope.launch {
|
||||||
|
flushMutex.withLock {
|
||||||
|
val buffer = chunkBuffer.remove(messageId) ?: return@withLock
|
||||||
|
val pending = buffer.toString()
|
||||||
|
if (pending.isEmpty()) return@withLock
|
||||||
|
_streamingContent.update { current ->
|
||||||
|
current + (messageId to (current[messageId].orEmpty() + pending))
|
||||||
|
}
|
||||||
|
lastFlushAt[messageId] = android.os.SystemClock.uptimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// 👈 将数据加载移到IO线程,避免阻塞主线程导致UI卡顿
|
// 👈 将数据加载移到IO线程,避免阻塞主线程导致UI卡顿
|
||||||
|
|
@ -132,18 +172,21 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
// 👈 用户手动中断当前正在生成的 AI 回答的方法!
|
// 👈 用户手动中断当前正在生成的 AI 回答的方法!
|
||||||
fun cancelActiveStreaming() {
|
fun cancelActiveStreaming() {
|
||||||
activeChatJob?.cancel() // 物理取消协程作业,断开 SSE OkHttp 连接并关闭流!
|
activeChatJob?.cancel()
|
||||||
activeChatJob = null
|
activeChatJob = null
|
||||||
|
|
||||||
// 将当前正在流式输出的 AI 消息状态重置为完成,防止气泡卡死在 streaming 样式
|
|
||||||
_messages.value = _messages.value.map { msg ->
|
_messages.value = _messages.value.map { msg ->
|
||||||
if (msg.isStreaming) {
|
if (msg.isStreaming) {
|
||||||
msg.copy(isStreaming = false)
|
val finalContent = _streamingContent.value[msg.id] ?: msg.content
|
||||||
|
msg.copy(content = finalContent, isStreaming = false)
|
||||||
} else msg
|
} else msg
|
||||||
}
|
}
|
||||||
|
_streamingContent.value = emptyMap()
|
||||||
_isLoading.value = false // 释放锁,允许用户立刻开始下一次提问!
|
chunkBuffer.clear()
|
||||||
saveCacheToLocal() // 强制存盘归档
|
lastFlushAt.clear()
|
||||||
|
|
||||||
|
_isLoading.value = false
|
||||||
|
saveCacheToLocal()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNewSession() {
|
fun createNewSession() {
|
||||||
|
|
@ -232,20 +275,21 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun appendAiMessageChunk(messageId: String, chunk: String) {
|
private fun appendAiMessageChunk(messageId: String, chunk: String) {
|
||||||
_messages.value = _messages.value.map { msg ->
|
chunkBuffer.computeIfAbsent(messageId) { StringBuilder() }.append(chunk)
|
||||||
if (msg.id == messageId) {
|
scheduleFlush(messageId)
|
||||||
msg.copy(content = msg.content + chunk)
|
|
||||||
} else msg
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun finalizeAiMessage(messageId: String) {
|
private fun finalizeAiMessage(messageId: String) {
|
||||||
|
val finalResidue = chunkBuffer.remove(messageId)?.toString() ?: ""
|
||||||
_messages.value = _messages.value.map { msg ->
|
_messages.value = _messages.value.map { msg ->
|
||||||
if (msg.id == messageId) {
|
if (msg.id == messageId) {
|
||||||
msg.copy(isStreaming = false)
|
val finalContent = (_streamingContent.value[messageId] ?: msg.content) + finalResidue
|
||||||
|
_streamingContent.update { it - messageId }
|
||||||
|
msg.copy(content = finalContent, isStreaming = false)
|
||||||
} else msg
|
} else msg
|
||||||
}
|
}
|
||||||
saveCacheToLocal() // 👈 AI 回复完毕后,状态变更为非 streaming 并归档存盘
|
lastFlushAt.remove(messageId)
|
||||||
|
saveCacheToLocal()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSessionTitleIfFirstMessage(sessionId: String, content: String) {
|
private fun updateSessionTitleIfFirstMessage(sessionId: String, content: String) {
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,18 @@ fun ChatArea(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkwonRenderer(markdown: String, modifier: Modifier) {
|
fun MarkwonRenderer(markdown: String, modifier: Modifier) {
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
val markwon = remember(context) {
|
||||||
|
io.noties.markwon.Markwon.builder(context)
|
||||||
|
.usePlugin(io.noties.markwon.core.CorePlugin.create())
|
||||||
|
.usePlugin(io.noties.markwon.ext.tables.TablePlugin.create(context))
|
||||||
|
.usePlugin(io.noties.markwon.html.HtmlPlugin.create())
|
||||||
|
.usePlugin(io.noties.markwon.linkify.LinkifyPlugin.create())
|
||||||
|
.usePlugin(io.noties.markwon.ext.strikethrough.StrikethroughPlugin.create())
|
||||||
|
.usePlugin(io.noties.markwon.ext.tasklist.TaskListPlugin.create(context))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
androidx.compose.ui.viewinterop.AndroidView(
|
androidx.compose.ui.viewinterop.AndroidView(
|
||||||
factory = { ctx ->
|
factory = { ctx ->
|
||||||
val textView = SafeSelectableTextView(ctx).apply {
|
val textView = SafeSelectableTextView(ctx).apply {
|
||||||
|
|
@ -173,23 +185,14 @@ fun MarkwonRenderer(markdown: String, modifier: Modifier) {
|
||||||
setPadding(0, 0, 0, 0)
|
setPadding(0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val markwon = io.noties.markwon.Markwon.builder(ctx)
|
|
||||||
.usePlugin(io.noties.markwon.core.CorePlugin.create())
|
|
||||||
.usePlugin(io.noties.markwon.ext.tables.TablePlugin.create(ctx))
|
|
||||||
.usePlugin(io.noties.markwon.html.HtmlPlugin.create())
|
|
||||||
.usePlugin(io.noties.markwon.linkify.LinkifyPlugin.create())
|
|
||||||
.usePlugin(io.noties.markwon.ext.strikethrough.StrikethroughPlugin.create())
|
|
||||||
.usePlugin(io.noties.markwon.ext.tasklist.TaskListPlugin.create(ctx))
|
|
||||||
.build()
|
|
||||||
|
|
||||||
textView.tag = markwon
|
textView.tag = markwon
|
||||||
textView
|
textView
|
||||||
},
|
},
|
||||||
update = { view ->
|
update = { view ->
|
||||||
try {
|
try {
|
||||||
val markwon = view.tag as? io.noties.markwon.Markwon
|
val currentMarkwon = view.tag as? io.noties.markwon.Markwon
|
||||||
if (markwon != null) {
|
if (currentMarkwon != null) {
|
||||||
markwon.setMarkdown(view, markdown)
|
currentMarkwon.setMarkdown(view, markdown)
|
||||||
} else {
|
} else {
|
||||||
view.text = markdown
|
view.text = markdown
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,19 @@ fun ChatScaffold(
|
||||||
val sessions by viewModel.sessions.collectAsState()
|
val sessions by viewModel.sessions.collectAsState()
|
||||||
val currentSessionId by viewModel.currentSessionId.collectAsState()
|
val currentSessionId by viewModel.currentSessionId.collectAsState()
|
||||||
val messages by viewModel.messages.collectAsState()
|
val messages by viewModel.messages.collectAsState()
|
||||||
|
val streamingContent by viewModel.streamingContent.collectAsState()
|
||||||
var showClearDialog by remember { mutableStateOf(false) }
|
var showClearDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val currentMessages = messages.filter { it.sessionId == currentSessionId }
|
val currentMessages = remember(messages, streamingContent, currentSessionId) {
|
||||||
|
val streaming = streamingContent
|
||||||
|
messages.asSequence()
|
||||||
|
.filter { it.sessionId == currentSessionId }
|
||||||
|
.map { msg ->
|
||||||
|
val live = streaming[msg.id]
|
||||||
|
if (msg.isStreaming && live != null) msg.copy(content = live) else msg
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
// 清除确认对话框
|
// 清除确认对话框
|
||||||
if (showClearDialog) {
|
if (showClearDialog) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.stand.standapp.ui.chat.repo
|
||||||
|
|
||||||
import com.stand.standapp.AppConfig
|
import com.stand.standapp.AppConfig
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.stand.standapp.net.NetworkModule
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
|
@ -15,12 +16,9 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class AiChatRepository {
|
class AiChatRepository {
|
||||||
private val client = OkHttpClient.Builder()
|
private val client: OkHttpClient = NetworkModule.streamingClient()
|
||||||
.readTimeout(0, TimeUnit.MILLISECONDS) // 禁用读取超时以适配 SSE 流
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun streamChat(context: Context, message: String, token: String, historyJson: String): Flow<String> = callbackFlow {
|
fun streamChat(context: Context, message: String, token: String, historyJson: String): Flow<String> = callbackFlow {
|
||||||
// 1. 从 AppConfig 中动态读取当前服务器基准地址,完美适配真实网关
|
// 1. 从 AppConfig 中动态读取当前服务器基准地址,完美适配真实网关
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ object GpioManager {
|
||||||
Timber.tag("GPIO").i("Start GPIO listening thread")
|
Timber.tag("GPIO").i("Start GPIO listening thread")
|
||||||
while (mIsRunning) {
|
while (mIsRunning) {
|
||||||
try {
|
try {
|
||||||
// 硬件轮询频率优化:50ms 既能保证响应速度,又能降低 CPU 消耗
|
// 硬件轮询频率优化:150ms 既能保证响应速度,又能降低 CPU 消耗
|
||||||
Thread.sleep(50)
|
Thread.sleep(150)
|
||||||
val manager = mZysjSystemManager ?: continue
|
val manager = mZysjSystemManager ?: continue
|
||||||
|
|
||||||
// 获取 GPIO 1 的值 (0 按下, 1 松开)
|
// 获取 GPIO 1 的值 (0 按下, 1 松开)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.stand.standapp.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.stand.standapp.AppConfig
|
import com.stand.standapp.AppConfig
|
||||||
|
import com.stand.standapp.net.NetworkModule
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
|
|
@ -17,7 +18,7 @@ import kotlin.system.exitProcess
|
||||||
object LogManager {
|
object LogManager {
|
||||||
private val executor = Executors.newSingleThreadExecutor()
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
private const val MAX_DAYS = 15
|
private const val MAX_DAYS = 15
|
||||||
private val client = OkHttpClient()
|
private val client = NetworkModule.defaultClient
|
||||||
private var logDir: File? = null
|
private var logDir: File? = null
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
|
|
@ -98,19 +99,6 @@ object LogManager {
|
||||||
onCrashDetected(null, thread.name, throwable)
|
onCrashDetected(null, thread.name, throwable)
|
||||||
defaultHandler?.uncaughtException(thread, throwable) ?: exitProcess(1)
|
defaultHandler?.uncaughtException(thread, throwable) ?: exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 捕获主线程 Looper
|
|
||||||
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
android.os.Looper.loop()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
onCrashDetected(null, "MainLooper", e)
|
|
||||||
// 如果是 UnsatisfiedLinkError,这种错误无法恢复,必须抛出让系统处理
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cleanOldLogs(dir: File) {
|
private fun cleanOldLogs(dir: File) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue