Compare commits
19 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
d937ad854d | |
|
|
ea7c977f62 | |
|
|
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');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -524,13 +524,80 @@
|
||||||
var infoStr = window.android.getAboutInfo();
|
var infoStr = window.android.getAboutInfo();
|
||||||
try {
|
try {
|
||||||
var info = JSON.parse(infoStr);
|
var info = JSON.parse(infoStr);
|
||||||
var html = '<strong>应用名称:</strong> ' + info.appName + '<br>' +
|
|
||||||
'<strong>版本号:</strong> <span style="color: #3b82f6;">' + info.versionName + '</span><br>' +
|
var friendlyNames = {
|
||||||
'<strong>设备型号:</strong> ' + info.model + '<br>' +
|
appName: '应用名称',
|
||||||
'<strong>系统版本:</strong> Android ' + info.osVersion + '<br>' +
|
versionName: '版本号',
|
||||||
'<strong>CPU 架构:</strong> ' + info.cpuAbi + '<br>' +
|
model: '设备型号',
|
||||||
'<strong>运行模式:</strong> ' + (info.is64Bit ? "64位" : "32位兼容模式");
|
osVersion: '系统版本',
|
||||||
document.getElementById('aboutBody').innerHTML = html;
|
cpuAbi: 'CPU 架构',
|
||||||
|
is64Bit: '运行模式',
|
||||||
|
screen: '屏幕与显示',
|
||||||
|
width: '宽度像素',
|
||||||
|
height: '高度像素',
|
||||||
|
density: '屏幕密度',
|
||||||
|
densityDpi: '屏幕DPI',
|
||||||
|
physicalSize: '物理尺寸 (英寸)',
|
||||||
|
hardware: '硬件与厂商',
|
||||||
|
manufacturer: '制造商',
|
||||||
|
brand: '品牌',
|
||||||
|
fingerprint: '系统指纹',
|
||||||
|
system: '系统环境',
|
||||||
|
sdkVersion: 'SDK 版本',
|
||||||
|
language: '系统语言',
|
||||||
|
country: '国家代码',
|
||||||
|
securityPatch: '安全补丁',
|
||||||
|
memory: '内存信息',
|
||||||
|
totalRam: '总内存 (RAM)',
|
||||||
|
availRam: '可用内存',
|
||||||
|
storage: '存储信息',
|
||||||
|
totalInternal: '总内部存储',
|
||||||
|
availInternal: '可用内部存储',
|
||||||
|
network: '网络状态',
|
||||||
|
type: '网络类型',
|
||||||
|
ipAddress: 'IP 地址',
|
||||||
|
battery: '电池状态',
|
||||||
|
level: '电量百分比',
|
||||||
|
status: '充电状态',
|
||||||
|
plugged: '供电类型'
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (!bytes || bytes <= 0) return '0 B';
|
||||||
|
var k = 1024;
|
||||||
|
var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
var i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHtml(data, prefix) {
|
||||||
|
var keys = Object.keys(data);
|
||||||
|
var res = '';
|
||||||
|
for (var i = 0; i < keys.length; i++) {
|
||||||
|
var key = keys[i];
|
||||||
|
var val = data[key];
|
||||||
|
var label = friendlyNames[key] || key;
|
||||||
|
var indent = prefix ? 'margin-left: 15px;' : '';
|
||||||
|
|
||||||
|
if (val !== null && typeof val === 'object') {
|
||||||
|
res += '<div style="' + indent + 'margin-top: 8px; margin-bottom: 4px; border-bottom: 1px dashed #eee; padding-bottom: 2px;"><strong>' + label + '</strong></div>';
|
||||||
|
res += buildHtml(val, true);
|
||||||
|
} else {
|
||||||
|
var displayVal = val;
|
||||||
|
if (key === 'is64Bit') {
|
||||||
|
displayVal = val ? '64位' : '32位兼容模式';
|
||||||
|
} else if (key === 'totalRam' || key === 'availRam' || key === 'totalInternal' || key === 'availInternal') {
|
||||||
|
displayVal = formatBytes(val);
|
||||||
|
} else if (key === 'level') {
|
||||||
|
displayVal = val >= 0 ? val + '%' : '未知';
|
||||||
|
}
|
||||||
|
res += '<div style="' + indent + 'line-height: 1.6;"><strong>' + label + ':</strong> ' + displayVal + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('aboutBody').innerHTML = buildHtml(info, false);
|
||||||
document.getElementById('aboutModal').style.display = 'flex';
|
document.getElementById('aboutModal').style.display = 'flex';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
document.getElementById('aboutBody').innerText = "获取信息解析失败: " + infoStr;
|
document.getElementById('aboutBody').innerText = "获取信息解析失败: " + infoStr;
|
||||||
|
|
|
||||||
|
|
@ -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"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -378,22 +378,9 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
fun getAboutInfo(): String {
|
fun getAboutInfo(): String {
|
||||||
return try {
|
return try {
|
||||||
val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
|
val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
|
||||||
val json = JSONObject()
|
val json = com.stand.standapp.utils.DeviceInfoUtils.getDeviceInfoJson(activity)
|
||||||
json.put("appName", activity.getString(R.string.app_name))
|
json.put("appName", activity.getString(R.string.app_name))
|
||||||
json.put("versionName", packageInfo.versionName)
|
json.put("versionName", packageInfo.versionName ?: "1.0.0")
|
||||||
json.put("model", android.os.Build.MODEL)
|
|
||||||
json.put("osVersion", android.os.Build.VERSION.RELEASE)
|
|
||||||
|
|
||||||
val abis = android.os.Build.SUPPORTED_ABIS
|
|
||||||
json.put("cpuAbi", abis.joinToString(","))
|
|
||||||
|
|
||||||
val is64Bit = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
|
||||||
android.os.Process.is64Bit()
|
|
||||||
} else {
|
|
||||||
abis.any { it.contains("64") }
|
|
||||||
}
|
|
||||||
json.put("is64Bit", is64Bit)
|
|
||||||
|
|
||||||
json.toString()
|
json.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("AndroidInterface").e(e, "getAboutInfo failed")
|
Timber.tag("AndroidInterface").e(e, "getAboutInfo failed")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
chunkBuffer.clear()
|
||||||
|
lastFlushAt.clear()
|
||||||
|
|
||||||
_isLoading.value = false // 释放锁,允许用户立刻开始下一次提问!
|
_isLoading.value = false
|
||||||
saveCacheToLocal() // 强制存盘归档
|
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 中动态读取当前服务器基准地址,完美适配真实网关
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
package com.stand.standapp.utils
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.Process
|
||||||
|
import android.os.StatFs
|
||||||
|
import org.json.JSONObject
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.NetworkInterface
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
object DeviceInfoUtils {
|
||||||
|
|
||||||
|
fun getDeviceInfoJson(context: Context): JSONObject {
|
||||||
|
val json = JSONObject()
|
||||||
|
val appContext = context.applicationContext
|
||||||
|
|
||||||
|
// Base App info is typically set at the caller level, but we populate core details
|
||||||
|
try {
|
||||||
|
json.put("model", Build.MODEL)
|
||||||
|
json.put("osVersion", Build.VERSION.RELEASE)
|
||||||
|
|
||||||
|
val abis = Build.SUPPORTED_ABIS
|
||||||
|
json.put("cpuAbi", abis.joinToString(","))
|
||||||
|
|
||||||
|
val is64Bit = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Process.is64Bit()
|
||||||
|
} else {
|
||||||
|
abis.any { it.contains("64") }
|
||||||
|
}
|
||||||
|
json.put("is64Bit", is64Bit)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("DeviceInfoUtils").e(e, "Failed to get base device info")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Screen and display info
|
||||||
|
try {
|
||||||
|
val screenObj = JSONObject()
|
||||||
|
val metrics = appContext.resources.displayMetrics
|
||||||
|
val widthPixels = metrics.widthPixels
|
||||||
|
val heightPixels = metrics.heightPixels
|
||||||
|
val xdpi = metrics.xdpi
|
||||||
|
val ydpi = metrics.ydpi
|
||||||
|
|
||||||
|
screenObj.put("width", widthPixels)
|
||||||
|
screenObj.put("height", heightPixels)
|
||||||
|
screenObj.put("density", metrics.density)
|
||||||
|
screenObj.put("densityDpi", metrics.densityDpi)
|
||||||
|
|
||||||
|
val physicalSize = if (xdpi > 0 && ydpi > 0) {
|
||||||
|
val widthInches = widthPixels / xdpi
|
||||||
|
val heightInches = heightPixels / ydpi
|
||||||
|
Math.sqrt((widthInches * widthInches + heightInches * heightInches).toDouble())
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
screenObj.put("physicalSize", Math.round(physicalSize * 10.0) / 10.0) // 1 decimal place
|
||||||
|
|
||||||
|
json.put("screen", screenObj)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("DeviceInfoUtils").e(e, "Failed to get screen metrics")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Hardware and manufacturer info
|
||||||
|
try {
|
||||||
|
val hwObj = JSONObject()
|
||||||
|
hwObj.put("manufacturer", Build.MANUFACTURER)
|
||||||
|
hwObj.put("brand", Build.BRAND)
|
||||||
|
hwObj.put("hardware", Build.HARDWARE)
|
||||||
|
hwObj.put("fingerprint", Build.FINGERPRINT)
|
||||||
|
json.put("hardware", hwObj)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("DeviceInfoUtils").e(e, "Failed to get hardware info")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. System and Locale environment
|
||||||
|
try {
|
||||||
|
val sysObj = JSONObject()
|
||||||
|
sysObj.put("sdkVersion", Build.VERSION.SDK_INT)
|
||||||
|
sysObj.put("language", Locale.getDefault().language)
|
||||||
|
sysObj.put("country", Locale.getDefault().country)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
sysObj.put("securityPatch", Build.VERSION.SECURITY_PATCH)
|
||||||
|
} else {
|
||||||
|
sysObj.put("securityPatch", "")
|
||||||
|
}
|
||||||
|
json.put("system", sysObj)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("DeviceInfoUtils").e(e, "Failed to get system info")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Memory Info (RAM)
|
||||||
|
try {
|
||||||
|
val memObj = JSONObject()
|
||||||
|
val actManager = appContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||||
|
val memInfo = ActivityManager.MemoryInfo()
|
||||||
|
actManager.getMemoryInfo(memInfo)
|
||||||
|
memObj.put("totalRam", memInfo.totalMem)
|
||||||
|
memObj.put("availRam", memInfo.availMem)
|
||||||
|
json.put("memory", memObj)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("DeviceInfoUtils").e(e, "Failed to get memory info")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Storage Info
|
||||||
|
try {
|
||||||
|
val storageObj = JSONObject()
|
||||||
|
val path = Environment.getDataDirectory()
|
||||||
|
val stat = StatFs(path.path)
|
||||||
|
val blockSize = stat.blockSizeLong
|
||||||
|
val totalBlocks = stat.blockCountLong
|
||||||
|
val availableBlocks = stat.availableBlocksLong
|
||||||
|
|
||||||
|
storageObj.put("totalInternal", totalBlocks * blockSize)
|
||||||
|
storageObj.put("availInternal", availableBlocks * blockSize)
|
||||||
|
json.put("storage", storageObj)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("DeviceInfoUtils").e(e, "Failed to get storage info")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Network state
|
||||||
|
try {
|
||||||
|
val netObj = JSONObject()
|
||||||
|
val connManager = appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
var networkType = "NONE"
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
val activeNetwork = connManager.activeNetwork
|
||||||
|
val capabilities = connManager.getNetworkCapabilities(activeNetwork)
|
||||||
|
if (capabilities != null) {
|
||||||
|
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||||
|
networkType = "WIFI"
|
||||||
|
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||||
|
networkType = "CELLULAR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val activeNetworkInfo = connManager.activeNetworkInfo
|
||||||
|
if (activeNetworkInfo != null && activeNetworkInfo.isConnected) {
|
||||||
|
if (activeNetworkInfo.type == ConnectivityManager.TYPE_WIFI) {
|
||||||
|
networkType = "WIFI"
|
||||||
|
} else if (activeNetworkInfo.type == ConnectivityManager.TYPE_MOBILE) {
|
||||||
|
networkType = "CELLULAR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
netObj.put("type", networkType)
|
||||||
|
|
||||||
|
var ipAddress = ""
|
||||||
|
try {
|
||||||
|
val interfaces = Collections.list(NetworkInterface.getNetworkInterfaces())
|
||||||
|
for (intf in interfaces) {
|
||||||
|
val addrs = Collections.list(intf.inetAddresses)
|
||||||
|
for (addr in addrs) {
|
||||||
|
if (!addr.isLoopbackAddress) {
|
||||||
|
val sAddr = addr.hostAddress ?: continue
|
||||||
|
val isIPv4 = sAddr.indexOf(':') < 0
|
||||||
|
if (isIPv4) {
|
||||||
|
ipAddress = sAddr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ipAddress.isNotEmpty()) break
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("DeviceInfoUtils").w(e, "Failed to resolve IP address")
|
||||||
|
}
|
||||||
|
netObj.put("ipAddress", ipAddress)
|
||||||
|
json.put("network", netObj)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("DeviceInfoUtils").e(e, "Failed to get network status")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Battery status
|
||||||
|
try {
|
||||||
|
val batObj = JSONObject()
|
||||||
|
val batteryStatus: Intent? = appContext.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||||
|
val level = batteryStatus?.getIntExtra(android.os.BatteryManager.EXTRA_LEVEL, -1) ?: -1
|
||||||
|
val scale = batteryStatus?.getIntExtra(android.os.BatteryManager.EXTRA_SCALE, -1) ?: -1
|
||||||
|
val batteryPct = if (level >= 0 && scale > 0) (level * 100 / scale.toFloat()).toInt() else -1
|
||||||
|
batObj.put("level", batteryPct)
|
||||||
|
|
||||||
|
val status = batteryStatus?.getIntExtra(android.os.BatteryManager.EXTRA_STATUS, -1) ?: -1
|
||||||
|
val statusString = when (status) {
|
||||||
|
android.os.BatteryManager.BATTERY_STATUS_CHARGING -> "charging"
|
||||||
|
android.os.BatteryManager.BATTERY_STATUS_DISCHARGING -> "discharging"
|
||||||
|
android.os.BatteryManager.BATTERY_STATUS_FULL -> "full"
|
||||||
|
android.os.BatteryManager.BATTERY_STATUS_NOT_CHARGING -> "not_charging"
|
||||||
|
else -> "unknown"
|
||||||
|
}
|
||||||
|
batObj.put("status", statusString)
|
||||||
|
|
||||||
|
val chargePlug = batteryStatus?.getIntExtra(android.os.BatteryManager.EXTRA_PLUGGED, -1) ?: -1
|
||||||
|
val plugString = when (chargePlug) {
|
||||||
|
android.os.BatteryManager.BATTERY_PLUGGED_AC -> "ac"
|
||||||
|
android.os.BatteryManager.BATTERY_PLUGGED_USB -> "usb"
|
||||||
|
android.os.BatteryManager.BATTERY_PLUGGED_WIRELESS -> "wireless"
|
||||||
|
else -> "none"
|
||||||
|
}
|
||||||
|
batObj.put("plugged", plugString)
|
||||||
|
json.put("battery", batObj)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("DeviceInfoUtils").e(e, "Failed to get battery info")
|
||||||
|
}
|
||||||
|
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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