Compare commits
No commits in common. "main" and "newwebview" have entirely different histories.
main
...
newwebview
|
|
@ -35,4 +35,3 @@ local.properties
|
||||||
/.kotlin/
|
/.kotlin/
|
||||||
/第三方/*
|
/第三方/*
|
||||||
/startopencode
|
/startopencode
|
||||||
/docs/*
|
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ android {
|
||||||
applicationId = "com.stand.standapp"
|
applicationId = "com.stand.standapp"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 13
|
versionCode = 12
|
||||||
versionName = "1.0.12"
|
versionName = "1.0.11"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters.addAll(listOf("armeabi-v7a"))
|
abiFilters.addAll(listOf("armeabi-v7a","x86"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,13 +67,6 @@ dependencies {
|
||||||
implementation("org.mozilla.geckoview:geckoview-omni:115.0.20230710165010")
|
implementation("org.mozilla.geckoview:geckoview-omni:115.0.20230710165010")
|
||||||
implementation("com.google.android.material:material:1.12.0")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
implementation("com.squareup.okhttp3:okhttp-sse:4.12.0")
|
|
||||||
implementation("io.noties.markwon:core:4.6.2")
|
|
||||||
implementation("io.noties.markwon:ext-tables:4.6.2")
|
|
||||||
implementation("io.noties.markwon:html:4.6.2")
|
|
||||||
implementation("io.noties.markwon:linkify:4.6.2") // 👈 链接自动识别
|
|
||||||
implementation("io.noties.markwon:ext-strikethrough:4.6.2") // 👈 删除线支持
|
|
||||||
implementation("io.noties.markwon:ext-tasklist:4.6.2") // 👈 任务列表支持
|
|
||||||
implementation("com.github.getActivity:XXPermissions:18.2")
|
implementation("com.github.getActivity:XXPermissions:18.2")
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
|
|
@ -84,7 +77,6 @@ dependencies {
|
||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
implementation(libs.androidx.compose.material3)
|
implementation(libs.androidx.compose.material3)
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0") // 👈 新增这一行以支持 Compose 中的 viewModel()
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
# GeckoView 混淆规则
|
# GeckoView 混淆规则
|
||||||
-dontwarn org.mozilla.geckoview.**
|
-dontwarn org.mozilla.geckoview.**
|
||||||
-dontwarn org.mozilla.gecko.**
|
-keep class org.mozilla.gecko.util.DebugConfig { *; }
|
||||||
-keep class org.mozilla.gecko.** { *; }
|
|
||||||
-keep class org.mozilla.geckoview.** { *; }
|
|
||||||
|
|
||||||
# AndroidInterface 桥接类保护
|
# AndroidInterface 桥接类保护
|
||||||
-keep class com.stand.standapp.AndroidInterface { *; }
|
-keep class com.stand.standapp.AndroidInterface { *; }
|
||||||
|
|
@ -10,8 +8,6 @@
|
||||||
|
|
||||||
# 忽略不存在的可选依赖报错
|
# 忽略不存在的可选依赖报错
|
||||||
-dontwarn com.google.android.material.snackbar.**
|
-dontwarn com.google.android.material.snackbar.**
|
||||||
-dontwarn java.beans.**
|
|
||||||
-dontwarn org.yaml.snakeyaml.**
|
|
||||||
|
|
||||||
# OkHttp 混淆规则
|
# OkHttp 混淆规则
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
|
|
@ -23,22 +19,5 @@
|
||||||
# A resource is loaded at runtime for config index:
|
# A resource is loaded at runtime for config index:
|
||||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||||
|
|
||||||
# 保护 PTT 原生回调类(native 通过 JNI 反射调用,不能混淆)
|
|
||||||
-keep class com.example.kingway.ptt.** { *; }
|
|
||||||
|
|
||||||
## 保护打印机 SDK(jar 包中的类不能混淆)
|
|
||||||
-keep class com.dp.dp_serialportlist.** { *; }
|
|
||||||
-dontwarn com.dp.dp_serialportlist.**
|
|
||||||
-keep class com.zy.** { *; }
|
|
||||||
-dontwarn com.zy.**
|
|
||||||
#
|
|
||||||
## 保护硬件 GPIO 系统服务(隐藏 API,R8 不认识)
|
|
||||||
-keep class android.app.ZysjSystemManager { *; }
|
|
||||||
-dontwarn android.app.ZysjSystemManager
|
|
||||||
|
|
||||||
# 保护实体类 (如果你后续有 JSON 解析)
|
# 保护实体类 (如果你后续有 JSON 解析)
|
||||||
-keep class com.stand.standapp.** { *; }
|
-keep class com.stand.standapp.** { *; }
|
||||||
-dontwarn java.beans.**
|
|
||||||
|
|
||||||
# R8 missing class 处理
|
|
||||||
-ignorewarnings
|
|
||||||
|
|
|
||||||
|
|
@ -53,14 +53,6 @@
|
||||||
android:label="开发者模式"
|
android:label="开发者模式"
|
||||||
android:theme="@style/Theme.StandAPP" />
|
android:theme="@style/Theme.StandAPP" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".TempCallActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:label="临时呼叫"
|
|
||||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:launchMode="singleTop" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".printer.PrintTestActivity"
|
android:name=".printer.PrintTestActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
E:\SDK\platform-tools\adb.exe exec-out "run-as com.stand.standapp cat /data/data/com.stand.standapp/cfg.dat" > cfg.dat
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
(function() {
|
(function() {
|
||||||
|
// 在页面上下文中注入桥接脚本,避免 cross-compartment 访问被拒
|
||||||
var code = [
|
var code = [
|
||||||
'(function() {',
|
'(function() {',
|
||||||
' var _realAndroid = window.android;',
|
' var _realAndroid = window.android;',
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,10 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"browser_specific_settings": {
|
|
||||||
"gecko": {
|
|
||||||
"id": "standapp-bridge@standapp.local"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "standapp-bridge",
|
"name": "standapp-bridge",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"type": "extension",
|
|
||||||
"description": "JS-Native bridge for StandAPP",
|
"description": "JS-Native bridge for StandAPP",
|
||||||
"content_scripts": [{
|
"content_scripts": [{
|
||||||
"matches": ["<all_urls>"],
|
"matches": ["*://*/*"],
|
||||||
"js": ["bridge.js"],
|
"js": ["bridge.js"],
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"all_frames": true
|
"all_frames": true
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 135 KiB |
|
|
@ -92,13 +92,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 20px;">
|
<div style="margin-bottom: 20px;">
|
||||||
<div style="font-size: 13px; font-weight: bold; color: #64748b; margin-bottom: 8px;">高级业务样式与测试</div>
|
<div style="font-size: 13px; font-weight: bold; color: #64748b; margin-bottom: 8px;">高级业务样式</div>
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
||||||
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintColumns()">分栏打印测试</button>
|
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintColumns()">分栏打印测试</button>
|
||||||
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintAlarm()">警情通知单</button>
|
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintAlarm()">警情通知单</button>
|
||||||
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintDemo(false)">2寸演示单据</button>
|
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintDemo(false)">2寸演示单据</button>
|
||||||
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintDemo(true)">3寸演示单据</button>
|
<button class="btn btn-orange" style="margin:0; padding:10px; font-size:13px;" onclick="callNativePrintDemo(true)">3寸演示单据</button>
|
||||||
<button class="btn" style="margin:0; padding:10px; font-size:13px; background: #0ea5e9; grid-column: span 2;" onclick="testES6Features()">ES6 引擎特性测试</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -112,22 +111,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-bottom: 20px;">
|
|
||||||
<div style="font-size: 13px; font-weight: bold; color: #64748b; margin-bottom: 8px;">App 弹窗交互</div>
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
|
||||||
<button class="btn" style="margin:0; padding:10px; font-size:13px; background: #e11d48;" onclick="triggerRestartDialog()">触发重启提示</button>
|
|
||||||
<button class="btn" style="margin:0; padding:10px; font-size:13px; background: #0d9488;" onclick="triggerJsonDialog()">发送 JSON 弹窗</button>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 8px;">
|
|
||||||
<span class="ptt-label">提示标题:</span>
|
|
||||||
<input type="text" id="dialog-title" class="ws-input" value="系统通知" style="margin-bottom: 5px;">
|
|
||||||
<span class="ptt-label">提示内容:</span>
|
|
||||||
<input type="text" id="dialog-msg" class="ws-input" value="检测到配置更新,请重启应用以生效。">
|
|
||||||
<span class="ptt-label">JSON 数据:</span>
|
|
||||||
<textarea id="dialog-json" class="ws-input" rows="3" style="resize:vertical;">{"type":"alert","level":"warning","message":"测试消息","timestamp":1778505194}</textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
|
||||||
<button class="btn" style="background: #475569;" onclick="callSystemPrint()">1. 调用系统标准打印 (A4)</button>
|
<button class="btn" style="background: #475569;" onclick="callSystemPrint()">1. 调用系统标准打印 (A4)</button>
|
||||||
|
|
@ -181,29 +164,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 临时呼叫状态区域 -->
|
|
||||||
<div id="temp-call-status" style="display: none; margin-top: 10px; padding: 10px; background: #fef3c7; border-radius: 8px; border: 1px solid #f59e0b;">
|
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<span style="font-size: 20px;">📞</span>
|
|
||||||
<div>
|
|
||||||
<div style="font-weight: bold; color: #92400e; font-size: 14px;">临时呼叫进行中</div>
|
|
||||||
<div id="temp-call-group-name" style="font-size: 12px; color: #b45309;">临时群组</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button id="btn-maximize-temp-call" class="btn"
|
|
||||||
style="margin: 0; padding: 8px 16px; font-size: 12px; background: #f59e0b; min-width: auto;"
|
|
||||||
onclick="maximizeTempCall()">
|
|
||||||
最大化
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 8px; font-size: 13px;">
|
|
||||||
当前群组: <span id="ptt-current-group" style="color: #6366f1; font-weight: bold;">-</span>
|
|
||||||
<span id="ptt-current-group-id" style="color: #94a3b8; font-size: 11px; margin-left: 5px;"></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ws-input-group" style="margin-top: 10px;">
|
<div class="ws-input-group" style="margin-top: 10px;">
|
||||||
<div style="display: flex; gap: 8px;">
|
<div style="display: flex; gap: 8px;">
|
||||||
<div style="flex: 1;">
|
<div style="flex: 1;">
|
||||||
|
|
@ -233,7 +193,6 @@
|
||||||
<button class="btn ptt-btn ptt-action-btn" style="background: #6366f1;" onclick="pttGetGroups()" disabled>获取群组</button>
|
<button class="btn ptt-btn ptt-action-btn" style="background: #6366f1;" onclick="pttGetGroups()" disabled>获取群组</button>
|
||||||
<button class="btn ptt-btn ptt-action-btn" style="background: #8b5cf6;" onclick="pttGetMembers()" disabled>获取成员</button>
|
<button class="btn ptt-btn ptt-action-btn" style="background: #8b5cf6;" onclick="pttGetMembers()" disabled>获取成员</button>
|
||||||
<button class="btn ptt-btn ptt-action-btn" style="background: #3b82f6;" onclick="pttJumpGroup()" disabled>切换群组</button>
|
<button class="btn ptt-btn ptt-action-btn" style="background: #3b82f6;" onclick="pttJumpGroup()" disabled>切换群组</button>
|
||||||
<button class="btn ptt-btn ptt-action-btn" style="background: #0ea5e9;" onclick="pttGetCurrentGroup()">当前群组</button>
|
|
||||||
<button class="btn ptt-btn ptt-action-btn btn-orange" onclick="pttForceRelease()" disabled>强制释放</button>
|
<button class="btn ptt-btn ptt-action-btn btn-orange" onclick="pttForceRelease()" disabled>强制释放</button>
|
||||||
<button class="btn ptt-btn ptt-action-btn" style="background: #000; color: #fca5a5;" onclick="pttSimulateConflict()" disabled>模拟冲突</button>
|
<button class="btn ptt-btn ptt-action-btn" style="background: #000; color: #fca5a5;" onclick="pttSimulateConflict()" disabled>模拟冲突</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -297,15 +256,6 @@
|
||||||
updatePrinterStatus();
|
updatePrinterStatus();
|
||||||
// 每 3 秒同步一次打印机状态
|
// 每 3 秒同步一次打印机状态
|
||||||
setInterval(updatePrinterStatus, 3000);
|
setInterval(updatePrinterStatus, 3000);
|
||||||
// 确认接口可用后再加载历史
|
|
||||||
loadHistory();
|
|
||||||
// 通知 Android 页面加载完成
|
|
||||||
if (window.android.onPageFinished) {
|
|
||||||
window.android.onPageFinished();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// bridge 尚未注入,200ms 后重试
|
|
||||||
setTimeout(checkApp, 200);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -433,31 +383,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function triggerRestartDialog() {
|
|
||||||
if (window.android && window.android.showRestartDialog) {
|
|
||||||
var msg = document.getElementById('dialog-msg').value;
|
|
||||||
var title = document.getElementById('dialog-title').value;
|
|
||||||
window.android.showRestartDialog(msg, title);
|
|
||||||
} else {
|
|
||||||
alert("showRestartDialog 接口不可用");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerJsonDialog() {
|
|
||||||
if (window.android && window.android.showJsonDialog) {
|
|
||||||
var json = document.getElementById('dialog-json').value;
|
|
||||||
try {
|
|
||||||
JSON.parse(json);
|
|
||||||
} catch(e) {
|
|
||||||
alert("JSON 格式错误: " + e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.android.showJsonDialog(json);
|
|
||||||
} else {
|
|
||||||
alert("showJsonDialog 接口不可用");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用系统打印
|
// 调用系统打印
|
||||||
function callSystemPrint() {
|
function callSystemPrint() {
|
||||||
if (window.android && window.android.printPage) {
|
if (window.android && window.android.printPage) {
|
||||||
|
|
@ -493,19 +418,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传日志
|
// 上传日志
|
||||||
var isUploading = false;
|
|
||||||
function callUploadLogs() {
|
function callUploadLogs() {
|
||||||
if (isUploading) {
|
|
||||||
alert("正在上传中,请耐心等待(30秒内请勿重复点击)...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (window.android && window.android.uploadLogs) {
|
if (window.android && window.android.uploadLogs) {
|
||||||
isUploading = true;
|
|
||||||
alert("开始上传日志,请耐心等待...");
|
|
||||||
window.android.uploadLogs();
|
window.android.uploadLogs();
|
||||||
setTimeout(function() {
|
|
||||||
isUploading = false;
|
|
||||||
}, 30000);
|
|
||||||
} else {
|
} else {
|
||||||
alert("上传日志接口不可用");
|
alert("上传日志接口不可用");
|
||||||
}
|
}
|
||||||
|
|
@ -545,77 +460,7 @@
|
||||||
document.getElementById('aboutModal').style.display = 'none';
|
document.getElementById('aboutModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打印机查纸回调
|
// 安卓回调函数:退出确认时执行
|
||||||
window.onPaperStateChanged = function(hasPaper) {
|
|
||||||
if (hasPaper) {
|
|
||||||
alert("打印机状态正常:当前有纸");
|
|
||||||
} else {
|
|
||||||
alert("⚠️ 打印机缺纸!请装入纸卷!");
|
|
||||||
}
|
|
||||||
updatePrinterStatus();
|
|
||||||
};
|
|
||||||
|
|
||||||
// PTT 各种事件回调
|
|
||||||
window.onPttEvent = function(eventType, payload) {
|
|
||||||
console.log("收到 PTT 事件: ", eventType, payload);
|
|
||||||
var msg = "";
|
|
||||||
switch(eventType) {
|
|
||||||
case 'GroupList':
|
|
||||||
msg = "获取群组成功,共 " + payload.length + " 个群组。第一组: " + (payload[0] ? payload[0].groupName : "无");
|
|
||||||
break;
|
|
||||||
case 'MemberList':
|
|
||||||
msg = "获取群成员成功,共 " + payload.length + " 个成员。";
|
|
||||||
break;
|
|
||||||
case 'SpeakerUpdate':
|
|
||||||
msg = "当前讲话人: " + payload.talkName + " (组:" + payload.groupName + ")";
|
|
||||||
break;
|
|
||||||
case 'PlayStatus':
|
|
||||||
msg = (payload.playStatus === 1 ? "开始播放:" : "结束播放:") + payload.talkName;
|
|
||||||
break;
|
|
||||||
case 'TempCall':
|
|
||||||
msg = "临时呼叫消息: " + payload.msg;
|
|
||||||
break;
|
|
||||||
case 'UpdateGroup':
|
|
||||||
msg = "进入群组: " + payload.groupName;
|
|
||||||
break;
|
|
||||||
case 'Alarm':
|
|
||||||
msg = "⚠️收到报警: " + payload.memberName + " (纬度:" + payload.latitude + ")";
|
|
||||||
break;
|
|
||||||
case 'StopAlarm':
|
|
||||||
msg = "群成员停止告警";
|
|
||||||
break;
|
|
||||||
case 'SpeakResult':
|
|
||||||
msg = payload.success ? "话权获取成功" : "话权获取失败";
|
|
||||||
break;
|
|
||||||
case 'SpeakEnd':
|
|
||||||
msg = payload.success ? "话权释放成功" : "话权释放失败";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
msg = "收到事件: " + eventType;
|
|
||||||
}
|
|
||||||
showPttResult("[回调] " + msg);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ES6 特性测试函数(通过 eval 避免阻塞旧版 JS 引擎整体解析)
|
|
||||||
function testES6Features() {
|
|
||||||
try {
|
|
||||||
var code = [
|
|
||||||
"const arr = [1, 2, 3];",
|
|
||||||
"const [a, ...rest] = arr;",
|
|
||||||
"let sum = rest.reduce((acc, val) => acc + val, 0);",
|
|
||||||
"const myMap = new Map();",
|
|
||||||
"myMap.set('key', `Sum is ${sum}`);",
|
|
||||||
"new Promise((resolve) => {",
|
|
||||||
" setTimeout(() => resolve(myMap.get('key')), 100);",
|
|
||||||
"}).then(res => {",
|
|
||||||
" alert(`ES6 引擎测试成功!\\n解构: a=${a}\\nPromise/Map 返回: ${res}`);",
|
|
||||||
"});"
|
|
||||||
].join("");
|
|
||||||
eval(code);
|
|
||||||
} catch(e) {
|
|
||||||
alert("ES6 测试失败 (当前环境可能不支持): " + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function addExitText() {
|
function addExitText() {
|
||||||
var tag = document.getElementById('inject-status');
|
var tag = document.getElementById('inject-status');
|
||||||
if (tag) {
|
if (tag) {
|
||||||
|
|
@ -635,86 +480,15 @@
|
||||||
var loginId = window.android.pttGetLoginId ? window.android.pttGetLoginId() : "";
|
var loginId = window.android.pttGetLoginId ? window.android.pttGetLoginId() : "";
|
||||||
updatePttLoginStatus(status, loginId);
|
updatePttLoginStatus(status, loginId);
|
||||||
}
|
}
|
||||||
if (window.android && window.android.pttGetCurrentGroup) {
|
|
||||||
try {
|
|
||||||
var group = JSON.parse(window.android.pttGetCurrentGroup());
|
|
||||||
if (group.groupId) {
|
|
||||||
updatePttCurrentGroup(group.groupId, group.groupName);
|
|
||||||
}
|
|
||||||
} catch(e) {}
|
|
||||||
}
|
|
||||||
// 同步临时呼叫状态
|
|
||||||
syncTempCallStatus();
|
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// === 临时呼叫状态管理 ===
|
// 告知 Android 页面加载完成
|
||||||
function syncTempCallStatus() {
|
window.onload = function() {
|
||||||
if (window.android && window.android.getTempCallStatus) {
|
if (window.android && window.android.onPageFinished) {
|
||||||
try {
|
window.android.onPageFinished();
|
||||||
var status = JSON.parse(window.android.getTempCallStatus());
|
|
||||||
updateTempCallUI(status.isOpen, status.isMinimized);
|
|
||||||
} catch(e) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTempCallUI(isOpen, isMinimized) {
|
|
||||||
var statusDiv = document.getElementById('temp-call-status');
|
|
||||||
var maxBtn = document.getElementById('btn-maximize-temp-call');
|
|
||||||
var groupSpan = document.getElementById('temp-call-group-name');
|
|
||||||
|
|
||||||
// 只有在最小化状态时才显示最大化按钮
|
|
||||||
if (isMinimized) {
|
|
||||||
statusDiv.style.display = 'block';
|
|
||||||
maxBtn.style.display = 'inline-block';
|
|
||||||
maxBtn.disabled = false;
|
|
||||||
maxBtn.style.background = '#f59e0b';
|
|
||||||
maxBtn.style.cursor = 'pointer';
|
|
||||||
} else if (isOpen) {
|
|
||||||
// Activity 打开但未最小化,不显示状态区域
|
|
||||||
statusDiv.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
// Activity 已销毁且未最小化
|
|
||||||
statusDiv.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function maximizeTempCall() {
|
|
||||||
if (window.android && window.android.maximizeTempCall) {
|
|
||||||
window.android.maximizeTempCall();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 临时呼叫状态变化回调 (由 Android 原生调用)
|
|
||||||
window.onTempCallStateChanged = function(state) {
|
|
||||||
console.log("临时呼叫状态变化: " + state);
|
|
||||||
var statusDiv = document.getElementById('temp-call-status');
|
|
||||||
var maxBtn = document.getElementById('btn-maximize-temp-call');
|
|
||||||
|
|
||||||
switch(state) {
|
|
||||||
case 'minimized':
|
|
||||||
// 最小化:显示状态区域和最大化按钮
|
|
||||||
statusDiv.style.display = 'block';
|
|
||||||
maxBtn.style.display = 'inline-block';
|
|
||||||
maxBtn.disabled = false;
|
|
||||||
maxBtn.style.background = '#f59e0b';
|
|
||||||
showPttResult("[回调] 临时呼叫已最小化,可通过最大化按钮恢复");
|
|
||||||
break;
|
|
||||||
case 'resumed':
|
|
||||||
// 恢复:隐藏状态区域
|
|
||||||
statusDiv.style.display = 'none';
|
|
||||||
showPttResult("[回调] 临时呼叫页面已恢复");
|
|
||||||
break;
|
|
||||||
case 'destroyed':
|
|
||||||
// 销毁:隐藏所有
|
|
||||||
statusDiv.style.display = 'none';
|
|
||||||
showPttResult("[回调] 临时呼叫已结束");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 告知 Android 页面加载完成(已移至 checkApp 中,等 bridge 注入后再调用)
|
|
||||||
window.onload = function() {};
|
|
||||||
|
|
||||||
// === WebSocket 逻辑 ===
|
// === WebSocket 逻辑 ===
|
||||||
var ws = null;
|
var ws = null;
|
||||||
var clientId = "WEB_" + Math.random().toString(36).substr(2, 9);
|
var clientId = "WEB_" + Math.random().toString(36).substr(2, 9);
|
||||||
|
|
@ -864,10 +638,11 @@
|
||||||
addLog("系统: 终端暂无保存的记录", false);
|
addLog("系统: 终端暂无保存的记录", false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 不再弹窗,checkApp 会在 bridge 就绪后调用 loadHistory
|
alert("未检测到安卓接口,无法同步");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(loadHistory, 500);
|
||||||
|
|
||||||
// === PTT 业务测试逻辑 ===
|
// === PTT 业务测试逻辑 ===
|
||||||
var pttBtn = document.getElementById('ptt-speak-btn');
|
var pttBtn = document.getElementById('ptt-speak-btn');
|
||||||
|
|
@ -901,13 +676,6 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新当前群组信息 (由 Android 实时推送)
|
|
||||||
function updatePttCurrentGroup(groupId, groupName) {
|
|
||||||
document.getElementById('ptt-current-group').innerText = groupName || '-';
|
|
||||||
document.getElementById('ptt-current-group-id').innerText = groupId ? '(' + groupId + ')' : '';
|
|
||||||
showPttResult("进入群组: " + groupName + " (" + groupId + ")");
|
|
||||||
};
|
|
||||||
|
|
||||||
// PTT 登录成功回调 (由 Android 调用)
|
// PTT 登录成功回调 (由 Android 调用)
|
||||||
function onPttLoginSuccess() {
|
function onPttLoginSuccess() {
|
||||||
updatePttLoginStatusUI("02");
|
updatePttLoginStatusUI("02");
|
||||||
|
|
@ -1032,21 +800,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pttGetCurrentGroup() {
|
|
||||||
if (window.android && window.android.pttGetCurrentGroup) {
|
|
||||||
try {
|
|
||||||
var group = JSON.parse(window.android.pttGetCurrentGroup());
|
|
||||||
if (group.groupId) {
|
|
||||||
updatePttCurrentGroup(group.groupId, group.groupName);
|
|
||||||
} else {
|
|
||||||
showPttResult("当前无群组信息");
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
showPttResult("获取群组信息失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pttSendAtCmd() {
|
function pttSendAtCmd() {
|
||||||
var cmd = document.getElementById('ptt-at-cmd').value.trim();
|
var cmd = document.getElementById('ptt-at-cmd').value.trim();
|
||||||
if (!cmd) {
|
if (!cmd) {
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,13 @@ package com.example.kingway.ptt;
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioRecord;
|
import android.media.AudioRecord;
|
||||||
import android.media.MediaRecorder;
|
import android.media.MediaRecorder;
|
||||||
import timber.log.Timber;
|
import android.util.Log;
|
||||||
|
import java.util.Arrays;
|
||||||
import com.stand.standapp.MyApplication;
|
|
||||||
|
|
||||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
public class AudioRecordManager {
|
public class AudioRecordManager {
|
||||||
private final String TAG = "AudioRecordManager";
|
private final String TAG = "AudioRecordManager";
|
||||||
private static final int FREQUENCY = 8000; //16K采集率
|
private static final int FREQUENCY = 8000;
|
||||||
private AudioRecord mRecorder;
|
private AudioRecord mRecorder;
|
||||||
private Thread recordThread;
|
private Thread recordThread;
|
||||||
private boolean isStart = false;
|
private boolean isStart = false;
|
||||||
|
|
@ -23,24 +22,20 @@ public class AudioRecordManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initAudio() {
|
public void initAudio() {
|
||||||
//一、实例化一个AudioRecord类
|
|
||||||
int audioSource = MediaRecorder.AudioSource.MIC;
|
int audioSource = MediaRecorder.AudioSource.MIC;
|
||||||
mRecorder = new AudioRecord(audioSource,
|
mRecorder = new AudioRecord(audioSource,
|
||||||
FREQUENCY,
|
FREQUENCY,
|
||||||
AudioFormat.CHANNEL_IN_MONO,
|
AudioFormat.CHANNEL_IN_MONO,
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
bufferSize * 2);
|
bufferSize * 2);
|
||||||
Timber.tag(TAG).d("AudioRecord初始化: source=MIC, freq=%dHz, minBuf=%d", FREQUENCY, bufferSize * 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRecording(){
|
public void setRecording(){
|
||||||
Timber.tag(TAG).d("setRecording -> startRecording()");
|
|
||||||
if (mRecorder == null) initAudio();
|
if (mRecorder == null) initAudio();
|
||||||
if(mRecorder.getRecordingState() != 3) mRecorder.startRecording();
|
if(mRecorder.getRecordingState() != 3) mRecorder.startRecording();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopAudio(){
|
public void stopAudio(){
|
||||||
Timber.tag(TAG).d("stopAudio -> stop()");
|
|
||||||
if (mRecorder != null){
|
if (mRecorder != null){
|
||||||
mRecorder.stop();
|
mRecorder.stop();
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +47,6 @@ public class AudioRecordManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void destroyAudio(){
|
public void destroyAudio(){
|
||||||
Timber.tag(TAG).d("destroyAudio -> 释放AudioRecord");
|
|
||||||
if (mRecorder != null){
|
if (mRecorder != null){
|
||||||
mRecorder.stop();
|
mRecorder.stop();
|
||||||
mRecorder.release();
|
mRecorder.release();
|
||||||
|
|
@ -84,13 +78,12 @@ public class AudioRecordManager {
|
||||||
try {
|
try {
|
||||||
recordThread.interrupt();
|
recordThread.interrupt();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "录音线程interrupt失败");
|
|
||||||
recordThread = null;
|
recordThread = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recordThread = null;
|
recordThread = null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "destroyThread异常");
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
recordThread = null;
|
recordThread = null;
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +93,6 @@ public class AudioRecordManager {
|
||||||
* 启动录音线程
|
* 启动录音线程
|
||||||
*/
|
*/
|
||||||
private void startThread() {
|
private void startThread() {
|
||||||
Timber.tag(TAG).d("启动录音线程");
|
|
||||||
destroyThread();
|
destroyThread();
|
||||||
isStart = true;
|
isStart = true;
|
||||||
if (mRecorder == null) {
|
if (mRecorder == null) {
|
||||||
|
|
@ -123,13 +115,10 @@ public class AudioRecordManager {
|
||||||
int bytesRecord;
|
int bytesRecord;
|
||||||
byte[] tempBuffer = new byte[bufferSize];
|
byte[] tempBuffer = new byte[bufferSize];
|
||||||
if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
|
if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
|
||||||
Timber.tag(TAG).w("AudioRecord未初始化(state=%d),终止录音", mRecorder.getState());
|
|
||||||
stopRecord();
|
stopRecord();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mRecorder.startRecording();
|
mRecorder.startRecording();
|
||||||
Timber.tag(TAG).d("录音线程: startRecording() 成功");
|
|
||||||
int count = 0;
|
|
||||||
while (isStart) {
|
while (isStart) {
|
||||||
if (null != mRecorder) {
|
if (null != mRecorder) {
|
||||||
bytesRecord = mRecorder.read(tempBuffer, 0, bufferSize);
|
bytesRecord = mRecorder.read(tempBuffer, 0, bufferSize);
|
||||||
|
|
@ -138,16 +127,15 @@ public class AudioRecordManager {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (bytesRecord != 0 && bytesRecord != -1) {
|
if (bytesRecord != 0 && bytesRecord != -1) {
|
||||||
Timber.tag(TAG).v("录音帧: %d bytes", bytesRecord);
|
Log.i(TAG, "tempBuffer->" + Arrays.toString(tempBuffer));
|
||||||
MyApplication.getAppInstance().getpNative().pttNativeSendRecordCmd(tempBuffer);
|
MyApplication.getAppInstance().getpNative().pttNativeSendRecordCmd(tempBuffer);
|
||||||
} else {
|
} else {
|
||||||
Timber.tag(TAG).w("录音循环退出: bytesRecord=%d", bytesRecord);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "录音线程异常");
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,13 +145,11 @@ public class AudioRecordManager {
|
||||||
* 启动录音
|
* 启动录音
|
||||||
*/
|
*/
|
||||||
public void startRecord() {
|
public void startRecord() {
|
||||||
Timber.tag(TAG).d("startRecord() 被调用");
|
|
||||||
try {
|
try {
|
||||||
stopRecord();
|
stopRecord();
|
||||||
|
|
||||||
startThread();
|
startThread();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "startRecord异常");
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,8 +157,8 @@ public class AudioRecordManager {
|
||||||
* 停止录音
|
* 停止录音
|
||||||
*/
|
*/
|
||||||
public void stopRecord() {
|
public void stopRecord() {
|
||||||
Timber.tag(TAG).d("stopRecord() 被调用");
|
|
||||||
try {
|
try {
|
||||||
|
isStart = false;
|
||||||
destroyThread();
|
destroyThread();
|
||||||
if (mRecorder != null) {
|
if (mRecorder != null) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -181,12 +167,11 @@ public class AudioRecordManager {
|
||||||
}
|
}
|
||||||
mRecorder.release();
|
mRecorder.release();
|
||||||
mRecorder = null;
|
mRecorder = null;
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "释放AudioRecord失败"); }
|
} catch (Exception e) { }
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "stopRecord异常");
|
e.printStackTrace();
|
||||||
} finally {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,8 @@
|
||||||
package com.example.kingway.ptt;
|
package com.example.kingway.ptt;
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
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.Executors;
|
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**解析接收到的AT指令*/
|
/**解析接收到的AT指令*/
|
||||||
public class CallBackResolution {
|
public class CallBackResolution {
|
||||||
|
|
@ -42,14 +34,6 @@ public class CallBackResolution {
|
||||||
pttLoginId = "";
|
pttLoginId = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getCurrentGroupId() {
|
|
||||||
return indexGroupId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getCurrentGroupName() {
|
|
||||||
return indexGroupName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void at_OEM_AT_cb(List<String> lisHex){
|
public static void at_OEM_AT_cb(List<String> lisHex){
|
||||||
if (lisHex.size() == 0) {
|
if (lisHex.size() == 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -57,34 +41,30 @@ public class CallBackResolution {
|
||||||
|
|
||||||
String ngx = "\\u";
|
String ngx = "\\u";
|
||||||
try {
|
try {
|
||||||
Timber.tag(TAG).i( "lisHex-->" + lisHex.toString());
|
Timber.tag(TAG).i("lisHex-->" + lisHex.toString());
|
||||||
String firstHex = lisHex.get(0);
|
String firstHex = lisHex.get(0);
|
||||||
String errorNum = lisHex.get(1);
|
String errorNum = lisHex.get(1);
|
||||||
Timber.tag(TAG).d( "at_OEM_AT_cb firstHex: " + firstHex + ", errorNum: " + errorNum);
|
Timber.tag(TAG).d("at_OEM_AT_cb firstHex: " + firstHex + ", errorNum: " + errorNum);
|
||||||
switch (firstHex){
|
switch (firstHex){
|
||||||
case "82"://登录状态通知指令
|
case "82"://登录状态通知指令
|
||||||
String loginId = lisHex.get(2) + "" + lisHex.get(3) + "" + lisHex.get(4) + "" + lisHex.get(5);
|
String loginId = lisHex.get(2) + "" + lisHex.get(3) + "" + lisHex.get(4) + "" + lisHex.get(5);
|
||||||
pttLoginStatus = errorNum;
|
pttLoginStatus = errorNum;
|
||||||
pttLoginId = loginId;
|
pttLoginId = loginId;
|
||||||
Timber.tag(TAG).d("PTT login status: "+pttLoginStatus+", id: "+pttLoginId);
|
Timber.tag(TAG).i("PTT login status: %s, id: %s", pttLoginStatus, pttLoginId);
|
||||||
// 实时推送状态到前端
|
// 实时推送状态到前端
|
||||||
|
com.stand.standapp.MainActivity.executeJs("updatePttLoginStatus('" + pttLoginStatus + "','" + pttLoginId + "')");
|
||||||
CallBackUtil.callBackLoinState(errorNum, loginId);
|
CallBackUtil.callBackLoinState(errorNum, loginId);
|
||||||
loginState = isLoginConflict(errorNum);
|
loginState = isLoginConflict(errorNum);
|
||||||
CallBackUtil.callBackLoginConflict(loginState);
|
CallBackUtil.callBackLoginConflict(loginState);
|
||||||
if (loginState) {
|
if (loginState) {
|
||||||
Timber.tag(TAG).w("Login conflict detected, showing conflict dialog");
|
|
||||||
com.stand.standapp.MainActivity.executeJs("updatePttLoginStatus('" + pttLoginStatus + "','" + pttLoginId + "')");
|
|
||||||
com.stand.standapp.MainActivity.showPttConflictDialog();
|
com.stand.standapp.MainActivity.showPttConflictDialog();
|
||||||
}
|
}
|
||||||
if ("00".equals(errorNum)) {
|
|
||||||
com.stand.standapp.MainActivity.executeJs("updatePttLoginStatus('" + pttLoginStatus + "','" + pttLoginId + "')");
|
|
||||||
}
|
|
||||||
if ("02".equals(errorNum)) {
|
if ("02".equals(errorNum)) {
|
||||||
com.stand.standapp.MainActivity.onPttLoginSuccess();
|
com.stand.standapp.MainActivity.onPttLoginSuccess();
|
||||||
com.stand.standapp.MainActivity.executeJs("updatePttLoginStatus('" + pttLoginStatus + "','" + pttLoginId + "')");
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case "a2"://账号有视频功能
|
case "a2"://账号有视频功能
|
||||||
CallBackUtil.callBackHaveVideo();
|
CallBackUtil.callBackHaveVideo();
|
||||||
break;
|
break;
|
||||||
|
|
@ -101,18 +81,18 @@ public class CallBackResolution {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "0b"://发起讲话返回状态
|
case "0b"://发起讲话返回状态
|
||||||
Timber.tag(TAG).d( "at_OEM_AT_cb ptt speak...");
|
Timber.tag(TAG).d("at_OEM_AT_cb ptt speak...");
|
||||||
CallBackUtil.callBackSpeakPlaySuccess(errorNum.equals("00")? true : false);
|
CallBackUtil.callBackSpeakPlaySuccess(errorNum.equals("00")? true : false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "0c"://结束讲话返回状态
|
case "0c"://结束讲话返回状态
|
||||||
Timber.tag(TAG).d( "at_OEM_AT_cb speak end...");
|
Timber.tag(TAG).d("at_OEM_AT_cb speak end...");
|
||||||
CallBackUtil.callBackSpeakReleaseSuccess(errorNum.equals("00")? true : false);
|
CallBackUtil.callBackSpeakReleaseSuccess(errorNum.equals("00")? true : false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "80"://返回群组信息
|
case "80"://返回群组信息
|
||||||
Timber.tag(TAG).d( "at_OEM_AT_cb get group info: " + lisHex);
|
Timber.tag(TAG).d("at_OEM_AT_cb get group info: " + lisHex);
|
||||||
StringBuilder strGroup = new StringBuilder();
|
String strGroup = "";
|
||||||
String groupId = lisHex.get(6) + "" + lisHex.get(7) + "" + lisHex.get(8) + "" + lisHex.get(9);
|
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);
|
int groupNo = Integer.valueOf((lisHex.get(4) + "" + lisHex.get(5)), 16);
|
||||||
int groupCount = Integer.valueOf((lisHex.get(10) + "" + lisHex.get(11)), 16);
|
int groupCount = Integer.valueOf((lisHex.get(10) + "" + lisHex.get(11)), 16);
|
||||||
|
|
@ -123,20 +103,20 @@ public class CallBackResolution {
|
||||||
size = (size / 2) - 1;
|
size = (size / 2) - 1;
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
strGroup.append(ngx).append(lisHex.get(i + numB)).append(lisHex.get(i + numB - 1));
|
strGroup += ngx + lisHex.get(i + numB) + "" + lisHex.get(i + numB - 1);
|
||||||
} else {
|
} else {
|
||||||
strGroup.append(ngx).append(lisHex.get(i + numB + 1)).append(lisHex.get(i + numB));
|
strGroup += ngx + lisHex.get(i + numB + 1) + "" + lisHex.get(i + numB);
|
||||||
numB = numB + 1;
|
numB = numB + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "Decode hex failed");
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
String tempTxt = "";
|
String tempTxt = "";
|
||||||
tempTxt = unicodeToString(strGroup.toString());
|
tempTxt = unicodeToString(strGroup);
|
||||||
Timber.tag(TAG).i( " 组名称 " + tempTxt);
|
Timber.tag(TAG).i(" 组名称 " + tempTxt);
|
||||||
CallBackUtil.callBackGroupInfo(groupId, tempTxt, groupNo, groupCount);
|
CallBackUtil.callBackGroupInfo(groupId, tempTxt, groupNo, groupCount);
|
||||||
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.add(groupInfo);
|
||||||
|
|
@ -144,7 +124,7 @@ public class CallBackResolution {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "81"://返回群成员信息
|
case "81"://返回群成员信息
|
||||||
StringBuilder groupMemberName = new StringBuilder();
|
String groupMemberName = "";
|
||||||
String status = lisHex.get(1);
|
String status = lisHex.get(1);
|
||||||
boolean haveVideo = lisHex.get(10).equals("01") ? true : false;
|
boolean haveVideo = lisHex.get(10).equals("01") ? true : false;
|
||||||
String memberId = lisHex.get(6) + "" + lisHex.get(7) + "" + lisHex.get(8) + "" + lisHex.get(9);
|
String memberId = lisHex.get(6) + "" + lisHex.get(7) + "" + lisHex.get(8) + "" + lisHex.get(9);
|
||||||
|
|
@ -156,19 +136,21 @@ public class CallBackResolution {
|
||||||
size = (size / 2) - 1;
|
size = (size / 2) - 1;
|
||||||
for (int i = 0; i < size; i++) {
|
for (int i = 0; i < size; i++) {
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
groupMemberName.append(ngx).append(lisHex.get(i + numB)).append(lisHex.get(i + numB - 1));
|
groupMemberName += ngx + lisHex.get(i + numB) + "" + lisHex.get(i + numB - 1);
|
||||||
} else {
|
} else {
|
||||||
groupMemberName.append(ngx).append(lisHex.get(i + numB + 1)).append(lisHex.get(i + numB));
|
groupMemberName += ngx + lisHex.get(i + numB + 1) + "" + lisHex.get(i + numB);
|
||||||
numB = numB + 1;
|
numB = numB + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "Decode hex failed");
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
String tempTxt = unicodeToString(groupMemberName.toString());
|
String tempTxt = unicodeToString(groupMemberName);
|
||||||
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.add(infoDto);
|
||||||
|
//输出群成员信息memberInfoDtos
|
||||||
|
Timber.tag(TAG).d("at_OEM_AT_cb group member info: " + infoDto);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -199,14 +181,13 @@ public class CallBackResolution {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "Decode hex failed");
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
String tempTxt = "";
|
String tempTxt = "";
|
||||||
for (int i = 0; i < stringsSpker.size(); i++) {
|
for (int i = 0; i < stringsSpker.size(); i++) {
|
||||||
tempTxt += unicode(stringsSpker.get(i));
|
tempTxt += unicode(stringsSpker.get(i));
|
||||||
}
|
}
|
||||||
talkName = tempTxt;
|
talkName = tempTxt;
|
||||||
CallBackUtil.callBackNotifySpker(speckType, indexGroupId, indexGroupName, talkId, talkName);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -239,67 +220,14 @@ public class CallBackResolution {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "Decode hex failed");
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
String tempTxt = "";
|
String tempTxt = "";
|
||||||
for (int i = 0; i < stringsTalk.size(); i++) {
|
for (int i = 0; i < stringsTalk.size(); i++) {
|
||||||
tempTxt += unicode(stringsTalk.get(i));
|
tempTxt += unicode(stringsTalk.get(i));
|
||||||
}
|
}
|
||||||
int code=-1;
|
Timber.tag(TAG).i(" 临时讲话 信息通知的指令 " + tempTxt);
|
||||||
if(tempTxt.equals("退出临时呼叫")){
|
|
||||||
code=0;
|
|
||||||
} else if (tempTxt.startsWith("临时呼叫临时群组")) {
|
|
||||||
code=1;
|
|
||||||
} else if (tempTxt.equals("呼出失败")||tempTxt.equals("呼叫失败")) {
|
|
||||||
code=2;
|
|
||||||
} else if (tempTxt.equals("呼叫成功")||tempTxt.equals("呼出成功")) {
|
|
||||||
code=3;
|
|
||||||
}else{
|
|
||||||
code=-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag(TAG).i( " 临时讲话 信息通知的指令 " + tempTxt + " code=" + code);
|
|
||||||
CallBackUtil.callBackTempCallNotify(tempTxt);
|
CallBackUtil.callBackTempCallNotify(tempTxt);
|
||||||
|
|
||||||
// 处理临时呼叫页面逻辑
|
|
||||||
if (code == 1) {
|
|
||||||
// 临时呼叫临时群组 → 打开临时呼叫页面
|
|
||||||
try {
|
|
||||||
android.content.Intent intent = new android.content.Intent(
|
|
||||||
com.stand.standapp.MyApplication.getAppInstance(),
|
|
||||||
com.stand.standapp.TempCallActivity.class
|
|
||||||
);
|
|
||||||
// 从84消息中提取临时群名称,格式为"临时呼叫XXX"
|
|
||||||
String tempGroupName = "临时会话";
|
|
||||||
if (tempTxt.startsWith("临时呼叫") && tempTxt.length() > "临时呼叫".length()) {
|
|
||||||
tempGroupName = tempTxt.substring("临时呼叫".length());
|
|
||||||
}
|
|
||||||
intent.putExtra("group_name", tempGroupName);
|
|
||||||
intent.addFlags(
|
|
||||||
android.content.Intent.FLAG_ACTIVITY_NEW_TASK |
|
|
||||||
android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
||||||
);
|
|
||||||
com.stand.standapp.MyApplication.getAppInstance().startActivity(intent);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.tag(TAG).e( "Failed to open TempCallActivity: " + e.getMessage());
|
|
||||||
}
|
|
||||||
} else if (code == 0) {
|
|
||||||
// 退出临时呼叫 → 关闭临时呼叫页面
|
|
||||||
if (com.stand.standapp.TempCallActivity.isOpen()) {
|
|
||||||
com.stand.standapp.TempCallActivity.closeWithMessage("临时呼叫已结束");
|
|
||||||
} else if (com.stand.standapp.TempCallActivity.isMinimizedState()) {
|
|
||||||
// 最小化状态下收到结束消息,清除状态并通知前端
|
|
||||||
com.stand.standapp.TempCallActivity.clearMinimizedState();
|
|
||||||
}
|
|
||||||
} else if (code == 2) {
|
|
||||||
// 呼叫失败 → 关闭临时呼叫页面
|
|
||||||
// if (com.stand.standapp.TempCallActivity.isOpen()) {
|
|
||||||
// com.stand.standapp.TempCallActivity.closeWithMessage("呼叫失败");
|
|
||||||
// } else if (com.stand.standapp.TempCallActivity.isMinimizedState()) {
|
|
||||||
// // 最小化状态下收到失败消息,清除状态并通知前端
|
|
||||||
// com.stand.standapp.TempCallActivity.clearMinimizedState();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -325,7 +253,7 @@ public class CallBackResolution {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "Decode hex failed");
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
String userGroupName = "";
|
String userGroupName = "";
|
||||||
for (int i = 0; i < stringsJumpGroup.size(); i++) {
|
for (int i = 0; i < stringsJumpGroup.size(); i++) {
|
||||||
|
|
@ -358,7 +286,7 @@ public class CallBackResolution {
|
||||||
String time = strLis[2];
|
String time = strLis[2];
|
||||||
CallBackUtil.callBackNotifyLocationStatus(0, locationMemberId, latitude, longitude, time);
|
CallBackUtil.callBackNotifyLocationStatus(0, locationMemberId, latitude, longitude, time);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e( "获取定位Exception-->" + e.getMessage());
|
Timber.tag(TAG).e(e, "获取定位Exception");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
CallBackUtil.callBackNotifyLocationStatus(1, "", 0.0, 0.0, "");
|
CallBackUtil.callBackNotifyLocationStatus(1, "", 0.0, 0.0, "");
|
||||||
|
|
@ -412,14 +340,14 @@ public class CallBackResolution {
|
||||||
List<String> strName = new ArrayList<>();
|
List<String> strName = new ArrayList<>();
|
||||||
double latitude = 0.0, longitude = 0.0;
|
double latitude = 0.0, longitude = 0.0;
|
||||||
try {
|
try {
|
||||||
String string = "";
|
String string = "";
|
||||||
for (int i = 4; i < lisHex.size(); i++) {
|
for (int i = 4; i < lisHex.size(); i++) {
|
||||||
string += lisHex.get(i).toString();
|
string += lisHex.get(i).toString();
|
||||||
}
|
}
|
||||||
String address = hexString2String(string);
|
String address = hexString2String(string);
|
||||||
Timber.tag(TAG).i( "address-->" + address);
|
Timber.tag(TAG).i("address-->" + address);
|
||||||
|
|
||||||
String[] strLis = address.split(",");
|
String[] strLis = address.split(",");
|
||||||
latitude = Double.parseDouble(strLis[0]);
|
latitude = Double.parseDouble(strLis[0]);
|
||||||
longitude = Double.parseDouble(strLis[1]);
|
longitude = Double.parseDouble(strLis[1]);
|
||||||
int size = lisHex.size() - (8 + strLis[0].length() + strLis[1].length());
|
int size = lisHex.size() - (8 + strLis[0].length() + strLis[1].length());
|
||||||
|
|
@ -459,47 +387,26 @@ public class CallBackResolution {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}catch (Exception e){
|
}catch (Exception e){
|
||||||
Timber.tag(TAG).e("at_OEM_AT_cb Exception " + e.getMessage());
|
Timber.tag(TAG).e(e, "at_OEM_AT_cb Exception");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断登录状态冲突, 1min内超过5次状态切换则视为登录冲突
|
|
||||||
* state
|
|
||||||
* 0 : 离线
|
|
||||||
* 1 : 登陆中
|
|
||||||
* 2 : 登陆成功
|
|
||||||
* 3 : 注销中
|
|
||||||
* 说明:第一次登录 通知状态 先01再02,
|
|
||||||
* 离线再上线 通知状态 先00再01
|
|
||||||
* tip:1min内会出现40次左右0-1切换
|
|
||||||
*/
|
|
||||||
public static boolean isLoginConflict(String state) {
|
public static boolean isLoginConflict(String state) {
|
||||||
Timber.tag(TAG).d( " isLoginConflict state: " + state + ",oldLoginSate: " + oldLoginSate + ", logNum: " + logNum);
|
|
||||||
if (oldLoginSate.equals("00") && state.equals("01")) {
|
if (oldLoginSate.equals("00") && state.equals("01")) {
|
||||||
Timber.tag(TAG).d( "isLoginConflict first login...");
|
|
||||||
logNum++;
|
logNum++;
|
||||||
oldLoginSate = state;
|
oldLoginSate = state;
|
||||||
} else if (oldLoginSate.equals("01") && state.equals("00")) {
|
} else if (oldLoginSate.equals("01") && state.equals("00")) {
|
||||||
Timber.tag(TAG).d( "isLoginConflict login state changed...");
|
|
||||||
logNum++;
|
logNum++;
|
||||||
oldLoginSate = state;
|
oldLoginSate = state;
|
||||||
}
|
}
|
||||||
if (1 == logNum) {
|
if (1 == logNum) {
|
||||||
offlineTime = System.currentTimeMillis();
|
offlineTime = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
Timber.tag(TAG).d( "isLoginConflict logNum: " + logNum + ", offlineTime: " + offlineTime);
|
if (logNum > 4) {
|
||||||
if (System.currentTimeMillis() - offlineTime <= 60 * 1000) {
|
if (System.currentTimeMillis() - offlineTime >= 60 * 1000) {
|
||||||
if (logNum > 5) {
|
|
||||||
Timber.tag(TAG).d("isLoginConflict login state changed: " + logNum + " in 1min");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Timber.tag(TAG).d("isLoginConflict maybe network change, reset...");
|
|
||||||
logNum = 0;
|
|
||||||
offlineTime = System.currentTimeMillis();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -508,8 +415,6 @@ public class CallBackResolution {
|
||||||
try {
|
try {
|
||||||
String[] txt = sData.split("\"");
|
String[] txt = sData.split("\"");
|
||||||
String data = txt[1];
|
String data = txt[1];
|
||||||
Timber.tag(TAG).d( "at_Play_TTS_cb sData: " + sData);
|
|
||||||
|
|
||||||
String ngx = "\\u";
|
String ngx = "\\u";
|
||||||
List<String> strings = new ArrayList<>();
|
List<String> strings = new ArrayList<>();
|
||||||
|
|
||||||
|
|
@ -532,45 +437,34 @@ public class CallBackResolution {
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "Decode hex failed");
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
StringBuilder sb = new StringBuilder();
|
String tempTxt = "";
|
||||||
for (int k = 0; k < strings.size(); k++) {
|
for (int k = 0; k < strings.size(); k++) {
|
||||||
sb.append(unicode(strings.get(k)));
|
tempTxt += unicode(strings.get(k));
|
||||||
}
|
}
|
||||||
String tempTxt = sb.toString();
|
|
||||||
Timber.tag(TAG).i("at_Play_TTS_cb " + tempTxt);
|
|
||||||
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);
|
java.util.concurrent.ScheduledExecutorService executor = java.util.concurrent.Executors.newScheduledThreadPool(1);
|
||||||
Runnable task = () -> {
|
Runnable task = () -> {
|
||||||
Timber.tag(TAG).d( "callBackLogin send tcp and udp");
|
|
||||||
SendAtUtil.sendUDP();
|
SendAtUtil.sendUDP();
|
||||||
SendAtUtil.sendTCP();
|
SendAtUtil.sendTCP();
|
||||||
};
|
};
|
||||||
executor.scheduleAtFixedRate(task, 0, 40, TimeUnit.SECONDS);
|
executor.scheduleAtFixedRate(task, 0, 40, java.util.concurrent.TimeUnit.SECONDS);
|
||||||
} else if (tempTxt.contains("账号已更新")) {
|
|
||||||
Timber.tag(TAG).w("TTS reports account updated: '%s'", tempTxt);
|
|
||||||
} else if (tempTxt.contains("账号或密码错误")) {
|
} else if (tempTxt.contains("账号或密码错误")) {
|
||||||
CallBackUtil.callBackLogin(false, "");
|
CallBackUtil.callBackLogin(false, "");
|
||||||
Timber.tag(TAG).e("Account/password error, showing dialog. TTS: '%s'", tempTxt);
|
|
||||||
com.stand.standapp.MainActivity.showPttUpdateDialog("您的 PTT 账号信息账号或密码错误。\n请点击确认重启应用。","账号或密码错误");
|
|
||||||
}else if (tempTxt.contains("请配置账号")) {
|
|
||||||
Timber.tag(TAG).w("TTS reports no account configured: '%s'", tempTxt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Timber.tag(TAG).e(e, "Decode hex failed");
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static String unicode(String str) {
|
public static String unicode(String str) {
|
||||||
|
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\\\u(\\p{XDigit}{4}))");
|
||||||
Pattern pattern = Pattern.compile("(\\\\u(\\p{XDigit}{4}))");
|
java.util.regex.Matcher matcher = pattern.matcher(str);
|
||||||
Matcher matcher = pattern.matcher(str);
|
|
||||||
char ch;
|
char ch;
|
||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
String group = matcher.group(2);
|
String group = matcher.group(2);
|
||||||
|
|
@ -586,13 +480,9 @@ public class CallBackResolution {
|
||||||
StringBuilder str = new StringBuilder();
|
StringBuilder str = new StringBuilder();
|
||||||
for (int i = 0; i < t; i++) {
|
for (int i = 0; i < t; i++) {
|
||||||
String s = hex.substring(i * 6, (i + 1) * 6);
|
String s = hex.substring(i * 6, (i + 1) * 6);
|
||||||
// 高位需要补上00再转
|
|
||||||
String s1 = s.substring(2, 4) + "00";
|
String s1 = s.substring(2, 4) + "00";
|
||||||
// 低位直接转
|
|
||||||
String s2 = s.substring(4);
|
String s2 = s.substring(4);
|
||||||
// 将16进制的string转为int
|
|
||||||
int n = Integer.valueOf(s1, 16) + Integer.valueOf(s2, 16);
|
int n = Integer.valueOf(s1, 16) + Integer.valueOf(s2, 16);
|
||||||
// 将int转换为字符
|
|
||||||
char[] chars = Character.toChars(n);
|
char[] chars = Character.toChars(n);
|
||||||
str.append(new String(chars));
|
str.append(new String(chars));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.example.kingway.ptt;
|
package com.example.kingway.ptt;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import android.os.Handler;
|
||||||
|
import android.util.Log;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ public class CallBackUtil {
|
||||||
* name 用户名
|
* name 用户名
|
||||||
*/
|
*/
|
||||||
public static void callBackLogin(boolean loginState, String name) {
|
public static void callBackLogin(boolean loginState, String name) {
|
||||||
Timber.tag(TAG).d("callBackLogin loginState=%s name=%s", loginState, name);
|
Log.i(TAG, "callBackLogin loginState " + loginState + " name " + name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -34,7 +34,7 @@ public class CallBackUtil {
|
||||||
* loginId 用户ID,登录成功id有效
|
* loginId 用户ID,登录成功id有效
|
||||||
*/
|
*/
|
||||||
public static void callBackLoinState(String state, String loginId) {
|
public static void callBackLoinState(String state, String loginId) {
|
||||||
Timber.tag(TAG).d("callBackLoinState state=%s loginId=%s", state, loginId);
|
Log.d(TAG, "callBackLoinState state: " + state + ", loginId: " + loginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -43,14 +43,13 @@ public class CallBackUtil {
|
||||||
* false:登录不冲突
|
* false:登录不冲突
|
||||||
* */
|
* */
|
||||||
public static void callBackLoginConflict(boolean loginConflict) {
|
public static void callBackLoginConflict(boolean loginConflict) {
|
||||||
Timber.tag(TAG).d("callBackLoginConflict: %s", loginConflict);
|
Log.d(TAG, "callBackLoginConflict: " + loginConflict);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录账号有视频功能通知
|
* 登录账号有视频功能通知
|
||||||
*/
|
*/
|
||||||
public static boolean callBackHaveVideo() {
|
public static boolean callBackHaveVideo() {
|
||||||
Timber.tag(TAG).d("callBackHaveVideo invoked");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +60,7 @@ public class CallBackUtil {
|
||||||
* state true-成功, false-失败
|
* state true-成功, false-失败
|
||||||
*/
|
*/
|
||||||
public static void callBackOnGroupSuccess(boolean state) {
|
public static void callBackOnGroupSuccess(boolean state) {
|
||||||
Timber.tag(TAG).d("callBackOnGroupSuccess state=%s", state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -69,19 +68,7 @@ public class CallBackUtil {
|
||||||
* state true-成功, false-失败
|
* state true-成功, false-失败
|
||||||
*/
|
*/
|
||||||
public static void callBackOnMemberSuccess(boolean state, List<GroupMemberInfoDto> dtos) {
|
public static void callBackOnMemberSuccess(boolean state, List<GroupMemberInfoDto> dtos) {
|
||||||
try {
|
|
||||||
org.json.JSONArray arr = new org.json.JSONArray();
|
|
||||||
if (dtos != null && state) {
|
|
||||||
for (GroupMemberInfoDto dto : dtos) {
|
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
|
||||||
obj.put("memberId", dto.getMemberId());
|
|
||||||
obj.put("memberName", dto.getGmemberName());
|
|
||||||
obj.put("status", dto.getStatus());
|
|
||||||
arr.put(obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('MemberList', " + arr.toString() + ");}");
|
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -89,16 +76,8 @@ public class CallBackUtil {
|
||||||
* state true-成功, false-失败
|
* state true-成功, false-失败
|
||||||
*/
|
*/
|
||||||
public static void callBackSpeakPlaySuccess(boolean state) {
|
public static void callBackSpeakPlaySuccess(boolean state) {
|
||||||
Timber.tag(TAG).d("callBackSpeakPlaySuccess state=%s", state);
|
Log.d(TAG, "callBackSpeakPlaySuccess state: " + state);
|
||||||
if (state) {
|
if (state) AudioRecordManager.getInstance().startRecord();
|
||||||
AudioRecordManager.getInstance().startRecord();
|
|
||||||
Timber.tag(TAG).d("AudioRecord started");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
|
||||||
obj.put("success", state);
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('SpeakResult', " + obj.toString() + ");}");
|
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -106,11 +85,6 @@ public class CallBackUtil {
|
||||||
* state true-成功, false-失败
|
* state true-成功, false-失败
|
||||||
*/
|
*/
|
||||||
public static void callBackSpeakReleaseSuccess(boolean state) {
|
public static void callBackSpeakReleaseSuccess(boolean state) {
|
||||||
try {
|
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
|
||||||
obj.put("success", state);
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('SpeakEnd', " + obj.toString() + ");}");
|
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -120,26 +94,16 @@ public class CallBackUtil {
|
||||||
* groupNo 表示此组为第几个组
|
* groupNo 表示此组为第几个组
|
||||||
* groupCount 表示此组用户成员数
|
* groupCount 表示此组用户成员数
|
||||||
*/
|
*/
|
||||||
public static void callBackGroupInfo(String groupId, String groupName, int groupNo, int groupCount) {
|
public static void callBackGroupInfo(String groupId, String groupName, int groupNo, int groupCount) {}
|
||||||
Timber.tag(TAG).d("callBackGroupInfo groupId=%s name=%s no=%d count=%d", groupId, groupName, groupNo, groupCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 返回群组集合信息
|
* 返回群组集合信息
|
||||||
* GroupInfoDto 群组信息
|
* GroupInfoDto 群组信息
|
||||||
* */
|
* */
|
||||||
public static void callBackGroupInfo (ArrayList < GroupInfoDto > groupInfoDtos) {
|
public static void callBackGroupInfo (ArrayList < GroupInfoDto > groupInfoDtos) {
|
||||||
try {
|
for (GroupInfoDto groupInfoDto : groupInfoDtos) {
|
||||||
org.json.JSONArray arr = new org.json.JSONArray();
|
Log.d(TAG, "callBackGroupInfo: " + groupInfoDto.toString());
|
||||||
for (GroupInfoDto dto : groupInfoDtos) {
|
}
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
|
||||||
obj.put("groupId", dto.getGroupId());
|
|
||||||
obj.put("groupName", dto.getGroupName());
|
|
||||||
obj.put("groupNo", dto.getGroupNo());
|
|
||||||
arr.put(obj);
|
|
||||||
}
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('GroupList', " + arr.toString() + ");}");
|
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -148,50 +112,44 @@ public class CallBackUtil {
|
||||||
* talkId 讲话用户ID
|
* talkId 讲话用户ID
|
||||||
* talkName 讲话用户名字
|
* talkName 讲话用户名字
|
||||||
* */
|
* */
|
||||||
public static void callBackNotifySpker ( int speckType, String groupId, String groupName, String talkId, String talkName){
|
public static void callBackNotifySpker ( int speckType, String groupId, String
|
||||||
Timber.tag(TAG).d("callBackNotifySpker speckType=%d groupId=%s talkId=%s talkName=%s", speckType, groupId, talkId, talkName);
|
groupName, String talkId, String talkName){
|
||||||
try {
|
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
|
||||||
obj.put("speckType", speckType);
|
|
||||||
obj.put("groupId", groupId);
|
|
||||||
obj.put("groupName", groupName);
|
|
||||||
obj.put("talkId", talkId);
|
|
||||||
obj.put("talkName", talkName);
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('SpeakerUpdate', " + obj.toString() + ");}");
|
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void callBackNotifyPlayStatus ( int playStatus, int speckType, String groupId, String groupName, String talkId, String talkName){
|
/**
|
||||||
Timber.tag(TAG).d("callBackNotifyPlayStatus playStatus=%d talkId=%s", playStatus, talkId);
|
* 对方讲话通知
|
||||||
try {
|
* playStatus 1表示开始讲话, 0 表示结束讲话
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
* speckType 自己讲话状态: 0 : 表示自己无法讲话; 1 : 表示自己可以中断讲话人的讲话,可以进行强插讲话
|
||||||
obj.put("playStatus", playStatus);
|
* groupId 所在群组Id
|
||||||
obj.put("speckType", speckType);
|
* groupName 所在群组名称
|
||||||
obj.put("groupId", groupId);
|
* talkId 讲话用户Id
|
||||||
obj.put("groupName", groupName);
|
* talkName 讲话用户名称
|
||||||
obj.put("talkId", talkId);
|
* */
|
||||||
obj.put("talkName", talkName);
|
public static void callBackNotifyPlayStatus ( int playStatus, int speckType, String
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('PlayStatus', " + obj.toString() + ");}");
|
groupId, String groupName, String talkId, String talkName){
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*临时呼叫信息通知
|
||||||
|
* tempCallNotify:
|
||||||
|
* 临时呼叫xxx:表示被单呼,呼叫名:xxx
|
||||||
|
* 退出临时呼叫:表示退出单呼
|
||||||
|
* 呼叫成功:表示单呼成功
|
||||||
|
* 呼叫失败:表示单呼失败
|
||||||
|
* */
|
||||||
public static void callBackTempCallNotify (String tempCallNotify){
|
public static void callBackTempCallNotify (String tempCallNotify){
|
||||||
Timber.tag(TAG).d("callBackTempCallNotify msg=%s", tempCallNotify);
|
|
||||||
try {
|
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
|
||||||
obj.put("msg", tempCallNotify);
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('TempCall', " + obj.toString() + ");}");
|
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户进入群组信息通知
|
||||||
|
* userGroupId 用户所在的群组ID
|
||||||
|
* userGroupName 用户所在的群组名称
|
||||||
|
* */
|
||||||
public static void callBackNotifyUpdateGroup (String userGroupId, String userGroupName){
|
public static void callBackNotifyUpdateGroup (String userGroupId, String userGroupName){
|
||||||
Timber.tag(TAG).d("callBackNotifyUpdateGroup groupId=%s groupName=%s", userGroupId, userGroupName);
|
|
||||||
try {
|
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
|
||||||
obj.put("groupId", userGroupId);
|
|
||||||
obj.put("groupName", userGroupName);
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('UpdateGroup', " + obj.toString() + ");}");
|
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -202,17 +160,9 @@ public class CallBackUtil {
|
||||||
* longitude 经度
|
* longitude 经度
|
||||||
* time 时间
|
* time 时间
|
||||||
*/
|
*/
|
||||||
public static void callBackNotifyLocationStatus ( int status, String userId,double latitude, double longitude, String time){
|
public static void callBackNotifyLocationStatus ( int status, String userId,double latitude,
|
||||||
Timber.tag(TAG).d("callBackNotifyLocationStatus status=%d userId=%s lat=%f lng=%f", status, userId, latitude, longitude);
|
double longitude, String time){
|
||||||
try {
|
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
|
||||||
obj.put("status", status);
|
|
||||||
obj.put("userId", userId);
|
|
||||||
obj.put("latitude", latitude);
|
|
||||||
obj.put("longitude", longitude);
|
|
||||||
obj.put("time", time);
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('LocationStatus', " + obj.toString() + ");}");
|
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -220,7 +170,7 @@ public class CallBackUtil {
|
||||||
* status true:成功, false:失败
|
* status true:成功, false:失败
|
||||||
* */
|
* */
|
||||||
public static void callBackNotifyUploadLocation ( boolean status){
|
public static void callBackNotifyUploadLocation ( boolean status){
|
||||||
Timber.tag(TAG).d("callBackNotifyUploadLocation status=%s", status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -232,7 +182,7 @@ public class CallBackUtil {
|
||||||
* inv=1:1勾选单呼,0不勾选单呼
|
* inv=1:1勾选单呼,0不勾选单呼
|
||||||
* */
|
* */
|
||||||
public static void callBackNotifyCodeInfo (String codeInfo){
|
public static void callBackNotifyCodeInfo (String codeInfo){
|
||||||
Timber.tag(TAG).d("callBackNotifyCodeInfo: %s", codeInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -242,7 +192,7 @@ public class CallBackUtil {
|
||||||
* pwd=111111 账号密码:111111
|
* pwd=111111 账号密码:111111
|
||||||
* */
|
* */
|
||||||
public static void callBackNotifyPwdInfo (String pwdInfo){
|
public static void callBackNotifyPwdInfo (String pwdInfo){
|
||||||
Timber.tag(TAG).d("callBackNotifyPwdInfo: %s", pwdInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -250,7 +200,7 @@ public class CallBackUtil {
|
||||||
* status true:成功,false:失败
|
* status true:成功,false:失败
|
||||||
* */
|
* */
|
||||||
public static void callBackNotifyCodeOpenStatus ( boolean status){
|
public static void callBackNotifyCodeOpenStatus ( boolean status){
|
||||||
Timber.tag(TAG).d("callBackNotifyCodeOpenStatus status=%s", status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -258,7 +208,7 @@ public class CallBackUtil {
|
||||||
* status true:成功,false:失败
|
* status true:成功,false:失败
|
||||||
* */
|
* */
|
||||||
public static void callBackNotifyCodeExiteStatus ( boolean status){
|
public static void callBackNotifyCodeExiteStatus ( boolean status){
|
||||||
Timber.tag(TAG).d("callBackNotifyCodeExiteStatus status=%s", status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -266,7 +216,7 @@ public class CallBackUtil {
|
||||||
* status true:成功,false:失败
|
* status true:成功,false:失败
|
||||||
* */
|
* */
|
||||||
public static void callBackNotifyUploadAgentPwd ( boolean status){
|
public static void callBackNotifyUploadAgentPwd ( boolean status){
|
||||||
Timber.tag(TAG).d("callBackNotifyUploadAgentPwd status=%s", status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -274,7 +224,7 @@ public class CallBackUtil {
|
||||||
* status true:成功,false:失败
|
* status true:成功,false:失败
|
||||||
* */
|
* */
|
||||||
public static void callBackRequestReportAlarm ( boolean status){
|
public static void callBackRequestReportAlarm ( boolean status){
|
||||||
Timber.tag(TAG).d("callBackRequestReportAlarm status=%s", status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -282,7 +232,7 @@ public class CallBackUtil {
|
||||||
* status true:成功,false:失败
|
* status true:成功,false:失败
|
||||||
* */
|
* */
|
||||||
public static void callBackRequestStopAlarm ( boolean status){
|
public static void callBackRequestStopAlarm ( boolean status){
|
||||||
Timber.tag(TAG).d("callBackRequestStopAlarm status=%s", status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -292,22 +242,14 @@ public class CallBackUtil {
|
||||||
* longitude 经度
|
* longitude 经度
|
||||||
* */
|
* */
|
||||||
public static void callBackEnotifyAlarm (String memberName,double latitude, double longitude){
|
public static void callBackEnotifyAlarm (String memberName,double latitude, double longitude){
|
||||||
Timber.tag(TAG).w("callBackEnotifyAlarm memberName=%s lat=%f lng=%f", memberName, latitude, longitude);
|
|
||||||
try {
|
|
||||||
org.json.JSONObject obj = new org.json.JSONObject();
|
|
||||||
obj.put("memberName", memberName);
|
|
||||||
obj.put("latitude", latitude);
|
|
||||||
obj.put("longitude", longitude);
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('Alarm', " + obj.toString() + ");}");
|
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "Failed to push PTT event to JS"); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 收到群成员停止警告通知
|
* 收到群成员停止警告通知
|
||||||
* */
|
* */
|
||||||
public static void callBackEnotifyStopAlarm () {
|
public static void callBackEnotifyStopAlarm () {
|
||||||
Timber.tag(TAG).d("callBackEnotifyStopAlarm invoked");
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPttEvent){window.onPttEvent('StopAlarm', {});}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -315,7 +257,7 @@ public class CallBackUtil {
|
||||||
* status true:成功,false:失败
|
* status true:成功,false:失败
|
||||||
* */
|
* */
|
||||||
public static void callBackRequestNfc ( boolean status){
|
public static void callBackRequestNfc ( boolean status){
|
||||||
Timber.tag(TAG).d("callBackRequestNfc status=%s", status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@ package com.example.kingway.ptt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
* <pre>
|
||||||
* author : GaryLiang
|
|
||||||
* e-mail : 595184932@qq.com
|
|
||||||
* time : 2017/11/21 上午9:09
|
|
||||||
* desc : 组
|
* desc : 组
|
||||||
* version: 1.0
|
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@ import java.io.Serializable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
* <pre>
|
||||||
* author : GaryLiang
|
|
||||||
* e-mail : 595184932@qq.com
|
|
||||||
* time : 2017/11/21 上午9:09
|
|
||||||
* desc : 组 成员
|
* desc : 组 成员
|
||||||
* version: 1.0
|
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -81,4 +77,4 @@ public class GroupMemberInfoDto implements Serializable {
|
||||||
", haveVideo=" + haveVideo +
|
", haveVideo=" + haveVideo +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package com.example.kingway.ptt;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public class MyApplication {
|
||||||
|
private static Context mContext;
|
||||||
|
public static String pageName = "";
|
||||||
|
public static pttNative pNative;
|
||||||
|
private static MyApplication mAppInstance = new MyApplication();
|
||||||
|
|
||||||
|
public static void init(Context context) {
|
||||||
|
if (mContext != null) return;
|
||||||
|
mContext = context.getApplicationContext();
|
||||||
|
pageName = mContext.getPackageName();
|
||||||
|
initPTT();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MyApplication getAppInstance() {
|
||||||
|
if (pNative == null && mContext != null) {
|
||||||
|
initPTT();
|
||||||
|
}
|
||||||
|
return mAppInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public pttNative getpNative() {
|
||||||
|
return pNative;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initPTT() {
|
||||||
|
if (pNative != null) return;
|
||||||
|
try {
|
||||||
|
pNative = new pttNative();
|
||||||
|
|
||||||
|
pNative.pttNativeSetPackageName(pageName);
|
||||||
|
pNative.pttNativeSetAtCallback();
|
||||||
|
pNative.pttNativeSetPlayCallback();
|
||||||
|
pNative.pttNativeSetPalyStartCallback();
|
||||||
|
pNative.pttNativeSetPlayStopCallback();
|
||||||
|
pNative.pttNativeSetRecordStartCallback();
|
||||||
|
pNative.pttNativeSetRecordStopCallback();
|
||||||
|
pNative.pttNativeSetTTSCallback();
|
||||||
|
|
||||||
|
pNative.pttNativePocTaskStart();
|
||||||
|
Log.i("PTT_INIT", "PTT init success");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Log.e("PTT_INIT", "Failed to init PTT: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void destroy() {
|
||||||
|
if (pNative != null) {
|
||||||
|
pNative.destory();
|
||||||
|
pNative = null;
|
||||||
|
}
|
||||||
|
mContext = null;
|
||||||
|
mAppInstance = new MyApplication();
|
||||||
|
CallBackResolution.resetPttLoginStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
package com.example.kingway.ptt;
|
package com.example.kingway.ptt;
|
||||||
|
|
||||||
import com.stand.standapp.MyApplication;
|
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -18,20 +16,6 @@ public class SendAtUtil {
|
||||||
sendRawCmd(cmd);
|
sendRawCmd(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取登录参数
|
|
||||||
*/
|
|
||||||
public static void toGetParam() {
|
|
||||||
send("020000\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消登录接口 (清除服务端/本地登录状态)
|
|
||||||
*/
|
|
||||||
public static void toCancelLogin() {
|
|
||||||
send("050000\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 账号注销接口
|
* 账号注销接口
|
||||||
*/
|
*/
|
||||||
|
|
@ -48,7 +32,6 @@ public class SendAtUtil {
|
||||||
* tempCall 是否开启单呼 1开启单呼,0关闭单呼
|
* tempCall 是否开启单呼 1开启单呼,0关闭单呼
|
||||||
* */
|
* */
|
||||||
public static void toLogin(String account, String pwd, String ip, int location, int tempCall){
|
public static void toLogin(String account, String pwd, String ip, int location, int tempCall){
|
||||||
Timber.tag("PTT").i("Login: account=%s, ip=%s, gps=%d, tempCall=%d", account, ip, location, tempCall);
|
|
||||||
String ipHex = str2HexStr("ip=" + ip + ";");
|
String ipHex = str2HexStr("ip=" + ip + ";");
|
||||||
String accountHex = str2HexStr("id=" + account + ";");
|
String accountHex = str2HexStr("id=" + account + ";");
|
||||||
String pwdHex = str2HexStr("pwd=" + pwd + ";");
|
String pwdHex = str2HexStr("pwd=" + pwd + ";");
|
||||||
|
|
@ -63,26 +46,22 @@ public class SendAtUtil {
|
||||||
* 按下PTT开始呼叫
|
* 按下PTT开始呼叫
|
||||||
* */
|
* */
|
||||||
public static void speakPlay(){
|
public static void speakPlay(){
|
||||||
Timber.tag("PTT").i("PTT speak START (press)");
|
|
||||||
send("0B0000");
|
send("0B0000");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 松开PTT结束呼叫*/
|
/** 松开PTT结束呼叫*/
|
||||||
public static void speakRelease(){
|
public static void speakRelease(){
|
||||||
Timber.tag("PTT").i("PTT speak STOP (release)");
|
|
||||||
AudioRecordManager.getInstance().stopRecord();
|
AudioRecordManager.getInstance().stopRecord();
|
||||||
send("0C0000");
|
send("0C0000");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发送TCP包 登录成功后需要定时发送TCP包*/
|
/** 发送TCP包 登录成功后需要定时发送TCP包*/
|
||||||
public static void sendTCP(){
|
public static void sendTCP(){
|
||||||
Timber.tag("PTT").v("TCP keepalive sent");
|
|
||||||
send("570000\r\n");
|
send("570000\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发送UDP包 登录后需要定时发送UDP包*/
|
/** 发送UDP包 登录后需要定时发送UDP包*/
|
||||||
public static void sendUDP(){
|
public static void sendUDP(){
|
||||||
Timber.tag("PTT").v("UDP keepalive sent");
|
|
||||||
send("580000\r\n");
|
send("580000\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +81,6 @@ public class SendAtUtil {
|
||||||
* groupId 十六进制群组Id
|
* groupId 十六进制群组Id
|
||||||
* */
|
* */
|
||||||
public static void jumpGroup(String groupId){
|
public static void jumpGroup(String groupId){
|
||||||
Timber.tag("PTT").i("Jump to group: %s", groupId);
|
|
||||||
send("090000" + groupId);
|
send("090000" + groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,11 +89,6 @@ public class SendAtUtil {
|
||||||
send("0E000000000000\r\n");
|
send("0E000000000000\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 结束多成员临时呼叫 */
|
|
||||||
public static void stopTempCall(){
|
|
||||||
send("5500000100000000\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字符串转换成十六进制字符串
|
* 字符串转换成十六进制字符串
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,36 @@
|
||||||
package com.example.kingway.ptt;
|
package com.example.kingway.ptt;
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.AutoCompleteTextView;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
import com.hjq.permissions.Permission;
|
import com.hjq.permissions.Permission;
|
||||||
import com.hjq.permissions.XXPermissions;
|
import com.hjq.permissions.XXPermissions;
|
||||||
import com.stand.standapp.R;
|
import com.stand.standapp.R;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class pttActivity extends AppCompatActivity {
|
public class pttActivity extends AppCompatActivity {
|
||||||
private Button pttBtn;
|
private Button pttBtn;
|
||||||
private AutoCompleteTextView etIp, etAccount, etPassword, etGroupId;
|
|
||||||
private static final String PREFS_NAME = "ptt_history";
|
|
||||||
private static final String KEY_IP_HISTORY = "ip_history";
|
|
||||||
private static final String KEY_ACCOUNT_HISTORY = "account_history";
|
|
||||||
private static final String KEY_PASSWORD_HISTORY = "password_history";
|
|
||||||
private static final String KEY_GROUP_HISTORY = "group_history";
|
|
||||||
private static final String SEPARATOR = ",";
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_ptt);
|
setContentView(R.layout.activity_ptt);
|
||||||
|
|
||||||
etIp = findViewById(R.id.etIp);
|
|
||||||
etAccount = findViewById(R.id.etAccount);
|
|
||||||
etPassword = findViewById(R.id.etPassword);
|
|
||||||
etGroupId = findViewById(R.id.etGroupId);
|
|
||||||
|
|
||||||
loadHistory();
|
|
||||||
|
|
||||||
pttBtn = (Button) findViewById(R.id.pttBtn);
|
pttBtn = (Button) findViewById(R.id.pttBtn);
|
||||||
|
|
||||||
|
// 初始化 PTT
|
||||||
|
MyApplication.init(this);
|
||||||
|
|
||||||
XXPermissions.with(this)
|
XXPermissions.with(this)
|
||||||
.permission(Permission.RECORD_AUDIO)
|
.permission(Permission.RECORD_AUDIO)
|
||||||
.request(null);
|
.request(null);
|
||||||
|
|
@ -61,38 +48,16 @@ public class pttActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
|
findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
SendAtUtil.toLogin("gzzdweb01", "123456", "61.243.1.123", 1, 1);
|
// SendAtUtil.toLogin("gzzdweb01", "123456", "61.243.1.123", 1, 1);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
findViewById(R.id.btnCustomLogin).setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
String ip = etIp.getText().toString().trim();
|
|
||||||
String account = etAccount.getText().toString().trim();
|
|
||||||
String password = etPassword.getText().toString().trim();
|
|
||||||
String groupId = etGroupId.getText().toString().trim();
|
|
||||||
|
|
||||||
if (ip.isEmpty() || account.isEmpty() || password.isEmpty() || groupId.isEmpty()) {
|
|
||||||
Toast.makeText(pttActivity.this, "请填写所有字段", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveToHistory(ip, account, password, groupId);
|
|
||||||
SendAtUtil.toLogin(account, password, ip, 1, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
findViewById(R.id.login1).setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
SendAtUtil.toLogin("test004", "123456", "220.154.131.231", 1, 1);
|
SendAtUtil.toLogin("test004", "123456", "220.154.131.231", 1, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 获取群组 */
|
||||||
findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
|
findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
|
@ -100,6 +65,7 @@ public class pttActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 获取群成员 */
|
||||||
findViewById(R.id.button3).setOnClickListener(new View.OnClickListener() {
|
findViewById(R.id.button3).setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
|
|
@ -107,65 +73,12 @@ public class pttActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 切换群组 */
|
||||||
findViewById(R.id.btnJumpGroup).setOnClickListener(new View.OnClickListener() {
|
findViewById(R.id.btnJumpGroup).setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
SendAtUtil.jumpGroup("00000001");
|
SendAtUtil.jumpGroup("00000001");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
findViewById(R.id.btnCancel1).setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
SendAtUtil.toCancelLogin();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
findViewById(R.id.btnCancel2).setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
SendAtUtil.toLogout();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadHistory() {
|
|
||||||
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
|
||||||
setupAdapter(etIp, prefs.getString(KEY_IP_HISTORY, ""));
|
|
||||||
setupAdapter(etAccount, prefs.getString(KEY_ACCOUNT_HISTORY, ""));
|
|
||||||
setupAdapter(etPassword, prefs.getString(KEY_PASSWORD_HISTORY, ""));
|
|
||||||
setupAdapter(etGroupId, prefs.getString(KEY_GROUP_HISTORY, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupAdapter(AutoCompleteTextView view, String historyStr) {
|
|
||||||
List<String> items = new ArrayList<>();
|
|
||||||
if (!historyStr.isEmpty()) {
|
|
||||||
items.addAll(Arrays.asList(historyStr.split(SEPARATOR)));
|
|
||||||
}
|
|
||||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
|
|
||||||
android.R.layout.simple_dropdown_item_1line, items);
|
|
||||||
view.setAdapter(adapter);
|
|
||||||
view.setThreshold(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveToHistory(String ip, String account, String password, String groupId) {
|
|
||||||
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
|
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
|
||||||
editor.putString(KEY_IP_HISTORY, addToHistory(prefs.getString(KEY_IP_HISTORY, ""), ip));
|
|
||||||
editor.putString(KEY_ACCOUNT_HISTORY, addToHistory(prefs.getString(KEY_ACCOUNT_HISTORY, ""), account));
|
|
||||||
editor.putString(KEY_PASSWORD_HISTORY, addToHistory(prefs.getString(KEY_PASSWORD_HISTORY, ""), password));
|
|
||||||
editor.putString(KEY_GROUP_HISTORY, addToHistory(prefs.getString(KEY_GROUP_HISTORY, ""), groupId));
|
|
||||||
editor.apply();
|
|
||||||
loadHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String addToHistory(String existing, String newValue) {
|
|
||||||
LinkedHashSet<String> set = new LinkedHashSet<>();
|
|
||||||
if (!existing.isEmpty()) {
|
|
||||||
set.addAll(Arrays.asList(existing.split(SEPARATOR)));
|
|
||||||
}
|
|
||||||
set.remove(newValue);
|
|
||||||
set.add(newValue);
|
|
||||||
return String.join(SEPARATOR, set);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import android.media.AudioFormat;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioRecord;
|
import android.media.AudioRecord;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
import timber.log.Timber;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -15,9 +15,8 @@ public class pttNative {
|
||||||
static {
|
static {
|
||||||
try {
|
try {
|
||||||
System.loadLibrary("ptt");
|
System.loadLibrary("ptt");
|
||||||
Timber.tag(TAG).d("Native library 'ptt' loaded successfully");
|
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e(e, "Failed to load ptt library");
|
Log.e(TAG, "Failed to load ptt library: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,8 +36,8 @@ public class pttNative {
|
||||||
//Callback Function for Recv the message from POC Lib
|
//Callback Function for Recv the message from POC Lib
|
||||||
public void OEM_AT_cb(String sData)
|
public void OEM_AT_cb(String sData)
|
||||||
{
|
{
|
||||||
Timber.tag(TAG).d("Poc2 AT Callback: %s", sData);
|
Log.e(TAG,"Poc2 AT Callback: " + sData);
|
||||||
int len, i, j=0;
|
int len, i;
|
||||||
char[] bytes = sData.toCharArray();
|
char[] bytes = sData.toCharArray();
|
||||||
|
|
||||||
List<String> lisHex = new ArrayList<>();
|
List<String> lisHex = new ArrayList<>();
|
||||||
|
|
@ -51,15 +50,12 @@ public class pttNative {
|
||||||
lisHex.add(temp.length() == 1 ? "0" + temp : temp);
|
lisHex.add(temp.length() == 1 ? "0" + temp : temp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Timber.tag(TAG).w("OEM_AT_cb: unexpected format, len=%d, data=%s", bytes.length, sData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CallBackResolution.at_OEM_AT_cb(lisHex);
|
CallBackResolution.at_OEM_AT_cb(lisHex);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OEM_Play_cb(byte[] bytes){
|
public void OEM_Play_cb(byte[] bytes){
|
||||||
Timber.tag(TAG).d("OEM_Play_cb: %d bytes", bytes.length);
|
|
||||||
if (mAudioTrack == null) {
|
if (mAudioTrack == null) {
|
||||||
initAudioTrack();
|
initAudioTrack();
|
||||||
}
|
}
|
||||||
|
|
@ -72,27 +68,27 @@ public class pttNative {
|
||||||
|
|
||||||
public void OEM_Play_Start_cb()
|
public void OEM_Play_Start_cb()
|
||||||
{
|
{
|
||||||
Timber.tag(TAG).e("Play Start!!!!");
|
Log.e("Play Start!!!!\r\n","");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OEM_Play_Stop_cb()
|
public void OEM_Play_Stop_cb()
|
||||||
{
|
{
|
||||||
Timber.tag(TAG).e("Play Stop!!!!");
|
Log.e("Play Stop!!!!\r\n","");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OEM_Record_Start_cb()
|
public void OEM_Record_Start_cb()
|
||||||
{
|
{
|
||||||
Timber.tag(TAG).e("Record Start!!!!");
|
Log.e("Record Start!!!!\r\n","");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OEM_Record_Stop_cb()
|
public void OEM_Record_Stop_cb()
|
||||||
{
|
{
|
||||||
Timber.tag(TAG).e("Record Stop!!!!");
|
Log.e("Record Stop!!!!\r\n","");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OEM_Play_TTS_cb(String sData)
|
public void OEM_Play_TTS_cb(String sData)
|
||||||
{
|
{
|
||||||
Timber.tag(TAG).e("Play TTS " + sData);
|
Log.e(TAG , "Play TTS " + sData);
|
||||||
CallBackResolution.at_Play_TTS_cb(sData);
|
CallBackResolution.at_Play_TTS_cb(sData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,17 +112,16 @@ public class pttNative {
|
||||||
private native int OEM_Set_Package_Name(String name);
|
private native int OEM_Set_Package_Name(String name);
|
||||||
|
|
||||||
public void pttNativeSendRecordCmd(byte[] data) {
|
public void pttNativeSendRecordCmd(byte[] data) {
|
||||||
Timber.tag(TAG).d("pttNativeSendRecordCmd: %d bytes", data.length);
|
|
||||||
OEM_Push_Record_Data(data);
|
OEM_Push_Record_Data(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void pttNativeSendAtCmd(String strAtCmd)
|
public void pttNativeSendAtCmd(String strAtCmd)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
Timber.tag(TAG).i("AT Send: %s", strAtCmd);
|
Log.i(TAG,"strAtCmd " + strAtCmd);
|
||||||
OEM_AT_Send(strAtCmd);
|
OEM_AT_Send(strAtCmd);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e( "Native AT Send failed: " + e.getMessage());
|
Log.e(TAG, "Native AT Send failed: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,7 +130,7 @@ public class pttNative {
|
||||||
try {
|
try {
|
||||||
OEM_Poc_Task_Start();
|
OEM_Poc_Task_Start();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e(e, "Native PocTaskStart failed");
|
Log.e(TAG, "Native PocTaskStart failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -144,7 +139,6 @@ public class pttNative {
|
||||||
try {
|
try {
|
||||||
return OEM_Set_AT_CallBack("OEM_AT_cb");
|
return OEM_Set_AT_CallBack("OEM_AT_cb");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e(e, "Failed to set AT callback");
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +148,6 @@ public class pttNative {
|
||||||
try {
|
try {
|
||||||
return OEM_Set_Play_CallBack("OEM_Play_cb");
|
return OEM_Set_Play_CallBack("OEM_Play_cb");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e(e, "Failed to set Play callback");
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,7 +157,6 @@ public class pttNative {
|
||||||
try {
|
try {
|
||||||
return OEM_Set_Play_Start_Callback("OEM_Play_Start_cb");
|
return OEM_Set_Play_Start_Callback("OEM_Play_Start_cb");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e(e, "Failed to set PlayStart callback");
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -174,7 +166,6 @@ public class pttNative {
|
||||||
try {
|
try {
|
||||||
return OEM_Set_Play_Stop_Callback("OEM_Play_Stop_cb");
|
return OEM_Set_Play_Stop_Callback("OEM_Play_Stop_cb");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e(e, "Failed to set PlayStop callback");
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +175,6 @@ public class pttNative {
|
||||||
try {
|
try {
|
||||||
return OEM_Set_Record_Start_Callback("OEM_Record_Start_cb");
|
return OEM_Set_Record_Start_Callback("OEM_Record_Start_cb");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e(e, "Failed to set RecordStart callback");
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -194,7 +184,6 @@ public class pttNative {
|
||||||
try {
|
try {
|
||||||
return OEM_Set_Record_Stop_Callback("OEM_Record_Stop_cb");
|
return OEM_Set_Record_Stop_Callback("OEM_Record_Stop_cb");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e(e, "Failed to set RecordStop callback");
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +193,6 @@ public class pttNative {
|
||||||
try {
|
try {
|
||||||
return OEM_Set_TTS_Callback("OEM_Play_TTS_cb");
|
return OEM_Set_TTS_Callback("OEM_Play_TTS_cb");
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e(e, "Failed to set TTS callback");
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -214,25 +202,22 @@ public class pttNative {
|
||||||
try {
|
try {
|
||||||
return OEM_Set_Package_Name(name);
|
return OEM_Set_Package_Name(name);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Timber.tag(TAG).e(e, "Failed to set package callback");
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public pttNative() {
|
public pttNative() {
|
||||||
Timber.tag(TAG).d("pttNative instance created");
|
|
||||||
initAudioTrack();
|
initAudioTrack();
|
||||||
}
|
}
|
||||||
|
|
||||||
private AudioTrack mAudioTrack;
|
private AudioTrack mAudioTrack;
|
||||||
public void initAudioTrack() {
|
public void initAudioTrack() {
|
||||||
int mAudioMinBufSize = AudioTrack.getMinBufferSize(8000, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT);
|
int mAudioMinBufSize = AudioTrack.getMinBufferSize(8000, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT);
|
||||||
Timber.tag(TAG).d("AudioTrack init: minBufSize=%d", mAudioMinBufSize);
|
|
||||||
|
|
||||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||||
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) // 通信场景
|
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) // 内容类型
|
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||||
.setLegacyStreamType(AudioManager.STREAM_MUSIC) // 设置通话流类型
|
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
AudioFormat audioFormat = new AudioFormat.Builder()
|
AudioFormat audioFormat = new AudioFormat.Builder()
|
||||||
|
|
@ -248,7 +233,7 @@ public class pttNative {
|
||||||
AudioTrack.MODE_STREAM,
|
AudioTrack.MODE_STREAM,
|
||||||
AudioManager.AUDIO_SESSION_ID_GENERATE
|
AudioManager.AUDIO_SESSION_ID_GENERATE
|
||||||
);
|
);
|
||||||
mAudioTrack.setVolume(1.0f);// 设置音量
|
mAudioTrack.setVolume(1.0f);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -260,7 +245,7 @@ public class pttNative {
|
||||||
}
|
}
|
||||||
mAudioTrack.release();
|
mAudioTrack.release();
|
||||||
mAudioTrack = null;
|
mAudioTrack = null;
|
||||||
} catch (Exception e) { Timber.tag(TAG).e(e, "AudioTrack destroy failed"); }
|
} catch (Exception e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
"pttSpeakPlay" -> { pttSpeakPlay(); "" }
|
"pttSpeakPlay" -> { pttSpeakPlay(); "" }
|
||||||
"pttGetLoginStatus" -> pttGetLoginStatus()
|
"pttGetLoginStatus" -> pttGetLoginStatus()
|
||||||
"pttGetLoginId" -> pttGetLoginId()
|
"pttGetLoginId" -> pttGetLoginId()
|
||||||
"pttGetCurrentGroup" -> pttGetCurrentGroup()
|
|
||||||
"pttSpeakRelease" -> { pttSpeakRelease(); "" }
|
"pttSpeakRelease" -> { pttSpeakRelease(); "" }
|
||||||
"pttForceRelease" -> { pttForceRelease(); "" }
|
"pttForceRelease" -> { pttForceRelease(); "" }
|
||||||
"pttGetGroupInfo" -> { pttGetGroupInfo(); "" }
|
"pttGetGroupInfo" -> { pttGetGroupInfo(); "" }
|
||||||
|
|
@ -81,10 +80,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
"getMessages" -> getMessages()
|
"getMessages" -> getMessages()
|
||||||
"clearMessages" -> { clearMessages(); "" }
|
"clearMessages" -> { clearMessages(); "" }
|
||||||
"onPageFinished" -> { onPageFinished(); "" }
|
"onPageFinished" -> { onPageFinished(); "" }
|
||||||
"showRestartDialog" -> { showRestartDialog(args.optString(0, ""), args.optString(1, "提示")); "" }
|
|
||||||
"showJsonDialog" -> { showJsonDialog(args.optString(0, "")); "" }
|
|
||||||
"getTempCallStatus" -> getTempCallStatus()
|
|
||||||
"maximizeTempCall" -> { maximizeTempCall(); "" }
|
|
||||||
else -> {
|
else -> {
|
||||||
Timber.tag("Bridge").w("Unknown method: $method")
|
Timber.tag("Bridge").w("Unknown method: $method")
|
||||||
""
|
""
|
||||||
|
|
@ -107,8 +102,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
SendAtUtil.speakPlay()
|
SendAtUtil.speakPlay()
|
||||||
Timber.tag("PTT").d("Speak Play")
|
Timber.tag("PTT").d("Speak Play")
|
||||||
MainActivity.executeJs("updatePttTalkStatus(true)")
|
MainActivity.executeJs("updatePttTalkStatus(true)")
|
||||||
} else {
|
|
||||||
Timber.tag("PTT").w("Speak Play ignored: already speaking")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,20 +113,7 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
return com.example.kingway.ptt.CallBackResolution.getPttLoginId()
|
return com.example.kingway.ptt.CallBackResolution.getPttLoginId()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pttGetCurrentGroup(): String {
|
|
||||||
return try {
|
|
||||||
val json = JSONObject()
|
|
||||||
json.put("groupId", com.example.kingway.ptt.CallBackResolution.getCurrentGroupId())
|
|
||||||
json.put("groupName", com.example.kingway.ptt.CallBackResolution.getCurrentGroupName())
|
|
||||||
json.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("PTT").e(e, "pttGetCurrentGroup failed")
|
|
||||||
"{\"groupId\":\"\",\"groupName\":\"\"}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pttSpeakRelease() {
|
fun pttSpeakRelease() {
|
||||||
Timber.tag("PTT").d("pttSpeakRelease called, isSpeaking=%s", isSpeaking)
|
|
||||||
if (isSpeaking) {
|
if (isSpeaking) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val duration = now - lastSpeakTime
|
val duration = now - lastSpeakTime
|
||||||
|
|
@ -156,42 +136,34 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
SendAtUtil.speakRelease()
|
SendAtUtil.speakRelease()
|
||||||
Timber.tag("PTT").d("Real Speak Release")
|
Timber.tag("PTT").d("Real Speak Release")
|
||||||
MainActivity.executeJs("updatePttTalkStatus(false)")
|
MainActivity.executeJs("updatePttTalkStatus(false)")
|
||||||
} else {
|
|
||||||
Timber.tag("PTT").w("realRelease skipped: already released")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pttForceRelease() {
|
fun pttForceRelease() {
|
||||||
val wasSpeaking = isSpeaking
|
|
||||||
isSpeaking = false
|
isSpeaking = false
|
||||||
SendAtUtil.speakRelease()
|
SendAtUtil.speakRelease()
|
||||||
Timber.tag("PTT").d("Force Release (wasSpeaking=%s)", wasSpeaking)
|
Timber.tag("PTT").d("Force Release")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pttGetGroupInfo() {
|
fun pttGetGroupInfo() {
|
||||||
Timber.tag("PTT").d("pttGetGroupInfo called")
|
|
||||||
SendAtUtil.getGroupInfo()
|
SendAtUtil.getGroupInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pttGetGroupMemberInfo(gid: String) {
|
fun pttGetGroupMemberInfo(gid: String) {
|
||||||
Timber.tag("PTT").d("pttGetGroupMemberInfo: gid=%s", gid)
|
|
||||||
SendAtUtil.getGroupMemberInfo(gid)
|
SendAtUtil.getGroupMemberInfo(gid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun simulatePttConflict() {
|
fun simulatePttConflict() {
|
||||||
Timber.tag("PTT").w("simulatePttConflict: showing conflict dialog via JS bridge")
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
MainActivity.showPttConflictDialog()
|
MainActivity.showPttConflictDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pttSendAtCmd(cmd: String) {
|
fun pttSendAtCmd(cmd: String) {
|
||||||
Timber.tag("PTT").d("pttSendAtCmd: %s", cmd)
|
|
||||||
SendAtUtil.sendRawCmd(cmd)
|
SendAtUtil.sendRawCmd(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pttJumpGroup(gid: String) {
|
fun pttJumpGroup(gid: String) {
|
||||||
Timber.tag("PTT").d("pttJumpGroup: gid=%s", gid)
|
|
||||||
SendAtUtil.jumpGroup(gid)
|
SendAtUtil.jumpGroup(gid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,10 +241,10 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
|
|
||||||
fun cutPaper(allCut: Boolean) {
|
fun cutPaper(allCut: Boolean) {
|
||||||
try {
|
try {
|
||||||
if (allCut){
|
PrinterManager.getFactory()?.let {
|
||||||
PrinterManager.getFactory()?.PaperAllCut();
|
if (it.isConnection) {
|
||||||
}else{
|
if (allCut) it.PaperAllCut() else it.PaperCut()
|
||||||
PrinterManager.getFactory()?.PaperCut();
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Timber.tag("JS_Interface").e(e, "cutPaper failed")
|
Timber.tag("JS_Interface").e(e, "cutPaper failed")
|
||||||
|
|
@ -299,12 +271,12 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
val zipPath = LogManager.zipLogs(activity)
|
val zipPath = LogManager.zipLogs(activity)
|
||||||
if (zipPath != null) {
|
if (zipPath != null) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Toast.makeText(activity.applicationContext, "日志已打包: $zipPath", Toast.LENGTH_LONG).show()
|
Toast.makeText(activity, "日志已打包: $zipPath", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
return zipPath
|
return zipPath
|
||||||
} else {
|
} else {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Toast.makeText(activity.applicationContext, "暂无日志或打包失败", Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity, "暂无日志或打包失败", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -313,7 +285,7 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
fun uploadLogs() {
|
fun uploadLogs() {
|
||||||
LogManager.uploadLogs(activity) { success, message ->
|
LogManager.uploadLogs(activity) { success, message ->
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Toast.makeText(activity.applicationContext, if (success) "日志上传成功" else "上传失败: $message", Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity, if (success) "日志上传成功" else "上传失败: $message", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -324,7 +296,7 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
*/
|
*/
|
||||||
fun printPage() {
|
fun printPage() {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Toast.makeText(activity.applicationContext, "GeckoView 暂不支持系统打印,请使用热敏打印功能", Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity, "GeckoView 暂不支持系统打印,请使用热敏打印功能", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,7 +306,7 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
Timber.tag("Print").d("收到 H5 原始打印数据: $data")
|
Timber.tag("Print").d("收到 H5 原始打印数据: $data")
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
try {
|
try {
|
||||||
Toast.makeText(activity.applicationContext, "H5指令打印: $data", Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity, "H5指令打印: $data", Toast.LENGTH_SHORT).show()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("Print").e(e, "Toast显示失败")
|
Timber.tag("Print").e(e, "Toast显示失败")
|
||||||
}
|
}
|
||||||
|
|
@ -370,7 +342,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
|
val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
|
||||||
packageInfo.versionName ?: "1.0.0"
|
packageInfo.versionName ?: "1.0.0"
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("AndroidInterface").e(e, "getVersionName failed")
|
|
||||||
"1.0.0"
|
"1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -396,7 +367,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
|
|
||||||
json.toString()
|
json.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("AndroidInterface").e(e, "getAboutInfo failed")
|
|
||||||
"{ \"appName\": \"消防值守台\", \"versionName\": \"1.0.0\" }"
|
"{ \"appName\": \"消防值守台\", \"versionName\": \"1.0.0\" }"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -419,7 +389,6 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE)
|
val prefs = activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE)
|
||||||
prefs.getString("history", "") ?: ""
|
prefs.getString("history", "") ?: ""
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("Storage").e(e, "getMessages failed")
|
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -428,7 +397,7 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
try {
|
try {
|
||||||
activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE).edit().clear().apply()
|
activity.getSharedPreferences("msg_records", Context.MODE_PRIVATE).edit().clear().apply()
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Toast.makeText(activity.applicationContext, "记录已清空", Toast.LENGTH_SHORT).show()
|
Toast.makeText(activity, "记录已清空", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("Storage").e(e, "清空记录异常")
|
Timber.tag("Storage").e(e, "清空记录异常")
|
||||||
|
|
@ -440,67 +409,9 @@ class AndroidInterface(private val activity: MainActivity) {
|
||||||
try {
|
try {
|
||||||
activity.cancelTimeoutTimer()
|
activity.cancelTimeoutTimer()
|
||||||
activity.findViewById<android.view.View>(R.id.loading_layout)?.visibility = android.view.View.GONE
|
activity.findViewById<android.view.View>(R.id.loading_layout)?.visibility = android.view.View.GONE
|
||||||
|
|
||||||
// 正式界面模式下,页面加载完成后传递登录数据给前端
|
|
||||||
val isOfficial = activity.isOfficialMode()
|
|
||||||
val data = activity.getLoginData()
|
|
||||||
Timber.tag("AndroidInterface").d("onPageFinished: isOfficial=$isOfficial, data=$data")
|
|
||||||
if (isOfficial && data.isNotEmpty()) {
|
|
||||||
val escaped = data.replace("'", "\\'").replace("\n", "\\n")
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.appLogin){window.appLogin('$escaped');}")
|
|
||||||
Timber.tag("AndroidInterface").d("Called appLogin")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("UI").e(e, "隐藏加载布局失败")
|
Timber.tag("UI").e(e, "隐藏加载布局失败")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showRestartDialog(message: String, title: String) {
|
|
||||||
Timber.tag("AndroidInterface").w("showRestartDialog from JS: title=%s msg=%s", title, message)
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
com.stand.standapp.MainActivity.showPttUpdateDialog(message, title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showJsonDialog(jsonStr: String) {
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
try {
|
|
||||||
val formatted = try {
|
|
||||||
val obj = org.json.JSONObject(jsonStr)
|
|
||||||
obj.toString(2)
|
|
||||||
} catch (_: Exception) {
|
|
||||||
jsonStr
|
|
||||||
}
|
|
||||||
androidx.appcompat.app.AlertDialog.Builder(activity)
|
|
||||||
.setTitle("JSON 数据")
|
|
||||||
.setMessage(formatted)
|
|
||||||
.setPositiveButton("确认", null)
|
|
||||||
.show()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("UI").e(e, "showJsonDialog error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTempCallStatus(): String {
|
|
||||||
return try {
|
|
||||||
val json = JSONObject()
|
|
||||||
json.put("isOpen", TempCallActivity.isOpen())
|
|
||||||
json.put("isMinimized", TempCallActivity.isMinimizedState())
|
|
||||||
json.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
"{\"isOpen\":false,\"isMinimized\":false}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun maximizeTempCall() {
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
try {
|
|
||||||
TempCallActivity.maximize()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("TempCall").e(e, "maximizeTempCall error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ object AppConfig {
|
||||||
private const val KEY_REMEMBER_LOGIN = "remember_login"
|
private const val KEY_REMEMBER_LOGIN = "remember_login"
|
||||||
private const val KEY_USERNAME = "saved_username"
|
private const val KEY_USERNAME = "saved_username"
|
||||||
private const val KEY_PASSWORD = "saved_password"
|
private const val KEY_PASSWORD = "saved_password"
|
||||||
private const val KEY_ACCESS_TOKEN = "access_token"
|
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
const val DEFAULT_SERVER_URL = "https://template.gyouzhe.com"
|
const val DEFAULT_SERVER_URL = "https://template.gyouzhe.com"
|
||||||
|
|
@ -69,12 +68,4 @@ object AppConfig {
|
||||||
fun setSavedPassword(context: Context, password: String) {
|
fun setSavedPassword(context: Context, password: String) {
|
||||||
getPrefs(context).edit().putString(KEY_PASSWORD, password).apply()
|
getPrefs(context).edit().putString(KEY_PASSWORD, password).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAccessToken(context: Context): String {
|
|
||||||
return getPrefs(context).getString(KEY_ACCESS_TOKEN, "") ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAccessToken(context: Context, token: String) {
|
|
||||||
getPrefs(context).edit().putString(KEY_ACCESS_TOKEN, token).apply()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package com.stand.standapp
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Process
|
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
|
@ -32,7 +31,6 @@ class LoginActivity : AppCompatActivity() {
|
||||||
val etPassword = findViewById<EditText>(R.id.et_password)
|
val etPassword = findViewById<EditText>(R.id.et_password)
|
||||||
val cbRemember = findViewById<CheckBox>(R.id.cb_remember)
|
val cbRemember = findViewById<CheckBox>(R.id.cb_remember)
|
||||||
val btnLogin = findViewById<Button>(R.id.btn_login)
|
val btnLogin = findViewById<Button>(R.id.btn_login)
|
||||||
val btnLoginOfficial = findViewById<Button>(R.id.btn_login_official)
|
|
||||||
val btnExit = findViewById<Button>(R.id.btn_exit)
|
val btnExit = findViewById<Button>(R.id.btn_exit)
|
||||||
val ivSettings = findViewById<ImageView>(R.id.iv_settings)
|
val ivSettings = findViewById<ImageView>(R.id.iv_settings)
|
||||||
val tvCopyright = findViewById<TextView>(R.id.tv_copyright)
|
val tvCopyright = findViewById<TextView>(R.id.tv_copyright)
|
||||||
|
|
@ -56,30 +54,18 @@ class LoginActivity : AppCompatActivity() {
|
||||||
val password = etPassword.text.toString().trim()
|
val password = etPassword.text.toString().trim()
|
||||||
|
|
||||||
if (username.isEmpty() || password.isEmpty()) {
|
if (username.isEmpty() || password.isEmpty()) {
|
||||||
Toast.makeText(applicationContext, "请输入账号和密码", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "请输入账号和密码", Toast.LENGTH_SHORT).show()
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行真实接口登录
|
// 执行真实接口登录
|
||||||
performLogin(username, password, cbRemember.isChecked, false)
|
performLogin(username, password, cbRemember.isChecked)
|
||||||
}
|
|
||||||
|
|
||||||
// 3.1 正式界面登录按钮点击事件
|
|
||||||
btnLoginOfficial.setOnClickListener {
|
|
||||||
val username = etUsername.text.toString().trim()
|
|
||||||
val password = etPassword.text.toString().trim()
|
|
||||||
|
|
||||||
if (username.isEmpty() || password.isEmpty()) {
|
|
||||||
Toast.makeText(applicationContext, "请输入账号和密码", Toast.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
performLogin(username, password, cbRemember.isChecked, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 退出按钮
|
// 4. 退出按钮
|
||||||
btnExit.setOnClickListener {
|
btnExit.setOnClickListener {
|
||||||
com.stand.standapp.utils.AppKiller.killApp(this)
|
pttLogoutAndDestroy()
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 彩蛋:点击底部版权 10 次进入开发者模式
|
// 5. 彩蛋:点击底部版权 10 次进入开发者模式
|
||||||
|
|
@ -97,16 +83,15 @@ class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
if (clickCount >= 10) {
|
if (clickCount >= 10) {
|
||||||
clickCount = 0
|
clickCount = 0
|
||||||
Toast.makeText(applicationContext, "开发者模式已激活", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "开发者模式已激活", Toast.LENGTH_SHORT).show()
|
||||||
startActivity(Intent(this, DeveloperActivity::class.java))
|
startActivity(Intent(this, DeveloperActivity::class.java))
|
||||||
} else if (clickCount > 5) {
|
} else if (clickCount > 5) {
|
||||||
Toast.makeText(applicationContext, "再点击 ${10 - clickCount} 次进入调试模式", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "再点击 ${10 - clickCount} 次进入调试模式", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performLogin(username: String, password: String, remember: Boolean, officialMode: Boolean) {
|
private fun performLogin(username: String, password: String, remember: Boolean) {
|
||||||
Timber.tag("Login").d("开始登录: username=%s, server=%s", username, AppConfig.getServerUrl(this))
|
|
||||||
val serverUrl = AppConfig.getServerUrl(this)
|
val serverUrl = AppConfig.getServerUrl(this)
|
||||||
val loginUrl = "$serverUrl/api/auth/login"
|
val loginUrl = "$serverUrl/api/auth/login"
|
||||||
|
|
||||||
|
|
@ -129,10 +114,9 @@ class LoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
Timber.tag("Login").e(e, "登录网络请求失败")
|
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
loadingDialog.dismiss()
|
loadingDialog.dismiss()
|
||||||
Toast.makeText(applicationContext, "网络异常: ${e.message}", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@LoginActivity, "网络异常: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,47 +137,49 @@ class LoginActivity : AppCompatActivity() {
|
||||||
AppConfig.setSavedPassword(this@LoginActivity, "")
|
AppConfig.setSavedPassword(this@LoginActivity, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取登录数据
|
Toast.makeText(this@LoginActivity, "登录成功", Toast.LENGTH_SHORT).show()
|
||||||
val data = json.optJSONObject("data")
|
|
||||||
val loginData = data?.toString() ?: "{}"
|
|
||||||
val accessToken = data?.optString("access_token", "") ?: ""
|
|
||||||
AppConfig.setAccessToken(this@LoginActivity, accessToken)
|
|
||||||
|
|
||||||
Toast.makeText(applicationContext, "登录成功", Toast.LENGTH_SHORT).show()
|
// 1. PTT 引擎初始化并异步登录 (避免阻塞 UI)
|
||||||
|
try {
|
||||||
// 获取独立的 PTT 账号并初始化
|
com.example.kingway.ptt.MyApplication.init(this@LoginActivity)
|
||||||
Timber.tag("PTT").d("开始PTT初始化流程: username=%s, hasToken=%b", username, accessToken.isNotEmpty())
|
com.example.kingway.ptt.SendAtUtil.toLogout()
|
||||||
initPttWithAccount(username, accessToken)
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("PTT").e(e, "PTT init error")
|
||||||
|
}
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
Thread.sleep(500)
|
||||||
|
runOnUiThread {
|
||||||
|
Toast.makeText(this@LoginActivity, "PTT初始化中", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
Thread.sleep(1500)
|
||||||
|
com.example.kingway.ptt.SendAtUtil.toLogin("gzzdweb01", "123456", "61.243.1.123", 1, 1)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("PTT").e(e, "PTT login error")
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
|
||||||
// 2. 启动硬件 GPIO 监听
|
// 2. 启动硬件 GPIO 监听
|
||||||
try {
|
try {
|
||||||
com.stand.standapp.utils.GpioManager.init(this@LoginActivity)
|
com.stand.standapp.utils.GpioManager.init(this@LoginActivity)
|
||||||
com.stand.standapp.utils.GpioManager.startListening()
|
com.stand.standapp.utils.GpioManager.startListening()
|
||||||
Timber.tag("GPIO").d("GPIO初始化并启动监听成功")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("GPIO").e(e, "GPIO init error")
|
Timber.tag("GPIO").e(e, "GPIO init error")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 初始化打印机 (异步)
|
// 3. 初始化打印机 (异步)
|
||||||
PrinterManager.init(this@LoginActivity)
|
PrinterManager.init(this@LoginActivity)
|
||||||
|
|
||||||
// 4. 检查是否有临时群组,有则打开临时呼叫页面
|
startActivity(Intent(this@LoginActivity, MainActivity::class.java))
|
||||||
checkTempGroup(username, data)
|
|
||||||
|
|
||||||
// 5. 启动 MainActivity,传递登录模式和数据
|
|
||||||
val intent = Intent(this@LoginActivity, MainActivity::class.java)
|
|
||||||
intent.putExtra("official_mode", officialMode)
|
|
||||||
intent.putExtra("login_data", loginData)
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
val msg = json.optString("msg", "登录失败")
|
val msg = json.optString("msg", "登录失败")
|
||||||
Toast.makeText(applicationContext, msg, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@LoginActivity, msg, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("Login").e(e, "解析响应失败")
|
Timber.tag("Login").e(e, "解析响应失败")
|
||||||
Toast.makeText(applicationContext, "服务器响应异常", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@LoginActivity, "服务器响应异常", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +203,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
btnReset.setOnClickListener {
|
btnReset.setOnClickListener {
|
||||||
etServerUrl.setText(AppConfig.DEFAULT_SERVER_URL)
|
etServerUrl.setText(AppConfig.DEFAULT_SERVER_URL)
|
||||||
etSerialPort.setText(AppConfig.DEFAULT_SERIAL_PORT)
|
etSerialPort.setText(AppConfig.DEFAULT_SERIAL_PORT)
|
||||||
Toast.makeText(applicationContext, "已重置数值 (待保存)", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "已重置数值 (待保存)", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogView.findViewById<TextView>(R.id.btn_cancel).setOnClickListener {
|
dialogView.findViewById<TextView>(R.id.btn_cancel).setOnClickListener {
|
||||||
|
|
@ -236,7 +222,7 @@ class LoginActivity : AppCompatActivity() {
|
||||||
AppConfig.setServerUrl(this, serverUrl)
|
AppConfig.setServerUrl(this, serverUrl)
|
||||||
AppConfig.setSerialPort(this, serialPort)
|
AppConfig.setSerialPort(this, serialPort)
|
||||||
|
|
||||||
Toast.makeText(applicationContext, "参数已生效", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, "参数已生效", Toast.LENGTH_SHORT).show()
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,135 +240,25 @@ class LoginActivity : AppCompatActivity() {
|
||||||
.setTitle("提示")
|
.setTitle("提示")
|
||||||
.setMessage("确定要退出应用吗?")
|
.setMessage("确定要退出应用吗?")
|
||||||
.setPositiveButton("确定") { _, _ ->
|
.setPositiveButton("确定") { _, _ ->
|
||||||
com.stand.standapp.utils.AppKiller.killApp(this)
|
pttLogoutAndDestroy()
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
.setNegativeButton("取消", null)
|
.setNegativeButton("取消", null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkTempGroup(account: String, loginData: org.json.JSONObject?) {
|
private fun pttLogoutAndDestroy() {
|
||||||
val accessToken = loginData?.optString("access_token", "") ?: return
|
|
||||||
if (accessToken.isEmpty()) return
|
|
||||||
|
|
||||||
val serverUrl = AppConfig.getServerUrl(this)
|
|
||||||
val url = "$serverUrl/api/external/ptt/user/groups?account=$account"
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.addHeader("Authorization", "Bearer $accessToken")
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
|
||||||
Timber.tag("TempGroup").w(e, "检查临时群组失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
|
||||||
try {
|
|
||||||
val body = response.body?.string() ?: return
|
|
||||||
val json = JSONObject(body)
|
|
||||||
val dataObj = json.optJSONObject("data")
|
|
||||||
val list = dataObj?.optJSONArray("data") ?: return
|
|
||||||
|
|
||||||
for (i in 0 until list.length()) {
|
|
||||||
val group = list.optJSONObject(i) ?: continue
|
|
||||||
val gtype = group.optString("gtype", "")
|
|
||||||
if (gtype == "2") {
|
|
||||||
val gname = group.optString("gname", "临时群组")
|
|
||||||
Timber.tag("TempGroup").d("发现临时群组: $gname")
|
|
||||||
// 打开临时呼叫页面
|
|
||||||
val appCtx = com.stand.standapp.MyApplication.getAppInstance()
|
|
||||||
val intent = android.content.Intent(
|
|
||||||
appCtx,
|
|
||||||
TempCallActivity::class.java
|
|
||||||
)
|
|
||||||
intent.putExtra("group_name", gname)
|
|
||||||
intent.addFlags(
|
|
||||||
android.content.Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
||||||
android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK or
|
|
||||||
android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
|
||||||
)
|
|
||||||
appCtx.startActivity(intent)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("TempGroup").e(e, "解析临时群组响应失败")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initPttWithAccount(username: String, accessToken: String) {
|
|
||||||
if (accessToken.isEmpty()) {
|
|
||||||
Timber.tag("PTT").w("accessToken为空,使用默认PTT密码: username=%s", username)
|
|
||||||
doPttInit(username, "123456")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val serverUrl = AppConfig.getServerUrl(this)
|
|
||||||
val url = "$serverUrl/api/poc-card/account/$username"
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(url)
|
|
||||||
.addHeader("Authorization", "Bearer $accessToken")
|
|
||||||
.get()
|
|
||||||
.build()
|
|
||||||
|
|
||||||
client.newCall(request).enqueue(object : Callback {
|
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
|
||||||
Timber.tag("PTT").w(e, "获取 PTT 账号失败,使用默认账号")
|
|
||||||
doPttInit(username, "123456")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
|
||||||
try {
|
|
||||||
val body = response.body?.string() ?: ""
|
|
||||||
val json = JSONObject(body)
|
|
||||||
if (json.optInt("code") == 0) {
|
|
||||||
val data = json.optJSONObject("data")
|
|
||||||
val pttUsername = data?.optString("cardNumber", username) ?: username
|
|
||||||
val pttPassword = data?.optString("pocPassword", "123456") ?: "123456"
|
|
||||||
Timber.tag("PTT").d("获取到服务器PTT账号: cardNumber=%s", pttUsername)
|
|
||||||
doPttInit(pttUsername, pttPassword)
|
|
||||||
} else {
|
|
||||||
Timber.tag("PTT").w("获取PTT账号失败, code=%d", json.optInt("code"))
|
|
||||||
doPttInit(username, "123456")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("PTT").e(e, "解析 PTT 账号失败")
|
|
||||||
doPttInit(username, "123456")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doPttInit(pttUsername: String, pttPassword: String) {
|
|
||||||
Timber.tag("PTT").d("PTT初始化开始: username=%s", pttUsername)
|
|
||||||
android.os.Handler(android.os.Looper.getMainLooper()).post {
|
|
||||||
Toast.makeText(applicationContext, "PTT初始化中...", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. PTT 引擎强制重置后登录 (销毁旧 session,确保干净状态)
|
|
||||||
try {
|
try {
|
||||||
Timber.tag("PTT").d("发送PTT取消登录指令")
|
|
||||||
com.example.kingway.ptt.SendAtUtil.toCancelLogin()
|
|
||||||
Timber.tag("PTT").d("发送PTT注销指令")
|
|
||||||
com.example.kingway.ptt.SendAtUtil.toLogout()
|
com.example.kingway.ptt.SendAtUtil.toLogout()
|
||||||
|
Timber.tag("LoginActivity").i("PTT logout sent")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("PTT").e(e, "PTT init error")
|
Timber.tag("LoginActivity").e(e, "PTT logout failed")
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
com.example.kingway.ptt.MyApplication.destroy()
|
||||||
|
Timber.tag("LoginActivity").i("PTT native destroyed")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("LoginActivity").e(e, "PTT destroy failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 Handler 替代硬编码 Thread.sleep,避免阻塞或随意开辟线程
|
|
||||||
Timber.tag("PTT").d("等待1500ms以确保旧session完全清理")
|
|
||||||
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
|
||||||
try {
|
|
||||||
Timber.tag("PTT").d("发送PTT登录指令: username=%s, ip=%s", pttUsername, "220.154.131.231")
|
|
||||||
com.example.kingway.ptt.SendAtUtil.toLogin(pttUsername, pttPassword, "220.154.131.231", 1, 1)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("PTT").e(e, "PTT login error")
|
|
||||||
}
|
|
||||||
}, 1500)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,11 @@ import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import android.widget.FrameLayout
|
|
||||||
import org.mozilla.geckoview.GeckoResult
|
import org.mozilla.geckoview.GeckoResult
|
||||||
import org.mozilla.geckoview.GeckoRuntime
|
import org.mozilla.geckoview.GeckoRuntime
|
||||||
import org.mozilla.geckoview.GeckoSession
|
import org.mozilla.geckoview.GeckoSession
|
||||||
import org.mozilla.geckoview.GeckoSessionSettings
|
import org.mozilla.geckoview.GeckoSessionSettings
|
||||||
import org.mozilla.geckoview.GeckoView
|
import org.mozilla.geckoview.GeckoView
|
||||||
import org.mozilla.geckoview.StorageController.ClearFlags
|
|
||||||
import org.mozilla.geckoview.WebRequestError
|
import org.mozilla.geckoview.WebRequestError
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
@ -19,21 +17,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
private var instance: MainActivity? = null
|
private var instance: MainActivity? = null
|
||||||
|
|
||||||
// 防止对话框堆叠
|
|
||||||
@Volatile
|
|
||||||
private var isConflictDialogShowing = false
|
|
||||||
|
|
||||||
// 待显示的弹窗(instance 为 null 时暂存)
|
|
||||||
@Volatile
|
|
||||||
private var pendingDialog: Pair<String, String>? = null
|
|
||||||
|
|
||||||
// 待执行的 JS 代码(由 bridge.js 轮询获取)
|
// 待执行的 JS 代码(由 bridge.js 轮询获取)
|
||||||
@Volatile
|
@Volatile
|
||||||
private var pendingJsCode: String? = null
|
private var pendingJsCode: String? = null
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun executeJs(script: String) {
|
fun executeJs(script: String) {
|
||||||
Timber.tag("ExecuteJs").d(script)
|
|
||||||
synchronized(pendingJsCode ?: Any()) {
|
synchronized(pendingJsCode ?: Any()) {
|
||||||
// 合并多次调用(用换行分隔)
|
// 合并多次调用(用换行分隔)
|
||||||
pendingJsCode = if (pendingJsCode != null) "$pendingJsCode;$script" else script
|
pendingJsCode = if (pendingJsCode != null) "$pendingJsCode;$script" else script
|
||||||
|
|
@ -49,7 +38,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
// 硬件按键触发
|
// 硬件按键触发
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun onHardwarePttDown() {
|
fun onHardwarePttDown() {
|
||||||
Timber.tag("MainActivity").d("onHardwarePttDown triggered")
|
|
||||||
instance?.runOnUiThread {
|
instance?.runOnUiThread {
|
||||||
instance?.pttInterface?.pttSpeakPlay()
|
instance?.pttInterface?.pttSpeakPlay()
|
||||||
executeJs("onHardwarePttDown()")
|
executeJs("onHardwarePttDown()")
|
||||||
|
|
@ -58,7 +46,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun onHardwarePttUp() {
|
fun onHardwarePttUp() {
|
||||||
Timber.tag("MainActivity").d("onHardwarePttUp triggered")
|
|
||||||
instance?.runOnUiThread {
|
instance?.runOnUiThread {
|
||||||
instance?.pttInterface?.pttSpeakRelease()
|
instance?.pttInterface?.pttSpeakRelease()
|
||||||
executeJs("onHardwarePttUp()")
|
executeJs("onHardwarePttUp()")
|
||||||
|
|
@ -67,55 +54,28 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun showPttConflictDialog() {
|
fun showPttConflictDialog() {
|
||||||
Timber.tag("PTT").w("showPttConflictDialog: account conflict detected")
|
|
||||||
if (isConflictDialogShowing) return
|
|
||||||
isConflictDialogShowing = true
|
|
||||||
instance?.runOnUiThread {
|
instance?.runOnUiThread {
|
||||||
val ctx = instance ?: return@runOnUiThread
|
val ctx = instance ?: return@runOnUiThread
|
||||||
androidx.appcompat.app.AlertDialog.Builder(ctx)
|
androidx.appcompat.app.AlertDialog.Builder(ctx)
|
||||||
.setTitle("账号冲突")
|
.setTitle("账号冲突")
|
||||||
.setMessage("您的 PTT 账号已在其他设备登录,当前设备已被迫下线。\n请点击确认重启应用。")
|
.setMessage("您的 PTT 账号已在其他设备登录,当前设备已被迫下线。\n请点击确认退出并重新登录。")
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton("确认") { _, _ ->
|
.setPositiveButton("确认") { _, _ ->
|
||||||
com.stand.standapp.utils.AppKiller.killApp(ctx, restart = true)
|
try {
|
||||||
|
com.example.kingway.ptt.SendAtUtil.toLogout()
|
||||||
|
com.example.kingway.ptt.MyApplication.destroy()
|
||||||
|
com.stand.standapp.utils.GpioManager.stopListening()
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
val pm = ctx.packageManager
|
||||||
|
val intent = pm.getLaunchIntentForPackage(ctx.packageName)
|
||||||
|
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
ctx.startActivity(intent)
|
||||||
|
android.os.Process.killProcess(android.os.Process.myPid())
|
||||||
}
|
}
|
||||||
.setOnDismissListener { isConflictDialogShowing = false }
|
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun showPttUpdateDialog(msg: String, title: String) {
|
|
||||||
Timber.tag("PTT").w("showPttUpdateDialog: title=%s msg=%s", title, msg)
|
|
||||||
if (isConflictDialogShowing) return
|
|
||||||
val ctx = instance
|
|
||||||
if (ctx == null) {
|
|
||||||
pendingDialog = title to msg
|
|
||||||
Timber.tag("PTT").d("Pending dialog stored (instance null): title=%s", title)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isConflictDialogShowing = true
|
|
||||||
ctx.runOnUiThread {
|
|
||||||
androidx.appcompat.app.AlertDialog.Builder(ctx)
|
|
||||||
.setTitle(title)
|
|
||||||
.setMessage(msg)
|
|
||||||
.setCancelable(false)
|
|
||||||
.setPositiveButton("确认") { _, _ ->
|
|
||||||
com.stand.standapp.utils.AppKiller.killApp(ctx, restart = true)
|
|
||||||
}
|
|
||||||
.setOnDismissListener { isConflictDialogShowing = false }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun showPendingDialogIfNeeded() {
|
|
||||||
val pending = pendingDialog ?: return
|
|
||||||
Timber.tag("PTT").d("Flushing pending dialog: %s", pending.second)
|
|
||||||
pendingDialog = null
|
|
||||||
showPttUpdateDialog(pending.second, pending.first)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取并清除待执行的 JS 代码(由 bridge.js 轮询)
|
// 获取并清除待执行的 JS 代码(由 bridge.js 轮询)
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun pollPendingJs(): String {
|
fun pollPendingJs(): String {
|
||||||
|
|
@ -133,11 +93,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
private var canGoBack = false
|
private var canGoBack = false
|
||||||
private lateinit var geckoView: GeckoView
|
private lateinit var geckoView: GeckoView
|
||||||
|
|
||||||
private var officialMode = false
|
|
||||||
private var loginData = ""
|
|
||||||
|
|
||||||
private val mHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
private val mHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||||
private val TIMEOUT_MS = 10000L
|
private val TIMEOUT_MS = 30000L
|
||||||
|
|
||||||
private val timeoutRunnable = Runnable {
|
private val timeoutRunnable = Runnable {
|
||||||
showErrorPage()
|
showErrorPage()
|
||||||
|
|
@ -146,12 +103,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
instance = this
|
instance = this
|
||||||
showPendingDialogIfNeeded()
|
|
||||||
|
|
||||||
// 读取登录模式和数据
|
|
||||||
officialMode = intent.getBooleanExtra("official_mode", false)
|
|
||||||
loginData = intent.getStringExtra("login_data") ?: ""
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
|
@ -167,50 +118,22 @@ class MainActivity : AppCompatActivity() {
|
||||||
// 创建 GeckoRuntime
|
// 创建 GeckoRuntime
|
||||||
geckoRuntime = createGeckoRuntime()
|
geckoRuntime = createGeckoRuntime()
|
||||||
|
|
||||||
|
// 安装桥接扩展
|
||||||
|
geckoRuntime?.webExtensionController?.installBuiltIn(
|
||||||
|
"resource://android/assets/extensions/bridge/"
|
||||||
|
)
|
||||||
|
|
||||||
// 创建 AndroidInterface
|
// 创建 AndroidInterface
|
||||||
pttInterface = AndroidInterface(this)
|
pttInterface = AndroidInterface(this)
|
||||||
|
|
||||||
// 创建 GeckoSession 并配置(但不加载页面)
|
// 创建 GeckoSession 并配置
|
||||||
val session = createSession()
|
createSessionAndLoad()
|
||||||
geckoSession = session
|
|
||||||
geckoView.setSession(session)
|
|
||||||
|
|
||||||
startTimeoutTimer()
|
startTimeoutTimer()
|
||||||
|
|
||||||
// 安装桥接扩展,等安装完成后再加载页面
|
|
||||||
geckoRuntime?.webExtensionController?.installBuiltIn(
|
|
||||||
"resource://android/assets/extensions/bridge/"
|
|
||||||
)?.accept { extension ->
|
|
||||||
Timber.tag("MainActivity").d("WebExtension installed: ${extension?.metaData?.name}")
|
|
||||||
runOnUiThread {
|
|
||||||
val serverUrl = AppConfig.getServerUrl(this)
|
|
||||||
val finalUrl = if (officialMode) {
|
|
||||||
"$serverUrl/sysdispatch/home?t=${System.currentTimeMillis()}"
|
|
||||||
} else {
|
|
||||||
"$serverUrl/example/print_demo.html?t=${System.currentTimeMillis()}"
|
|
||||||
}
|
|
||||||
Timber.tag("MainActivity").d("Loading URL: $finalUrl (officialMode=$officialMode)")
|
|
||||||
session.loadUri(finalUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动时检查更新
|
// 启动时检查更新
|
||||||
UpdateManager(this).checkUpdate()
|
UpdateManager(this).checkUpdate()
|
||||||
|
|
||||||
// 👈 动态注入 Compose 容器,让 FloatingChatWidget 呈现在最上层!
|
|
||||||
val composeView = androidx.compose.ui.platform.ComposeView(this).apply {
|
|
||||||
setContent {
|
|
||||||
com.stand.standapp.ui.chat.FloatingChatWidget()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findViewById<FrameLayout>(android.R.id.content)?.addView(
|
|
||||||
composeView,
|
|
||||||
FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("MainActivity").e(e, "Fatal Crash")
|
Timber.tag("MainActivity").e(e, "Fatal Crash")
|
||||||
}
|
}
|
||||||
|
|
@ -220,17 +143,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
val runtime = GeckoRuntime.getDefault(this)
|
val runtime = GeckoRuntime.getDefault(this)
|
||||||
runtime.settings.aboutConfigEnabled = true
|
runtime.settings.aboutConfigEnabled = true
|
||||||
runtime.settings.consoleOutputEnabled = true
|
runtime.settings.consoleOutputEnabled = true
|
||||||
// 强制清缓存,确保 JS/CSS 等资源获取最新版本
|
|
||||||
try {
|
|
||||||
// runtime.storageController.clearData(ClearFlags.ALL)
|
|
||||||
Timber.tag("MainActivity").i("GeckoView cache cleared")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("MainActivity").w(e, "Failed to clear GeckoView cache")
|
|
||||||
}
|
|
||||||
return runtime
|
return runtime
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSession(): GeckoSession {
|
private fun createSessionAndLoad() {
|
||||||
val session = GeckoSession()
|
val session = GeckoSession()
|
||||||
val sessionSettings = session.settings
|
val sessionSettings = session.settings
|
||||||
|
|
||||||
|
|
@ -238,7 +154,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
sessionSettings.allowJavascript = true
|
sessionSettings.allowJavascript = true
|
||||||
sessionSettings.userAgentMode = GeckoSessionSettings.USER_AGENT_MODE_MOBILE
|
sessionSettings.userAgentMode = GeckoSessionSettings.USER_AGENT_MODE_MOBILE
|
||||||
|
|
||||||
val runtime = geckoRuntime ?: return session
|
val runtime = geckoRuntime ?: return
|
||||||
|
|
||||||
// 设置各种代理
|
// 设置各种代理
|
||||||
setupDelegates(session)
|
setupDelegates(session)
|
||||||
|
|
@ -246,7 +162,16 @@ class MainActivity : AppCompatActivity() {
|
||||||
// 打开会话
|
// 打开会话
|
||||||
session.open(runtime)
|
session.open(runtime)
|
||||||
|
|
||||||
return session
|
geckoSession = session
|
||||||
|
|
||||||
|
// 将 session 绑定到 GeckoView
|
||||||
|
geckoView.setSession(session)
|
||||||
|
|
||||||
|
// 加载页面
|
||||||
|
val serverUrl = AppConfig.getServerUrl(this)
|
||||||
|
val finalUrl = "$serverUrl/example/print_demo.html?t=${System.currentTimeMillis()}"
|
||||||
|
Timber.tag("MainActivity").d("Loading URL: $finalUrl")
|
||||||
|
session.loadUri(finalUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupDelegates(session: GeckoSession) {
|
private fun setupDelegates(session: GeckoSession) {
|
||||||
|
|
@ -278,15 +203,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||||
Timber.tag("MainActivity").d("Page stopped, success: $success")
|
Timber.tag("MainActivity").d("Page stopped, success: $success")
|
||||||
// 正式界面模式下,页面加载完成后传递登录数据给前端
|
|
||||||
// if (success && officialMode && loginData.isNotEmpty()) {
|
|
||||||
// // 延迟 500ms 确保 JS 上下文就绪
|
|
||||||
// mHandler.postDelayed({
|
|
||||||
// val escaped = loginData.replace("'", "\\'").replace("\n", "\\n")
|
|
||||||
// executeJs("if(window.appLogin){window.appLogin('$escaped');}")
|
|
||||||
// Timber.tag("MainActivity").d("Called appLogin with loginData")
|
|
||||||
// }, 500)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,81 +225,17 @@ class MainActivity : AppCompatActivity() {
|
||||||
val result = handleBridgeCall(payload, prompt.defaultValue ?: "")
|
val result = handleBridgeCall(payload, prompt.defaultValue ?: "")
|
||||||
return GeckoResult.fromValue(prompt.confirm(result))
|
return GeckoResult.fromValue(prompt.confirm(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认处理(显示对话框)
|
// 默认处理(显示对话框)
|
||||||
val res = GeckoResult<GeckoSession.PromptDelegate.PromptResponse>()
|
return null
|
||||||
runOnUiThread {
|
|
||||||
val input = android.widget.EditText(this@MainActivity)
|
|
||||||
input.setText(prompt.defaultValue)
|
|
||||||
androidx.appcompat.app.AlertDialog.Builder(this@MainActivity)
|
|
||||||
.setTitle(prompt.title ?: "")
|
|
||||||
.setMessage(prompt.message)
|
|
||||||
.setView(input)
|
|
||||||
.setPositiveButton("确定") { _, _ -> res.complete(prompt.confirm(input.text.toString())) }
|
|
||||||
.setNegativeButton("取消") { _, _ -> res.complete(prompt.dismiss()) }
|
|
||||||
.setOnCancelListener { res.complete(prompt.dismiss()) }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAlertPrompt(
|
override fun onAlertPrompt(
|
||||||
session: GeckoSession,
|
session: GeckoSession,
|
||||||
prompt: GeckoSession.PromptDelegate.AlertPrompt
|
prompt: GeckoSession.PromptDelegate.AlertPrompt
|
||||||
): GeckoResult<GeckoSession.PromptDelegate.PromptResponse>? {
|
): GeckoResult<GeckoSession.PromptDelegate.PromptResponse>? {
|
||||||
val res = GeckoResult<GeckoSession.PromptDelegate.PromptResponse>()
|
Timber.tag("WebConsole").d("Alert: ${prompt.message}")
|
||||||
runOnUiThread {
|
|
||||||
androidx.appcompat.app.AlertDialog.Builder(this@MainActivity)
|
|
||||||
.setMessage(prompt.message)
|
|
||||||
.setPositiveButton("确定") { _, _ -> res.complete(prompt.dismiss()) }
|
|
||||||
.setOnCancelListener { res.complete(prompt.dismiss()) }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAuthPrompt(
|
|
||||||
session: GeckoSession,
|
|
||||||
prompt: GeckoSession.PromptDelegate.AuthPrompt
|
|
||||||
): GeckoResult<GeckoSession.PromptDelegate.PromptResponse>? {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChoicePrompt(
|
|
||||||
session: GeckoSession,
|
|
||||||
prompt: GeckoSession.PromptDelegate.ChoicePrompt
|
|
||||||
): GeckoResult<GeckoSession.PromptDelegate.PromptResponse>? {
|
|
||||||
val res = GeckoResult<GeckoSession.PromptDelegate.PromptResponse>()
|
|
||||||
runOnUiThread {
|
|
||||||
val choices = prompt.choices.map { it.label }.toTypedArray()
|
|
||||||
androidx.appcompat.app.AlertDialog.Builder(this@MainActivity)
|
|
||||||
.setTitle(prompt.title)
|
|
||||||
.setSingleChoiceItems(choices, -1) { dialog, which ->
|
|
||||||
dialog.dismiss()
|
|
||||||
res.complete(prompt.confirm(prompt.choices[which]))
|
|
||||||
}
|
|
||||||
.setOnCancelListener { res.complete(prompt.dismiss()) }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onButtonPrompt(
|
|
||||||
session: GeckoSession,
|
|
||||||
prompt: GeckoSession.PromptDelegate.ButtonPrompt
|
|
||||||
): GeckoResult<GeckoSession.PromptDelegate.PromptResponse>? {
|
|
||||||
val res = GeckoResult<GeckoSession.PromptDelegate.PromptResponse>()
|
|
||||||
runOnUiThread {
|
|
||||||
androidx.appcompat.app.AlertDialog.Builder(this@MainActivity)
|
|
||||||
.setTitle(prompt.title ?: "提示")
|
|
||||||
.setMessage(prompt.message)
|
|
||||||
.setPositiveButton("确定") { _, _ -> res.complete(prompt.confirm(GeckoSession.PromptDelegate.ButtonPrompt.Type.POSITIVE)) }
|
|
||||||
.setNegativeButton("取消") { _, _ -> res.complete(prompt.dismiss()) }
|
|
||||||
.setOnCancelListener { res.complete(prompt.dismiss()) }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,23 +263,19 @@ class MainActivity : AppCompatActivity() {
|
||||||
.setTitle("提示")
|
.setTitle("提示")
|
||||||
.setMessage("确定要退出应用吗?")
|
.setMessage("确定要退出应用吗?")
|
||||||
.setPositiveButton("确定") { _, _ ->
|
.setPositiveButton("确定") { _, _ ->
|
||||||
com.stand.standapp.utils.AppKiller.killApp(this)
|
pttLogoutAndDestroy()
|
||||||
|
finish()
|
||||||
}
|
}
|
||||||
.setNegativeButton("取消", null)
|
.setNegativeButton("取消", null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pttLogoutAndDestroy() {
|
private fun pttLogoutAndDestroy() {
|
||||||
try {
|
|
||||||
com.example.kingway.ptt.SendAtUtil.toCancelLogin()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("LoginActivity").e(e, "PTT cancel failed")
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
com.example.kingway.ptt.SendAtUtil.toLogout()
|
com.example.kingway.ptt.SendAtUtil.toLogout()
|
||||||
Timber.tag("LoginActivity").i("PTT logout sent")
|
Timber.tag("MainActivity").i("PTT logout sent")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("LoginActivity").e(e, "PTT logout failed")
|
Timber.tag("MainActivity").e(e, "PTT logout failed")
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
com.stand.standapp.utils.GpioManager.stopListening()
|
com.stand.standapp.utils.GpioManager.stopListening()
|
||||||
|
|
@ -435,27 +283,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
Timber.tag("MainActivity").e(e, "GPIO stop failed")
|
Timber.tag("MainActivity").e(e, "GPIO stop failed")
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
com.stand.standapp.utils.GpioManager.stopListening()
|
com.example.kingway.ptt.MyApplication.destroy()
|
||||||
|
Timber.tag("MainActivity").i("PTT native destroyed")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("LoginActivity").e(e, "GPIO stop failed")
|
Timber.tag("MainActivity").e(e, "PTT destroy failed")
|
||||||
}
|
|
||||||
try {
|
|
||||||
// com.example.kingway.ptt.MyApplication.destroy()
|
|
||||||
Timber.tag("LoginActivity").i("PTT native destroyed")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("LoginActivity").e(e, "PTT destroy failed")
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// PrinterManager.shutdown()
|
|
||||||
Timber.tag("LoginActivity").i("Printer shut down")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("LoginActivity").e(e, "Printer shutdown failed")
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
com.stand.standapp.utils.LogManager.shutdown()
|
|
||||||
Timber.tag("LoginActivity").i("LogManager shut down")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("LoginActivity").e(e, "LogManager shutdown failed")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -490,10 +321,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
mHandler.removeCallbacks(timeoutRunnable)
|
mHandler.removeCallbacks(timeoutRunnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isOfficialMode(): Boolean = officialMode
|
|
||||||
|
|
||||||
fun getLoginData(): String = loginData
|
|
||||||
|
|
||||||
private fun showErrorPage() {
|
private fun showErrorPage() {
|
||||||
findViewById<android.view.View>(R.id.loading_layout)?.visibility = android.view.View.GONE
|
findViewById<android.view.View>(R.id.loading_layout)?.visibility = android.view.View.GONE
|
||||||
findViewById<android.view.View>(R.id.error_layout)?.visibility = android.view.View.VISIBLE
|
findViewById<android.view.View>(R.id.error_layout)?.visibility = android.view.View.VISIBLE
|
||||||
|
|
|
||||||
|
|
@ -1,109 +1,50 @@
|
||||||
package com.stand.standapp
|
package com.stand.standapp
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.util.Log
|
|
||||||
import com.example.kingway.ptt.SendAtUtil
|
|
||||||
import com.example.kingway.ptt.pttNative
|
|
||||||
import com.stand.standapp.utils.LogManager
|
import com.stand.standapp.utils.LogManager
|
||||||
|
import com.example.kingway.ptt.MyApplication as PTTMyApplication
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class MyApplication : Application() {
|
class MyApplication : Application() {
|
||||||
|
|
||||||
// 单例实例
|
|
||||||
companion object {
|
companion object {
|
||||||
private lateinit var mApp: MyApplication
|
private var instance: MyApplication? = null
|
||||||
|
fun getAppInstance(): MyApplication = instance!!
|
||||||
@JvmStatic
|
|
||||||
fun getAppInstance(): MyApplication {
|
|
||||||
if (pNative == null) {
|
|
||||||
initPTT()
|
|
||||||
}
|
|
||||||
return mApp
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun initPTT() {
|
|
||||||
if (pNative != null) return
|
|
||||||
ensureCfgDat()
|
|
||||||
try {
|
|
||||||
pNative = pttNative()
|
|
||||||
|
|
||||||
pNative?.pttNativeSetPackageName(pageName)
|
|
||||||
pNative?.pttNativeSetAtCallback()
|
|
||||||
pNative?.pttNativeSetPlayCallback()
|
|
||||||
pNative?.pttNativeSetPalyStartCallback()
|
|
||||||
pNative?.pttNativeSetPlayStopCallback()
|
|
||||||
pNative?.pttNativeSetRecordStartCallback()
|
|
||||||
pNative?.pttNativeSetRecordStopCallback()
|
|
||||||
pNative?.pttNativeSetTTSCallback()
|
|
||||||
|
|
||||||
pNative?.pttNativePocTaskStart()
|
|
||||||
Log.i("PTT_INIT", "PTT init success")
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Log.e("PTT_INIT", "Failed to init PTT: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pageName: String = ""
|
|
||||||
var pNative: pttNative? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 首次安装时从 assets 复制一份有效的 cfg.dat,
|
|
||||||
* 避免 native 读不到配置报 zpoc_init_cfg 13。
|
|
||||||
*/
|
|
||||||
private fun ensureCfgDat() {
|
|
||||||
try {
|
|
||||||
val cfgFile = File("/data/data/$pageName/cfg.dat")
|
|
||||||
if (cfgFile.exists()) return
|
|
||||||
val app = mApp
|
|
||||||
app.assets.open("cfg.dat").use { input ->
|
|
||||||
cfgFile.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Timber.tag("PTT_INIT").i("Copied cfg.dat from assets")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("PTT_INIT").w(e, "Failed to copy cfg.dat from assets")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取 pttNative 实例(非空)
|
|
||||||
*/
|
|
||||||
fun getpNative(): pttNative? {
|
|
||||||
return pNative
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
|
||||||
mApp = this
|
|
||||||
pageName = packageName
|
|
||||||
|
|
||||||
com.stand.standapp.utils.AppKiller.setupUncaughtExceptionHandler(this)
|
|
||||||
|
|
||||||
initPTT()
|
|
||||||
// 第一步:初始化日志
|
// 第一步:初始化日志
|
||||||
LogManager.init(this)
|
LogManager.init(this)
|
||||||
|
|
||||||
// 初始化 xCrash 以捕获 Native (SO) 崩溃
|
// 【核心】初始化 xCrash 以捕获 Native (SO) 崩溃
|
||||||
xcrash.XCrash.init(this, xcrash.XCrash.InitParameters().apply {
|
xcrash.XCrash.init(this, xcrash.XCrash.InitParameters().apply {
|
||||||
setAppVersion(packageManager.getPackageInfo(packageName, 0).versionName)
|
setAppVersion(packageManager.getPackageInfo(packageName, 0).versionName)
|
||||||
setLogDir(getExternalFilesDir("tombstones")?.absolutePath)
|
setLogDir(getExternalFilesDir("tombstones")?.absolutePath)
|
||||||
setNativeDumpAllThreads(true)
|
setNativeDumpAllThreads(true)
|
||||||
|
|
||||||
|
// Java 崩溃回调
|
||||||
setJavaCallback { logPath, emergency ->
|
setJavaCallback { logPath, emergency ->
|
||||||
LogManager.onCrashDetected(logPath, "Java-xCrash", null)
|
LogManager.onCrashDetected(logPath, "Java-xCrash", null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Native 崩溃回调
|
||||||
setNativeCallback { logPath, emergency ->
|
setNativeCallback { logPath, emergency ->
|
||||||
LogManager.onCrashDetected(logPath, "Native-xCrash", null)
|
LogManager.onCrashDetected(logPath, "Native-xCrash", null)
|
||||||
android.util.Log.e("MyApplication", "NATIVE CRASH DETECTED at: $logPath")
|
android.util.Log.e("MyApplication", "NATIVE CRASH DETECTED at: $logPath")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
super.onCreate()
|
||||||
|
instance = this
|
||||||
|
|
||||||
Timber.tag("MyApplication").i("=== Application Starting ===")
|
Timber.tag("MyApplication").i("=== Application Starting ===")
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// // 第二步:异步或保护性初始化 PTT
|
||||||
|
// PTTMyApplication.init(this)
|
||||||
|
// Timber.tag("MyApplication").i("PTT Module init sequence finished")
|
||||||
|
// } catch (e: Throwable) {
|
||||||
|
// Timber.tag("MyApplication").e(e, "PTT Module CRASH during init")
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,285 +0,0 @@
|
||||||
package com.stand.standapp
|
|
||||||
|
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
|
||||||
import android.view.animation.AlphaAnimation
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.LinearInterpolator
|
|
||||||
import android.widget.Button
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.example.kingway.ptt.SendAtUtil
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class TempCallActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private var isSpeaking = false
|
|
||||||
private var lastSpeakTime = 0L
|
|
||||||
private val MIN_SPEAK_DURATION = 200L
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
|
||||||
private var pulseAnimator: ObjectAnimator? = null
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@Volatile
|
|
||||||
var instance: TempCallActivity? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
var isMinimized = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
// 存储当前临时群组名称,用于恢复时传递
|
|
||||||
@Volatile
|
|
||||||
var currentGroupName: String = "临时会话"
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun isOpen(): Boolean = instance != null
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun isMinimizedState(): Boolean = isMinimized
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun maximize() {
|
|
||||||
if (isMinimized) {
|
|
||||||
isMinimized = false
|
|
||||||
val intent = android.content.Intent(
|
|
||||||
com.stand.standapp.MyApplication.getAppInstance(),
|
|
||||||
TempCallActivity::class.java
|
|
||||||
)
|
|
||||||
intent.putExtra("group_name", currentGroupName)
|
|
||||||
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
com.stand.standapp.MyApplication.getAppInstance().startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun closeWithMessage(message: String) {
|
|
||||||
val appCtx = com.stand.standapp.MyApplication.getAppInstance()
|
|
||||||
instance?.runOnUiThread {
|
|
||||||
Toast.makeText(appCtx, message, Toast.LENGTH_SHORT).show()
|
|
||||||
instance?.finish()
|
|
||||||
} ?: run {
|
|
||||||
// Activity 已销毁但状态还在,清除状态
|
|
||||||
clearMinimizedState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun clearMinimizedState() {
|
|
||||||
isMinimized = false
|
|
||||||
currentGroupName = "临时会话"
|
|
||||||
MainActivity.executeJs("onTempCallStateChanged('destroyed')")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
// 全屏覆盖 + 常亮 + 锁屏显示
|
|
||||||
window.addFlags(
|
|
||||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
|
|
||||||
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
||||||
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
|
||||||
)
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_temp_call)
|
|
||||||
instance = this
|
|
||||||
isMinimized = false
|
|
||||||
|
|
||||||
val groupName = intent.getStringExtra("group_name") ?: "临时会话"
|
|
||||||
currentGroupName = groupName
|
|
||||||
findViewById<TextView>(R.id.tv_group_name).text = groupName
|
|
||||||
|
|
||||||
setupMinimizeButton()
|
|
||||||
setupPttButton()
|
|
||||||
setupHangupButton()
|
|
||||||
startPulseAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
isMinimized = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
pulseAnimator?.cancel()
|
|
||||||
if (instance == this) {
|
|
||||||
instance = null
|
|
||||||
// 如果不是最小化状态,说明是真正结束,清除所有状态
|
|
||||||
if (!isMinimized) {
|
|
||||||
currentGroupName = "临时会话"
|
|
||||||
}
|
|
||||||
// 通知 web 页面状态变化
|
|
||||||
MainActivity.executeJs("onTempCallStateChanged('" + if (isMinimized) "minimized" else "destroyed" + "')")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拦截返回键
|
|
||||||
override fun onBackPressed() {
|
|
||||||
showMinimizeDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 不要在 recent tasks 中显示
|
|
||||||
override fun finish() {
|
|
||||||
super.finish()
|
|
||||||
overridePendingTransition(0, android.R.anim.fade_out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateGroupName(name: String) {
|
|
||||||
findViewById<TextView>(R.id.tv_group_name)?.text = name
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startPulseAnimation() {
|
|
||||||
val pulse = findViewById<ImageView>(R.id.iv_pulse) ?: return
|
|
||||||
pulseAnimator = ObjectAnimator.ofFloat(pulse, "alpha", 1f, 0.3f).apply {
|
|
||||||
duration = 1000
|
|
||||||
repeatCount = ObjectAnimator.INFINITE
|
|
||||||
repeatMode = ObjectAnimator.REVERSE
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupPttButton() {
|
|
||||||
val btnPtt = findViewById<Button>(R.id.btn_ptt)
|
|
||||||
val viewGlow = findViewById<View>(R.id.view_glow)
|
|
||||||
|
|
||||||
val startSpeak = {
|
|
||||||
if (!isSpeaking) {
|
|
||||||
isSpeaking = true
|
|
||||||
lastSpeakTime = System.currentTimeMillis()
|
|
||||||
SendAtUtil.speakPlay()
|
|
||||||
btnPtt.text = "正在\n说话..."
|
|
||||||
btnPtt.setBackgroundResource(R.drawable.circle_ptt_speaking)
|
|
||||||
viewGlow.setBackgroundResource(R.drawable.circle_ptt_glow_active)
|
|
||||||
findViewById<TextView>(R.id.tv_talk_info).text = "正在通话"
|
|
||||||
findViewById<TextView>(R.id.tv_talk_status).apply {
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
text = "● 对讲中"
|
|
||||||
setTextColor(0xFFEF4444.toInt())
|
|
||||||
}
|
|
||||||
Timber.tag("TempCall").d("Speak Play")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val stopSpeak = {
|
|
||||||
if (isSpeaking) {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val duration = now - lastSpeakTime
|
|
||||||
if (duration < MIN_SPEAK_DURATION) {
|
|
||||||
handler.postDelayed({ realRelease(btnPtt, viewGlow) }, MIN_SPEAK_DURATION - duration)
|
|
||||||
} else {
|
|
||||||
realRelease(btnPtt, viewGlow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
btnPtt.setOnTouchListener { _, event ->
|
|
||||||
when (event.action) {
|
|
||||||
MotionEvent.ACTION_DOWN -> {
|
|
||||||
startSpeak()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
||||||
stopSpeak()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun realRelease(btnPtt: Button, viewGlow: View) {
|
|
||||||
if (isSpeaking) {
|
|
||||||
isSpeaking = false
|
|
||||||
SendAtUtil.speakRelease()
|
|
||||||
btnPtt.text = "按住\n说话"
|
|
||||||
btnPtt.setBackgroundResource(R.drawable.circle_ptt_button)
|
|
||||||
viewGlow.setBackgroundResource(R.drawable.circle_ptt_glow)
|
|
||||||
findViewById<TextView>(R.id.tv_talk_info).text = "按下按钮讲话"
|
|
||||||
findViewById<TextView>(R.id.tv_talk_status).apply {
|
|
||||||
visibility = View.GONE
|
|
||||||
}
|
|
||||||
Timber.tag("TempCall").d("Speak Release")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupHangupButton() {
|
|
||||||
findViewById<Button>(R.id.btn_hangup).setOnClickListener {
|
|
||||||
try {
|
|
||||||
SendAtUtil.stopTempCall()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("TempCall").e(e, "stopTempCall error")
|
|
||||||
}
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupMinimizeButton() {
|
|
||||||
findViewById<View>(R.id.ll_minimize).setOnClickListener {
|
|
||||||
minimizeAndFinish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showMinimizeDialog() {
|
|
||||||
androidx.appcompat.app.AlertDialog.Builder(this)
|
|
||||||
.setTitle("临时呼叫")
|
|
||||||
.setMessage("是否最小化临时呼叫页面?")
|
|
||||||
.setPositiveButton("最小化") { _, _ ->
|
|
||||||
minimizeAndFinish()
|
|
||||||
}
|
|
||||||
.setNegativeButton("取消", null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun minimizeAndFinish() {
|
|
||||||
isMinimized = true
|
|
||||||
// 通知 web 页面临时呼叫页面已最小化
|
|
||||||
MainActivity.executeJs("onTempCallStateChanged('minimized')")
|
|
||||||
// finish 但保持 isMinimized = true
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 由 GpioManager 调用,同步 GPIO 按下状态
|
|
||||||
fun onGpioPressed() {
|
|
||||||
val btnPtt = findViewById<Button>(R.id.btn_ptt) ?: return
|
|
||||||
val viewGlow = findViewById<View>(R.id.view_glow) ?: return
|
|
||||||
if (!isSpeaking) {
|
|
||||||
isSpeaking = true
|
|
||||||
lastSpeakTime = System.currentTimeMillis()
|
|
||||||
SendAtUtil.speakPlay()
|
|
||||||
btnPtt.text = "正在\n说话..."
|
|
||||||
btnPtt.setBackgroundResource(R.drawable.circle_ptt_speaking)
|
|
||||||
viewGlow.setBackgroundResource(R.drawable.circle_ptt_glow_active)
|
|
||||||
findViewById<TextView>(R.id.tv_talk_info).text = "正在通话"
|
|
||||||
findViewById<TextView>(R.id.tv_talk_status).apply {
|
|
||||||
visibility = View.VISIBLE
|
|
||||||
text = "● 对讲中"
|
|
||||||
setTextColor(0xFFEF4444.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 由 GpioManager 调用,同步 GPIO 松开状态
|
|
||||||
fun onGpioReleased() {
|
|
||||||
val btnPtt = findViewById<Button>(R.id.btn_ptt) ?: return
|
|
||||||
val viewGlow = findViewById<View>(R.id.view_glow) ?: return
|
|
||||||
if (isSpeaking) {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val duration = now - lastSpeakTime
|
|
||||||
if (duration < MIN_SPEAK_DURATION) {
|
|
||||||
handler.postDelayed({ realRelease(btnPtt, viewGlow) }, MIN_SPEAK_DURATION - duration)
|
|
||||||
} else {
|
|
||||||
realRelease(btnPtt, viewGlow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -70,7 +70,7 @@ class UpdateManager(private val context: Context) {
|
||||||
try {
|
try {
|
||||||
val body = response.body?.string() ?: throw IOException("返回内容为空")
|
val body = response.body?.string() ?: throw IOException("返回内容为空")
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
handler.post { Toast.makeText(context.applicationContext, "更新检查失败: ${response.code}", Toast.LENGTH_SHORT).show() }
|
handler.post { Toast.makeText(context, "更新检查失败: ${response.code}", Toast.LENGTH_SHORT).show() }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,23 +89,23 @@ class UpdateManager(private val context: Context) {
|
||||||
try {
|
try {
|
||||||
showUpdateDialog(versionName, content, id)
|
showUpdateDialog(versionName, content, id)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Toast.makeText(context.applicationContext, "显示更新弹窗失败", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "显示更新弹窗失败", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handler.post { Toast.makeText(context.applicationContext, "当前已是最新版本", Toast.LENGTH_SHORT).show() }
|
handler.post { Toast.makeText(context, "当前已是最新版本", Toast.LENGTH_SHORT).show() }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handler.post { Toast.makeText(context.applicationContext, "当前已是最新版本", Toast.LENGTH_SHORT).show() }
|
handler.post { Toast.makeText(context, "当前已是最新版本", Toast.LENGTH_SHORT).show() }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val msg = json.optString("msg", "未知错误")
|
val msg = json.optString("msg", "未知错误")
|
||||||
handler.post { Toast.makeText(context.applicationContext, "更新服务提示: $msg", Toast.LENGTH_SHORT).show() }
|
handler.post { Toast.makeText(context, "更新服务提示: $msg", Toast.LENGTH_SHORT).show() }
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("UpdateManager").e(e, "解析或处理更新结果失败")
|
Timber.tag("UpdateManager").e(e, "解析或处理更新结果失败")
|
||||||
handler.post { Toast.makeText(context.applicationContext, "更新信息解析失败", Toast.LENGTH_SHORT).show() }
|
handler.post { Toast.makeText(context, "更新信息解析失败", Toast.LENGTH_SHORT).show() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -150,7 +150,7 @@ class UpdateManager(private val context: Context) {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
handler.post {
|
handler.post {
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
Toast.makeText(context.applicationContext, "连接服务器失败", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "连接服务器失败", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,7 +158,7 @@ class UpdateManager(private val context: Context) {
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
handler.post {
|
handler.post {
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
Toast.makeText(context.applicationContext, "下载失败: ${response.code}", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "下载失败: ${response.code}", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +169,7 @@ class UpdateManager(private val context: Context) {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
handler.post {
|
handler.post {
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
Toast.makeText(context.applicationContext, "保存失败", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "保存失败", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,13 +210,13 @@ class UpdateManager(private val context: Context) {
|
||||||
|
|
||||||
handler.post {
|
handler.post {
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
Toast.makeText(context.applicationContext, "下载完成,文件已保存到系统下载目录", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "下载完成,文件已保存到系统下载目录", Toast.LENGTH_SHORT).show()
|
||||||
installApk(targetUri)
|
installApk(targetUri)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
handler.post {
|
handler.post {
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
Toast.makeText(context.applicationContext, "保存失败: ${e.message}", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, "保存失败: ${e.message}", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
inputStream?.close()
|
inputStream?.close()
|
||||||
|
|
@ -246,7 +246,7 @@ class UpdateManager(private val context: Context) {
|
||||||
val intent = Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
val intent = Intent(android.provider.Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||||
intent.data = Uri.parse("package:" + context.packageName)
|
intent.data = Uri.parse("package:" + context.packageName)
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
Toast.makeText(context.applicationContext, "请先授予安装权限", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "请先授予安装权限", Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +267,7 @@ class UpdateManager(private val context: Context) {
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("UpdateManager").e(e, "安装失败")
|
Timber.tag("UpdateManager").e(e, "安装失败")
|
||||||
Toast.makeText(context.applicationContext, "无法自动调起安装,请在系统下载目录中手动安装", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, "无法自动调起安装,请在系统下载目录中手动安装", Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,9 @@ object PrinterManager {
|
||||||
// 在 Android 系统层面主动提示
|
// 在 Android 系统层面主动提示
|
||||||
mContext?.let {
|
mContext?.let {
|
||||||
if (!hasPaper) {
|
if (!hasPaper) {
|
||||||
android.widget.Toast.makeText(it.applicationContext, "⚠️ 警告:打印机缺纸!", android.widget.Toast.LENGTH_LONG).show()
|
android.widget.Toast.makeText(it, "⚠️ 警告:打印机缺纸!", android.widget.Toast.LENGTH_LONG).show()
|
||||||
} else {
|
|
||||||
android.widget.Toast.makeText(it.applicationContext, "打印机状态:正常 (有纸)", android.widget.Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通知前端页面
|
|
||||||
com.stand.standapp.MainActivity.executeJs("if(window.onPaperStateChanged) { window.onPaperStateChanged($hasPaper); }")
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,260 +0,0 @@
|
||||||
package com.stand.standapp.ui.chat
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.stand.standapp.AppConfig
|
|
||||||
import com.stand.standapp.ui.chat.model.ChatMessage
|
|
||||||
import com.stand.standapp.ui.chat.model.ChatSession
|
|
||||||
import com.stand.standapp.ui.chat.repo.AiChatRepository
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
class ChatViewModel(application: Application) : AndroidViewModel(application) {
|
|
||||||
private val repository = AiChatRepository()
|
|
||||||
|
|
||||||
private val _sessions = MutableStateFlow<List<ChatSession>>(emptyList())
|
|
||||||
val sessions: StateFlow<List<ChatSession>> = _sessions.asStateFlow()
|
|
||||||
|
|
||||||
private val _currentSessionId = MutableStateFlow<String?>(null)
|
|
||||||
val currentSessionId: StateFlow<String?> = _currentSessionId.asStateFlow()
|
|
||||||
|
|
||||||
private val _messages = MutableStateFlow<List<ChatMessage>>(emptyList())
|
|
||||||
val messages: StateFlow<List<ChatMessage>> = _messages.asStateFlow()
|
|
||||||
|
|
||||||
// 👈 声明一个正在加载的状态,保证上一个问题没结束前无法再次发送提问!
|
|
||||||
private val _isLoading = MutableStateFlow(false)
|
|
||||||
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
|
||||||
|
|
||||||
// 👈 额外声明一个当前协程控制作业,用于物理取消/中止 SSE 对话!
|
|
||||||
private var activeChatJob: kotlinx.coroutines.Job? = null
|
|
||||||
|
|
||||||
private val prefs = application.getSharedPreferences("ai_chat_cache", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
init {
|
|
||||||
// 👈 将数据加载移到IO线程,避免阻塞主线程导致UI卡顿
|
|
||||||
viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) {
|
|
||||||
loadCacheFromLocal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadCacheFromLocal() {
|
|
||||||
try {
|
|
||||||
val sessionsJson = prefs.getString("cache_sessions", "")
|
|
||||||
val messagesJson = prefs.getString("cache_messages", "")
|
|
||||||
|
|
||||||
if (!sessionsJson.isNullOrBlank()) {
|
|
||||||
val sessionsArray = JSONArray(sessionsJson)
|
|
||||||
val restoredSessions = mutableListOf<ChatSession>()
|
|
||||||
for (i in 0 until sessionsArray.length()) {
|
|
||||||
val obj = sessionsArray.getJSONObject(i)
|
|
||||||
restoredSessions.add(
|
|
||||||
ChatSession(
|
|
||||||
id = obj.getString("id"),
|
|
||||||
title = obj.getString("title"),
|
|
||||||
timestamp = obj.getLong("timestamp")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_sessions.value = restoredSessions
|
|
||||||
if (restoredSessions.isNotEmpty()) {
|
|
||||||
_currentSessionId.value = restoredSessions.first().id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!messagesJson.isNullOrBlank()) {
|
|
||||||
val messagesArray = JSONArray(messagesJson)
|
|
||||||
val restoredMessages = mutableListOf<ChatMessage>()
|
|
||||||
val maxMessages = 200 // 👈 限制最多加载200条消息,避免大数据解析卡顿
|
|
||||||
val startIndex = (messagesArray.length() - maxMessages).coerceAtLeast(0)
|
|
||||||
for (i in startIndex until messagesArray.length()) {
|
|
||||||
val obj = messagesArray.getJSONObject(i)
|
|
||||||
restoredMessages.add(
|
|
||||||
ChatMessage(
|
|
||||||
id = obj.getString("id"),
|
|
||||||
sessionId = obj.getString("sessionId"),
|
|
||||||
role = ChatMessage.Role.valueOf(obj.getString("role")),
|
|
||||||
content = obj.getString("content"),
|
|
||||||
isStreaming = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_messages.value = restoredMessages
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_sessions.value.isEmpty()) {
|
|
||||||
createNewSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveCacheToLocal() {
|
|
||||||
try {
|
|
||||||
// 保存会话列表
|
|
||||||
val sessionsArray = JSONArray()
|
|
||||||
_sessions.value.forEach { session ->
|
|
||||||
val obj = JSONObject()
|
|
||||||
obj.put("id", session.id)
|
|
||||||
obj.put("title", session.title)
|
|
||||||
obj.put("timestamp", session.timestamp)
|
|
||||||
sessionsArray.put(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存消息列表
|
|
||||||
val messagesArray = JSONArray()
|
|
||||||
_messages.value.forEach { msg ->
|
|
||||||
val obj = JSONObject()
|
|
||||||
obj.put("id", msg.id)
|
|
||||||
obj.put("sessionId", msg.sessionId)
|
|
||||||
obj.put("role", msg.role.name)
|
|
||||||
obj.put("content", msg.content)
|
|
||||||
messagesArray.put(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit()
|
|
||||||
.putString("cache_sessions", sessionsArray.toString())
|
|
||||||
.putString("cache_messages", messagesArray.toString())
|
|
||||||
.apply()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 👈 用户手动中断当前正在生成的 AI 回答的方法!
|
|
||||||
fun cancelActiveStreaming() {
|
|
||||||
activeChatJob?.cancel() // 物理取消协程作业,断开 SSE OkHttp 连接并关闭流!
|
|
||||||
activeChatJob = null
|
|
||||||
|
|
||||||
// 将当前正在流式输出的 AI 消息状态重置为完成,防止气泡卡死在 streaming 样式
|
|
||||||
_messages.value = _messages.value.map { msg ->
|
|
||||||
if (msg.isStreaming) {
|
|
||||||
msg.copy(isStreaming = false)
|
|
||||||
} else msg
|
|
||||||
}
|
|
||||||
|
|
||||||
_isLoading.value = false // 释放锁,允许用户立刻开始下一次提问!
|
|
||||||
saveCacheToLocal() // 强制存盘归档
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createNewSession() {
|
|
||||||
val newSession = ChatSession()
|
|
||||||
// 👈 限制最多保留10组对话历史,超出时删除最旧的会话
|
|
||||||
val trimmedSessions = if (_sessions.value.size >= 10) {
|
|
||||||
val sessionsToKeep = _sessions.value.take(9)
|
|
||||||
// 同时删除最旧会话的消息
|
|
||||||
val removedSessionIds = _sessions.value.drop(9).map { it.id }.toSet()
|
|
||||||
_messages.value = _messages.value.filter { it.sessionId !in removedSessionIds }
|
|
||||||
sessionsToKeep
|
|
||||||
} else {
|
|
||||||
_sessions.value
|
|
||||||
}
|
|
||||||
_sessions.value = listOf(newSession) + trimmedSessions
|
|
||||||
_currentSessionId.value = newSession.id
|
|
||||||
saveCacheToLocal()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun switchSession(sessionId: String) {
|
|
||||||
_currentSessionId.value = sessionId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearAllSessions() {
|
|
||||||
_sessions.value = emptyList()
|
|
||||||
_messages.value = emptyList()
|
|
||||||
_currentSessionId.value = null
|
|
||||||
prefs.edit().clear().apply()
|
|
||||||
createNewSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendMessage(content: String) {
|
|
||||||
if (_isLoading.value) return // 👈 拦截提问:上一个回答正在流式写时,绝对不允许重复提交!
|
|
||||||
|
|
||||||
val sessionId = _currentSessionId.value ?: return
|
|
||||||
_isLoading.value = true // 👈 锁定发送状态
|
|
||||||
|
|
||||||
// 1. 如果是新会话,更新标题
|
|
||||||
updateSessionTitleIfFirstMessage(sessionId, content)
|
|
||||||
|
|
||||||
// 2. 添加用户消息
|
|
||||||
val userMsg = ChatMessage(sessionId = sessionId, role = ChatMessage.Role.USER, content = content)
|
|
||||||
|
|
||||||
// 3. 准备 AI 空消息,处于 streaming 状态
|
|
||||||
val aiMsgId = UUID.randomUUID().toString()
|
|
||||||
val aiMsg = ChatMessage(id = aiMsgId, sessionId = sessionId, role = ChatMessage.Role.AI, content = "", isStreaming = true)
|
|
||||||
|
|
||||||
_messages.value = _messages.value + userMsg + aiMsg
|
|
||||||
saveCacheToLocal() // 👈 用户发出消息,存入本地历史记录
|
|
||||||
|
|
||||||
// 4. 发起网络请求,流式追加内容
|
|
||||||
activeChatJob = viewModelScope.launch {
|
|
||||||
val token = AppConfig.getAccessToken(getApplication())
|
|
||||||
|
|
||||||
// 提取当前会话(除了刚发出的这条和尚未生成的AI回复之外的)历史对话上下文
|
|
||||||
// 👈 限制最多取最近10轮对话(20条消息),避免token超限
|
|
||||||
val currentSessionMsgs = _messages.value
|
|
||||||
.filter { it.sessionId == sessionId && it.id != userMsg.id && it.id != aiMsgId && !it.isStreaming }
|
|
||||||
.takeLast(6)
|
|
||||||
|
|
||||||
// 👈 使用 JSONArray 构建历史记录,确保特殊字符被正确转义
|
|
||||||
val historyArray = JSONArray()
|
|
||||||
currentSessionMsgs.forEach { chatMessage ->
|
|
||||||
val roleStr = if (chatMessage.role == ChatMessage.Role.USER) "user" else "assistant"
|
|
||||||
val historyItem = JSONObject()
|
|
||||||
historyItem.put("role", roleStr)
|
|
||||||
historyItem.put("content", chatMessage.content)
|
|
||||||
historyArray.put(historyItem)
|
|
||||||
}
|
|
||||||
val historyJson = if (currentSessionMsgs.isEmpty()) "" else historyArray.toString()
|
|
||||||
|
|
||||||
repository.streamChat(getApplication(), content, token, historyJson)
|
|
||||||
.catch { e ->
|
|
||||||
appendAiMessageChunk(aiMsgId, "\n[请求异常: ${e.message}]")
|
|
||||||
finalizeAiMessage(aiMsgId)
|
|
||||||
_isLoading.value = false // 发生异常释放锁
|
|
||||||
}
|
|
||||||
.onCompletion {
|
|
||||||
finalizeAiMessage(aiMsgId)
|
|
||||||
_isLoading.value = false // 传输完成释放锁
|
|
||||||
}
|
|
||||||
.collect { chunk ->
|
|
||||||
appendAiMessageChunk(aiMsgId, chunk)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun appendAiMessageChunk(messageId: String, chunk: String) {
|
|
||||||
_messages.value = _messages.value.map { msg ->
|
|
||||||
if (msg.id == messageId) {
|
|
||||||
msg.copy(content = msg.content + chunk)
|
|
||||||
} else msg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun finalizeAiMessage(messageId: String) {
|
|
||||||
_messages.value = _messages.value.map { msg ->
|
|
||||||
if (msg.id == messageId) {
|
|
||||||
msg.copy(isStreaming = false)
|
|
||||||
} else msg
|
|
||||||
}
|
|
||||||
saveCacheToLocal() // 👈 AI 回复完毕后,状态变更为非 streaming 并归档存盘
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSessionTitleIfFirstMessage(sessionId: String, content: String) {
|
|
||||||
val isFirst = _messages.value.none { it.sessionId == sessionId }
|
|
||||||
if (isFirst) {
|
|
||||||
val title = if (content.length > 10) content.take(10) + "..." else content
|
|
||||||
_sessions.value = _sessions.value.map {
|
|
||||||
if (it.id == sessionId) it.copy(title = title) else it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
package com.stand.standapp.ui.chat
|
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.stand.standapp.ui.chat.components.ChatScaffold
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FloatingChatWidget() {
|
|
||||||
var isExpanded by remember { mutableStateOf(false) }
|
|
||||||
val configuration = LocalConfiguration.current
|
|
||||||
val density = LocalDensity.current
|
|
||||||
|
|
||||||
val screenWidthPx = with(density) { configuration.screenWidthDp.dp.toPx() }
|
|
||||||
val screenHeightPx = with(density) { configuration.screenHeightDp.dp.toPx() }
|
|
||||||
val maxWidth = (configuration.screenWidthDp * 0.45f).dp
|
|
||||||
val maxHeight = (configuration.screenHeightDp * 0.75f).dp
|
|
||||||
val cardWidthPx = with(density) { maxWidth.toPx() }
|
|
||||||
val cardHeightPx = with(density) { maxHeight.toPx() }
|
|
||||||
val fabSizePx = with(density) { 56.dp.toPx() }
|
|
||||||
val paddingPx = with(density) { 16.dp.toPx() }
|
|
||||||
val navBarPx = with(density) { 32.dp.toPx() }
|
|
||||||
|
|
||||||
// 👈 FAB偏移量,全屏幕可拖动
|
|
||||||
var fabOffsetX by remember { mutableStateOf(0f) }
|
|
||||||
var fabOffsetY by remember { mutableStateOf(0f) }
|
|
||||||
|
|
||||||
// 👈 对话框额外偏移(拖动对话框产生)
|
|
||||||
var cardDragX by remember { mutableStateOf(0f) }
|
|
||||||
var cardDragY by remember { mutableStateOf(0f) }
|
|
||||||
|
|
||||||
if (isExpanded) {
|
|
||||||
BackHandler {
|
|
||||||
isExpanded = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
|
||||||
// FAB - 全屏幕可拖动
|
|
||||||
if (!isExpanded) {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = { isExpanded = true },
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.padding(16.dp)
|
|
||||||
.padding(bottom = 32.dp)
|
|
||||||
.offset {
|
|
||||||
IntOffset(
|
|
||||||
fabOffsetX.roundToInt()
|
|
||||||
.coerceIn(-(screenWidthPx - fabSizePx - paddingPx * 2).toInt(), 0),
|
|
||||||
fabOffsetY.roundToInt()
|
|
||||||
.coerceIn(-(screenHeightPx - fabSizePx - paddingPx * 2 - navBarPx).toInt(), 0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectDragGestures(
|
|
||||||
onDragStart = { },
|
|
||||||
onDragEnd = { },
|
|
||||||
onDragCancel = { },
|
|
||||||
onDrag = { change, dragAmount ->
|
|
||||||
change.consume()
|
|
||||||
fabOffsetX += dragAmount.x
|
|
||||||
fabOffsetY += dragAmount.y
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
containerColor = Color(0xFF10B981)
|
|
||||||
) {
|
|
||||||
Text("AI", color = Color.White, style = MaterialTheme.typography.titleMedium)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对话框 - 展开后必须在界面内
|
|
||||||
if (isExpanded) {
|
|
||||||
var visible by remember { mutableStateOf(false) }
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
visible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// 👈 用 FAB 位置计算对话框位置,但强制钳制在屏幕内
|
|
||||||
// FAB右下角为基准:对话框应该出现在 FAB 附近
|
|
||||||
val fabRight = screenWidthPx - paddingPx * 2 + fabOffsetX // FAB右边缘
|
|
||||||
val fabBottom = screenHeightPx - paddingPx * 2 - navBarPx + fabOffsetY // FAB下边缘
|
|
||||||
|
|
||||||
// 对话框优先放在 FAB 左上方,如果放不下就贴边
|
|
||||||
val rawX = fabRight - cardWidthPx - paddingPx // FAB左边
|
|
||||||
val rawY = fabBottom - cardHeightPx - paddingPx // FAB上边
|
|
||||||
|
|
||||||
// 👈 关键:钳制到屏幕内
|
|
||||||
val clampedX = rawX.coerceIn(0f, screenWidthPx - cardWidthPx)
|
|
||||||
val clampedY = rawY.coerceIn(0f, screenHeightPx - cardHeightPx)
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = visible,
|
|
||||||
enter = fadeIn(animationSpec = tween(300)) + scaleIn(initialScale = 0.8f, animationSpec = tween(300)),
|
|
||||||
exit = fadeOut(animationSpec = tween(200)) + scaleOut(targetScale = 0.8f, animationSpec = tween(200)),
|
|
||||||
modifier = Modifier
|
|
||||||
.offset {
|
|
||||||
IntOffset(
|
|
||||||
(clampedX + cardDragX).roundToInt()
|
|
||||||
.coerceIn(0, (screenWidthPx - cardWidthPx).toInt()),
|
|
||||||
(clampedY + cardDragY).roundToInt()
|
|
||||||
.coerceIn(0, (screenHeightPx - cardHeightPx).toInt())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.width(maxWidth)
|
|
||||||
.height(maxHeight)
|
|
||||||
.shadow(8.dp, RoundedCornerShape(16.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.surface, RoundedCornerShape(16.dp))
|
|
||||||
.clip(RoundedCornerShape(16.dp))
|
|
||||||
) {
|
|
||||||
ChatScaffold(
|
|
||||||
onClose = {
|
|
||||||
visible = false
|
|
||||||
isExpanded = false
|
|
||||||
// 👈 关闭时把FAB偏移同步(对话框拖动带动FAB)
|
|
||||||
fabOffsetX += cardDragX
|
|
||||||
fabOffsetY += cardDragY
|
|
||||||
cardDragX = 0f
|
|
||||||
cardDragY = 0f
|
|
||||||
},
|
|
||||||
onDrag = { dx, dy ->
|
|
||||||
cardDragX += dx
|
|
||||||
cardDragY += dy
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
||||||
package com.stand.standapp.ui.chat.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.Menu
|
|
||||||
import androidx.compose.material.icons.filled.Send
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.stand.standapp.ui.chat.model.ChatMessage
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun ChatArea(
|
|
||||||
messages: List<ChatMessage>,
|
|
||||||
onSendMessage: (String) -> Unit,
|
|
||||||
onOpenDrawer: () -> Unit,
|
|
||||||
onClose: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onDrag: (Float, Float) -> Unit = { _, _ -> },
|
|
||||||
onCancelStreaming: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
var inputText by remember { mutableStateOf("") }
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
|
|
||||||
LaunchedEffect(messages.size) {
|
|
||||||
if (messages.isNotEmpty()) {
|
|
||||||
listState.scrollToItem(messages.size - 1, Int.MAX_VALUE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isAnyStreaming = remember(messages) { messages.any { it.isStreaming } }
|
|
||||||
if (isAnyStreaming) {
|
|
||||||
val totalLength = messages.map { it.content.length }.sum()
|
|
||||||
LaunchedEffect(totalLength) {
|
|
||||||
if (messages.isNotEmpty()) {
|
|
||||||
listState.scrollToItem(messages.size - 1, Int.MAX_VALUE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize().background(Color.White)) {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text("AI 助手", style = MaterialTheme.typography.titleMedium) },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onOpenDrawer) {
|
|
||||||
Icon(Icons.Default.Menu, contentDescription = "Menu")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
TextButton(onClick = onClose) { Text("关闭") }
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFFF3F4F6)),
|
|
||||||
modifier = Modifier.pointerInput(Unit) {
|
|
||||||
detectDragGestures(
|
|
||||||
onDragStart = { },
|
|
||||||
onDragEnd = { },
|
|
||||||
onDragCancel = { },
|
|
||||||
onDrag = { change, dragAmount ->
|
|
||||||
change.consume()
|
|
||||||
onDrag(dragAmount.x, dragAmount.y)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = listState,
|
|
||||||
modifier = Modifier.weight(1f).fillMaxWidth().padding(horizontal = 12.dp),
|
|
||||||
contentPadding = PaddingValues(vertical = 12.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
items(messages, key = { it.id }) { msg ->
|
|
||||||
val isUser = msg.role == ChatMessage.Role.USER
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
contentAlignment = if (isUser) Alignment.CenterEnd else Alignment.CenterStart
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = if (isUser) Color(0xFF10B981) else Color(0xFFE5E7EB),
|
|
||||||
contentColor = if (isUser) Color.White else Color.Black
|
|
||||||
) {
|
|
||||||
if (isUser) {
|
|
||||||
androidx.compose.ui.viewinterop.AndroidView(
|
|
||||||
factory = { ctx ->
|
|
||||||
SafeSelectableTextView(ctx).apply {
|
|
||||||
setTextColor(android.graphics.Color.WHITE)
|
|
||||||
textSize = 14f
|
|
||||||
setTextIsSelectable(true)
|
|
||||||
setPadding(0, 0, 0, 0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update = { view ->
|
|
||||||
view.text = msg.content
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(12.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val contentWithCursor = msg.content + if (msg.isStreaming) " █" else ""
|
|
||||||
MarkwonRenderer(
|
|
||||||
markdown = contentWithCursor,
|
|
||||||
modifier = Modifier.padding(12.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(8.dp)
|
|
||||||
.navigationBarsPadding()
|
|
||||||
.imePadding(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = inputText,
|
|
||||||
onValueChange = { inputText = it },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
placeholder = { Text("输入你想问的问题...") },
|
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
enabled = !isAnyStreaming
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (isAnyStreaming) {
|
|
||||||
onCancelStreaming()
|
|
||||||
} else if (inputText.isNotBlank()) {
|
|
||||||
onSendMessage(inputText)
|
|
||||||
inputText = ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier.background(
|
|
||||||
if (isAnyStreaming) Color.Red else Color(0xFF10B981),
|
|
||||||
RoundedCornerShape(50)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
if (isAnyStreaming) Icons.Default.Close else Icons.Default.Send,
|
|
||||||
contentDescription = if (isAnyStreaming) "停止" else "发送",
|
|
||||||
tint = Color.White
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MarkwonRenderer(markdown: String, modifier: Modifier) {
|
|
||||||
androidx.compose.ui.viewinterop.AndroidView(
|
|
||||||
factory = { ctx ->
|
|
||||||
val textView = SafeSelectableTextView(ctx).apply {
|
|
||||||
setTextColor(android.graphics.Color.parseColor("#1F2937"))
|
|
||||||
textSize = 14f
|
|
||||||
setTextIsSelectable(true) // 👈 启用选择复制功能
|
|
||||||
movementMethod = android.text.method.LinkMovementMethod.getInstance()
|
|
||||||
setLineSpacing(0f, 1.15f)
|
|
||||||
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
|
|
||||||
},
|
|
||||||
update = { view ->
|
|
||||||
try {
|
|
||||||
val markwon = view.tag as? io.noties.markwon.Markwon
|
|
||||||
if (markwon != null) {
|
|
||||||
markwon.setMarkdown(view, markdown)
|
|
||||||
} else {
|
|
||||||
view.text = markdown
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
view.text = markdown
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
package com.stand.standapp.ui.chat.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.stand.standapp.ui.chat.ChatViewModel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun ChatScaffold(
|
|
||||||
viewModel: ChatViewModel = viewModel(),
|
|
||||||
onClose: () -> Unit,
|
|
||||||
onDrag: (Float, Float) -> Unit = { _, _ -> }
|
|
||||||
) {
|
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val sessions by viewModel.sessions.collectAsState()
|
|
||||||
val currentSessionId by viewModel.currentSessionId.collectAsState()
|
|
||||||
val messages by viewModel.messages.collectAsState()
|
|
||||||
var showClearDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val currentMessages = messages.filter { it.sessionId == currentSessionId }
|
|
||||||
|
|
||||||
// 清除确认对话框
|
|
||||||
if (showClearDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showClearDialog = false },
|
|
||||||
title = { Text("清除所有记录") },
|
|
||||||
text = { Text("确定要清除所有对话记录吗?此操作不可撤销。") },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
viewModel.clearAllSessions()
|
|
||||||
showClearDialog = false
|
|
||||||
scope.launch { drawerState.close() }
|
|
||||||
}
|
|
||||||
) { Text("确定", color = Color.Red) }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = { showClearDialog = false }) { Text("取消") }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ModalNavigationDrawer(
|
|
||||||
drawerState = drawerState,
|
|
||||||
gesturesEnabled = false,
|
|
||||||
drawerContent = {
|
|
||||||
ModalDrawerSheet(modifier = Modifier.width(240.dp)) {
|
|
||||||
Column(Modifier.fillMaxSize().padding(12.dp)) {
|
|
||||||
// 👈 顶部关闭按钮
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text("历史对话", style = MaterialTheme.typography.titleSmall)
|
|
||||||
TextButton(onClick = { scope.launch { drawerState.close() } }) {
|
|
||||||
Text("关闭")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(8.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
viewModel.createNewSession()
|
|
||||||
scope.launch { drawerState.close() }
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
contentPadding = PaddingValues(vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
|
||||||
Spacer(Modifier.width(4.dp))
|
|
||||||
Text("新建话题", style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
|
|
||||||
LazyColumn(modifier = Modifier.weight(1f)) {
|
|
||||||
items(sessions, key = { it.id }) { session ->
|
|
||||||
val isSelected = session.id == currentSessionId
|
|
||||||
Surface(
|
|
||||||
onClick = {
|
|
||||||
viewModel.switchSession(session.id)
|
|
||||||
scope.launch { drawerState.close() }
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 4.dp),
|
|
||||||
color = if (isSelected) MaterialTheme.colorScheme.surfaceVariant else Color.Transparent,
|
|
||||||
shape = RoundedCornerShape(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = session.title,
|
|
||||||
modifier = Modifier.padding(12.dp),
|
|
||||||
maxLines = 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 底部清除按钮
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = { showClearDialog = true },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = Color.Red)
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Delete, contentDescription = null, tint = Color.Red)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text("清除所有记录")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
ChatArea(
|
|
||||||
messages = currentMessages,
|
|
||||||
onSendMessage = { viewModel.sendMessage(it) },
|
|
||||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
|
||||||
onClose = onClose,
|
|
||||||
onDrag = onDrag,
|
|
||||||
onCancelStreaming = { viewModel.cancelActiveStreaming() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package com.stand.standapp.ui.chat.components
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.TextView
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安全的可选择文本 TextView,捕获 setTextIsSelectable 导致的 IndexOutOfBoundsException 崩溃。
|
|
||||||
* 问题根源:当文本更新时用户正在拖动选择手柄,resetDragAcceleratorState 尝试设置无效范围 (-1, -1)。
|
|
||||||
*/
|
|
||||||
class SafeSelectableTextView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : TextView(context, attrs, defStyleAttr) {
|
|
||||||
|
|
||||||
override fun onTouchEvent(event: android.view.MotionEvent): Boolean {
|
|
||||||
return try {
|
|
||||||
super.onTouchEvent(event)
|
|
||||||
} catch (e: IndexOutOfBoundsException) {
|
|
||||||
// 吞掉异常,避免崩溃
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
package com.stand.standapp.ui.chat.model
|
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
data class ChatSession(
|
|
||||||
val id: String = UUID.randomUUID().toString(),
|
|
||||||
val title: String = "新对话",
|
|
||||||
val timestamp: Long = System.currentTimeMillis()
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ChatMessage(
|
|
||||||
val id: String = UUID.randomUUID().toString(),
|
|
||||||
val sessionId: String,
|
|
||||||
val role: Role,
|
|
||||||
val content: String,
|
|
||||||
val isStreaming: Boolean = false
|
|
||||||
) {
|
|
||||||
enum class Role {
|
|
||||||
USER, AI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
package com.stand.standapp.ui.chat.repo
|
|
||||||
|
|
||||||
import com.stand.standapp.AppConfig
|
|
||||||
import android.content.Context
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class AiChatRepository {
|
|
||||||
private val client = OkHttpClient.Builder()
|
|
||||||
.readTimeout(0, TimeUnit.MILLISECONDS) // 禁用读取超时以适配 SSE 流
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun streamChat(context: Context, message: String, token: String, historyJson: String): Flow<String> = callbackFlow {
|
|
||||||
// 1. 从 AppConfig 中动态读取当前服务器基准地址,完美适配真实网关
|
|
||||||
val serverUrl = AppConfig.getServerUrl(context)
|
|
||||||
val requestUrl = "$serverUrl/api/chat/stream"
|
|
||||||
|
|
||||||
val jsonRequestBody = if (historyJson.isNotBlank()) {
|
|
||||||
"""{"message": "$message", "history": $historyJson}"""
|
|
||||||
} else {
|
|
||||||
"""{"message": "$message"}"""
|
|
||||||
}
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url(requestUrl)
|
|
||||||
.post(jsonRequestBody.toRequestBody("application/json".toMediaType()))
|
|
||||||
.header("Authorization", "Bearer $token")
|
|
||||||
.header("Accept", "text/event-stream")
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val call = client.newCall(request)
|
|
||||||
|
|
||||||
// 2. 在 IO 线程池中执行网络流式读取,杜绝 EventSource 找不到依赖的编译期痛点
|
|
||||||
val job = CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
try {
|
|
||||||
call.execute().use { response ->
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
trySend("[请求失败: 状态码 ${response.code}]")
|
|
||||||
close()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
val body = response.body
|
|
||||||
if (body == null) {
|
|
||||||
trySend("[空响应]")
|
|
||||||
close()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 逐行读取后端流式推送字符,手动解析 data 属性
|
|
||||||
// 👈 添加行缓冲,确保完整的markdown行被发送
|
|
||||||
BufferedReader(InputStreamReader(body.byteStream(), Charsets.UTF_8)).use { reader ->
|
|
||||||
var line: String?
|
|
||||||
var lastcontent = "";
|
|
||||||
while (reader.readLine().also { line = it } != null) {
|
|
||||||
val curLine = line ?: continue
|
|
||||||
if (curLine.startsWith("data:")) {
|
|
||||||
val content = curLine.substring(5)
|
|
||||||
// Timber.d("内容111111111:"+content)
|
|
||||||
if (content.contains("|") && lastcontent.contains("|")){
|
|
||||||
trySend("\n")
|
|
||||||
// Timber.d("1发送11111111111:换行")
|
|
||||||
trySend(content)
|
|
||||||
// Timber.d("2发送11111111111:"+content)
|
|
||||||
}else if (content.length==0) {
|
|
||||||
// 空data行表示换行
|
|
||||||
trySend("\n")
|
|
||||||
// Timber.d("3发送11111111111:换行")
|
|
||||||
}else {
|
|
||||||
trySend(content)
|
|
||||||
// Timber.d("4发送11111111111:"+content)
|
|
||||||
}
|
|
||||||
lastcontent = content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "SSE Stream error")
|
|
||||||
trySend("\n[连接异常: ${e.message}]")
|
|
||||||
close(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitClose {
|
|
||||||
call.cancel()
|
|
||||||
job.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
package com.stand.standapp.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
object AppKiller {
|
|
||||||
|
|
||||||
private var isKilling = false
|
|
||||||
|
|
||||||
fun killApp(context: Context, isCrash: Boolean = false, restart: Boolean = false) {
|
|
||||||
if (isKilling) return
|
|
||||||
isKilling = true
|
|
||||||
|
|
||||||
Timber.tag("AppKiller").i("Starting app cleanup. isCrash: $isCrash, restart: $restart")
|
|
||||||
|
|
||||||
try {
|
|
||||||
GpioManager.stopListening()
|
|
||||||
Timber.tag("AppKiller").i("GPIO stopped")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("AppKiller").e(e, "GPIO stop failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
com.example.kingway.ptt.SendAtUtil.toCancelLogin()
|
|
||||||
com.example.kingway.ptt.SendAtUtil.toLogout()
|
|
||||||
Timber.tag("AppKiller").i("PTT logged out")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("AppKiller").e(e, "PTT logout failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
com.stand.standapp.printer.PrinterManager.getFactory()?.ClosePort()
|
|
||||||
Timber.tag("AppKiller").i("Printer port closed")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("AppKiller").e(e, "Printer close failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.tag("AppKiller").i("Cleanup finished. Killing process.")
|
|
||||||
|
|
||||||
if (!isCrash) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(50)
|
|
||||||
} catch (e: Exception) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// LogManager.shutdown() 会关闭其内部的线程池
|
|
||||||
// 调用后绝对不能再使用 Timber 打印日志,否则会抛出 RejectedExecutionException 导致死循环
|
|
||||||
LogManager.shutdown()
|
|
||||||
} catch (e: Exception) {}
|
|
||||||
|
|
||||||
if (restart) {
|
|
||||||
try {
|
|
||||||
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
||||||
if (intent != null) {
|
|
||||||
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
|
||||||
val flags = android.app.PendingIntent.FLAG_ONE_SHOT or android.app.PendingIntent.FLAG_IMMUTABLE
|
|
||||||
val pendingIntent = android.app.PendingIntent.getActivity(context, 223344, intent, flags)
|
|
||||||
val mgr = context.getSystemService(Context.ALARM_SERVICE) as android.app.AlarmManager
|
|
||||||
mgr.set(android.app.AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
android.os.Process.killProcess(android.os.Process.myPid())
|
|
||||||
System.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setupUncaughtExceptionHandler(context: Context) {
|
|
||||||
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
|
||||||
Timber.tag("AppKiller").e(throwable, "Uncaught exception detected! Force cleanup.")
|
|
||||||
killApp(context, true)
|
|
||||||
defaultHandler?.uncaughtException(thread, throwable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import com.stand.standapp.MainActivity
|
import com.stand.standapp.MainActivity
|
||||||
import com.stand.standapp.TempCallActivity
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -13,7 +12,6 @@ import timber.log.Timber
|
||||||
*/
|
*/
|
||||||
object GpioManager {
|
object GpioManager {
|
||||||
private var mZysjSystemManager: ZysjSystemManager? = null
|
private var mZysjSystemManager: ZysjSystemManager? = null
|
||||||
@Volatile
|
|
||||||
private var mIsRunning = false
|
private var mIsRunning = false
|
||||||
private var mThread: Thread? = null
|
private var mThread: Thread? = null
|
||||||
private var lastGpioValue = 1 // 默认松开状态 (1)
|
private var lastGpioValue = 1 // 默认松开状态 (1)
|
||||||
|
|
@ -56,7 +54,6 @@ object GpioManager {
|
||||||
|
|
||||||
fun stopListening() {
|
fun stopListening() {
|
||||||
mIsRunning = false
|
mIsRunning = false
|
||||||
mThread?.interrupt()
|
|
||||||
mThread = null
|
mThread = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,16 +73,10 @@ object GpioManager {
|
||||||
Timber.tag("GPIO").d("PTT Speak Play (Hardware)")
|
Timber.tag("GPIO").d("PTT Speak Play (Hardware)")
|
||||||
// 统一使用 updatePttTalkStatus 更新 UI
|
// 统一使用 updatePttTalkStatus 更新 UI
|
||||||
MainActivity.executeJs("updatePttTalkStatus(true)")
|
MainActivity.executeJs("updatePttTalkStatus(true)")
|
||||||
// 同步 TempCallActivity
|
|
||||||
TempCallActivity.instance?.onGpioPressed()
|
|
||||||
Timber.tag("GPIO").d("Notified TempCallActivity: onGpioPressed")
|
|
||||||
} else {
|
|
||||||
Timber.tag("GPIO").w("GPIO pressed but isSpeaking already true — ignoring")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 硬件松开 PTT
|
// 硬件松开 PTT
|
||||||
Timber.tag("GPIO").d("GPIO released, isSpeaking=%s", isSpeaking)
|
|
||||||
if (isSpeaking) {
|
if (isSpeaking) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val duration = now - lastSpeakTime
|
val duration = now - lastSpeakTime
|
||||||
|
|
@ -111,11 +102,6 @@ object GpioManager {
|
||||||
Timber.tag("GPIO").d("PTT Speak Release (Hardware)")
|
Timber.tag("GPIO").d("PTT Speak Release (Hardware)")
|
||||||
// 统一使用 updatePttTalkStatus 更新 UI
|
// 统一使用 updatePttTalkStatus 更新 UI
|
||||||
MainActivity.executeJs("updatePttTalkStatus(false)")
|
MainActivity.executeJs("updatePttTalkStatus(false)")
|
||||||
// 同步 TempCallActivity
|
|
||||||
TempCallActivity.instance?.onGpioReleased()
|
|
||||||
Timber.tag("GPIO").d("Notified TempCallActivity: onGpioReleased")
|
|
||||||
} else {
|
|
||||||
Timber.tag("GPIO").w("GPIO realRelease skipped: already released")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -161,16 +161,6 @@ object LogManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun shutdown() {
|
|
||||||
Timber.tag("LogManager").i("Shutting down...")
|
|
||||||
try {
|
|
||||||
executor.shutdown()
|
|
||||||
executor.awaitTermination(3, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.tag("LogManager").e(e, "Executor shutdown failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uploadLogs(context: Context, callback: (Boolean, String) -> Unit) {
|
fun uploadLogs(context: Context, callback: (Boolean, String) -> Unit) {
|
||||||
executor.execute {
|
executor.execute {
|
||||||
val zipPath = zipLogs(context) ?: return@execute callback(false, "打包失败")
|
val zipPath = zipLogs(context) ?: return@execute callback(false, "打包失败")
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<gradient
|
<gradient
|
||||||
android:angle="135"
|
android:angle="45"
|
||||||
android:startColor="#020810"
|
android:endColor="#004A8F"
|
||||||
android:centerColor="#051120"
|
android:startColor="#001F3F"
|
||||||
android:endColor="#091D35"
|
|
||||||
android:type="linear" />
|
android:type="linear" />
|
||||||
</shape>
|
</shape>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<solid android:color="#66000000" />
|
|
||||||
<corners android:radius="16dp" />
|
|
||||||
<padding
|
|
||||||
android:left="8dp"
|
|
||||||
android:right="8dp"
|
|
||||||
android:top="4dp"
|
|
||||||
android:bottom="4dp" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#EF4444" />
|
|
||||||
<size android:width="64dp" android:height="64dp" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#66FFFFFF" />
|
|
||||||
<size
|
|
||||||
android:width="40dp"
|
|
||||||
android:height="40dp" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#3B82F6" />
|
|
||||||
<size android:width="150dp" android:height="150dp" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#223B82F6" />
|
|
||||||
<size android:width="180dp" android:height="180dp" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#333B82F6" />
|
|
||||||
<size android:width="180dp" android:height="180dp" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#2563EB" />
|
|
||||||
<size android:width="150dp" android:height="150dp" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#10B981" />
|
|
||||||
<size android:width="10dp" android:height="10dp" />
|
|
||||||
</shape>
|
|
||||||
|
Before Width: | Height: | Size: 73 KiB |
|
|
@ -0,0 +1,38 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="200dp"
|
||||||
|
android:height="200dp"
|
||||||
|
android:viewportWidth="200"
|
||||||
|
android:viewportHeight="200">
|
||||||
|
|
||||||
|
<!-- 背景圆盘 (纯净白) -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M100,100m-90,0a90,90 0,1 1,180 0a90,90 0,1 1,-180 0" />
|
||||||
|
|
||||||
|
<!-- 消防盾牌外廓 (消防红) -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#D62828"
|
||||||
|
android:pathData="M100,40 L155,62 L155,115 C155,145 130,170 100,180 C70,170 45,145 45,115 L45,62 Z" />
|
||||||
|
|
||||||
|
<!-- 盾牌内嵌线条 (深红) -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#9B2226"
|
||||||
|
android:pathData="M100,50 L145,68 L145,115 C145,140 125,160 100,170 C75,160 55,140 55,115 L55,68 Z" />
|
||||||
|
|
||||||
|
<!-- 核心星芒/十字 (象征调度指挥) -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M100,75 L112,98 L138,98 L118,115 L125,140 L100,125 L75,140 L82,115 L62,98 L88,98 Z" />
|
||||||
|
|
||||||
|
<!-- 外部环绕科技蓝装饰条 -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#004A8F"
|
||||||
|
android:strokeWidth="5"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M100,20 A80,80 0,0 1,180,100" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#004A8F"
|
||||||
|
android:strokeWidth="5"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M100,180 A80,80 0,0 1,20,100" />
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#004A8F"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
|
||||||
|
<!-- 消防盾牌图标,适配 Android 108dp 规范 -->
|
||||||
|
<group
|
||||||
|
android:translateX="18.5"
|
||||||
|
android:translateY="18.5"
|
||||||
|
android:scaleX="0.355"
|
||||||
|
android:scaleY="0.355">
|
||||||
|
|
||||||
|
<!-- 背景白底,确保中心可见 -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M100,100m-90,0a90,90 0,1 1,180 0a90,90 0,1 1,-180 0" />
|
||||||
|
|
||||||
|
<!-- 消防盾牌外廓 -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#D62828"
|
||||||
|
android:pathData="M100,40 L155,62 L155,115 C155,145 130,170 100,180 C70,170 45,145 45,115 L45,62 Z" />
|
||||||
|
|
||||||
|
<!-- 盾牌内嵌线条 -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#9B2226"
|
||||||
|
android:pathData="M100,50 L145,68 L145,115 C145,140 125,160 100,170 C75,160 55,140 55,115 L55,68 Z" />
|
||||||
|
|
||||||
|
<!-- 核心星芒/十字 -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M100,75 L112,98 L138,98 L118,115 L125,140 L100,125 L75,140 L82,115 L62,98 L88,98 Z" />
|
||||||
|
|
||||||
|
<!-- 科技蓝环绕装饰 -->
|
||||||
|
<path
|
||||||
|
android:strokeColor="#004A8F"
|
||||||
|
android:strokeWidth="5"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M100,20 A80,80 0,0 1,180,100" />
|
||||||
|
<path
|
||||||
|
android:strokeColor="#004A8F"
|
||||||
|
android:strokeWidth="5"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:pathData="M100,180 A80,80 0,0 1,20,100" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 68 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<item android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</layer-list>
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<gradient
|
<solid android:color="#3B82F6" />
|
||||||
android:angle="0"
|
<corners android:radius="8dp" />
|
||||||
android:startColor="#0055AA"
|
|
||||||
android:endColor="#00AAEE"
|
|
||||||
android:type="linear" />
|
|
||||||
<corners android:radius="2dp" />
|
|
||||||
<stroke android:width="1dp" android:color="#00CFFF" />
|
|
||||||
</shape>
|
</shape>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<solid android:color="@android:color/transparent" />
|
|
||||||
<corners android:radius="2dp" />
|
|
||||||
<stroke android:width="1dp" android:color="#1A4A6B" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#00BFFF" />
|
|
||||||
</shape>
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<solid android:color="#0D2035" />
|
<solid android:color="#FFFFFF" />
|
||||||
<corners android:radius="2dp" />
|
<corners android:radius="8dp" />
|
||||||
<stroke android:width="1dp" android:color="#1A4A6B" />
|
|
||||||
</shape>
|
</shape>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<solid android:color="#22FFFFFF" />
|
||||||
<!-- 深色半透明背景 -->
|
<corners android:radius="12dp" />
|
||||||
<item>
|
<stroke android:width="1dp" android:color="#44FFFFFF" />
|
||||||
<shape>
|
</shape>
|
||||||
<solid android:color="#D0071525" />
|
|
||||||
<corners android:radius="4dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
<!-- 青色发光边框 -->
|
|
||||||
<item>
|
|
||||||
<shape>
|
|
||||||
<solid android:color="@android:color/transparent" />
|
|
||||||
<stroke android:width="1dp" android:color="#00BFFF" />
|
|
||||||
<corners android:radius="4dp" />
|
|
||||||
</shape>
|
|
||||||
</item>
|
|
||||||
</layer-list>
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@drawable/bg_login">
|
android:background="@drawable/bg_login"
|
||||||
|
android:padding="20dp">
|
||||||
|
|
||||||
<!-- 服务器配置入口(右上角) -->
|
<!-- 服务器配置入口 (右上角) -->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/iv_settings"
|
android:id="@+id/iv_settings"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
|
|
@ -14,280 +15,128 @@
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:src="@android:drawable/ic_menu_manage"
|
android:src="@android:drawable/ic_menu_manage"
|
||||||
android:tint="#2A6A8A"
|
|
||||||
android:contentDescription="配置服务器" />
|
android:contentDescription="配置服务器" />
|
||||||
|
|
||||||
<!-- 顶部 LOGO + 标题区域 -->
|
<!-- 顶部标题 -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/ll_header"
|
android:id="@+id/ll_header"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="28dp"
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<!-- LOGO + 两侧装饰线 -->
|
<ImageView
|
||||||
<LinearLayout
|
android:layout_width="80dp"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="80dp"
|
||||||
android:layout_height="wrap_content"
|
android:src="@drawable/ic_fire_logo" />
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="50dp"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="#00BFFF" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="5dp"
|
|
||||||
android:layout_height="5dp"
|
|
||||||
android:layout_marginLeft="6dp"
|
|
||||||
android:layout_marginRight="6dp"
|
|
||||||
android:background="@drawable/shape_dot_accent" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="56dp"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
android:layout_marginLeft="10dp"
|
|
||||||
android:layout_marginRight="10dp"
|
|
||||||
android:src="@drawable/ic_fire_logo"
|
|
||||||
android:contentDescription="Logo" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="5dp"
|
|
||||||
android:layout_height="5dp"
|
|
||||||
android:layout_marginLeft="6dp"
|
|
||||||
android:layout_marginRight="6dp"
|
|
||||||
android:background="@drawable/shape_dot_accent" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="50dp"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="#00BFFF" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- 系统名称 -->
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:text="消防值守台"
|
android:text="消防值守台"
|
||||||
android:textColor="#00CFFF"
|
android:textColor="#FFFFFF"
|
||||||
android:textSize="26sp"
|
android:textSize="26sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold" />
|
||||||
android:letterSpacing="0.15" />
|
|
||||||
|
|
||||||
<!-- 英文副标题 -->
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:text="FIRE WATCH MONITORING PLATFORM"
|
|
||||||
android:textColor="#2A6A8A"
|
|
||||||
android:textSize="10sp"
|
|
||||||
android:letterSpacing="0.1" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- 登录卡片(居中) -->
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true">
|
android:layout_centerInParent="true">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/login_card"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/shape_login_box"
|
android:background="@drawable/shape_login_box"
|
||||||
android:elevation="8dp"
|
android:elevation="8dp"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="28dp"
|
android:padding="30dp"
|
||||||
app:layout_constraintWidth_max="450dp"
|
app:layout_constraintWidth_max="450dp"
|
||||||
app:layout_constraintWidth_default="percent"
|
app:layout_constraintWidth_default="percent"
|
||||||
app:layout_constraintWidth_percent="0.88"
|
app:layout_constraintWidth_percent="0.85"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent">
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
|
||||||
<!-- 卡片标题(带装饰线) -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_marginBottom="20dp">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:background="#1A4A6B" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="4dp"
|
|
||||||
android:layout_height="4dp"
|
|
||||||
android:layout_marginLeft="4dp"
|
|
||||||
android:layout_marginRight="4dp"
|
|
||||||
android:background="@drawable/shape_dot_accent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingLeft="10dp"
|
|
||||||
android:paddingRight="10dp"
|
|
||||||
android:text="用 户 登 录"
|
|
||||||
android:textColor="#00CFFF"
|
|
||||||
android:textSize="15sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="4dp"
|
|
||||||
android:layout_height="4dp"
|
|
||||||
android:layout_marginLeft="4dp"
|
|
||||||
android:layout_marginRight="4dp"
|
|
||||||
android:background="@drawable/shape_dot_accent" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:background="#1A4A6B" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- 测试账号提示 -->
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="25dp"
|
||||||
android:text="测试账号 test010 test011 密码: 123456"
|
android:text="用户登录"
|
||||||
android:textColor="#2A6A8A"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
|
|
||||||
<!-- 用户名输入框 -->
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
android:layout_marginBottom="14dp"
|
|
||||||
android:background="@drawable/shape_edit_text">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="44dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:text="账号"
|
|
||||||
android:textColor="#2A7A9A"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:layout_alignParentStart="true" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="1dp"
|
|
||||||
android:layout_height="20dp"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="44dp"
|
|
||||||
android:background="#1A4A6B" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/et_username"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginStart="52dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:hint="请输入用户名"
|
|
||||||
android:paddingRight="12dp"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textColor="#DDEEFF"
|
|
||||||
android:textColorHint="#2A6A8A"
|
|
||||||
android:textSize="14sp" />
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<!-- 密码输入框 -->
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
android:layout_marginBottom="14dp"
|
|
||||||
android:background="@drawable/shape_edit_text">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="44dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:text="密码"
|
|
||||||
android:textColor="#2A7A9A"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:layout_alignParentStart="true" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_width="1dp"
|
|
||||||
android:layout_height="20dp"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginStart="44dp"
|
|
||||||
android:background="#1A4A6B" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/et_password"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginStart="52dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:hint="请输入密码"
|
|
||||||
android:inputType="textPassword"
|
|
||||||
android:paddingRight="12dp"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textColor="#DDEEFF"
|
|
||||||
android:textColorHint="#2A6A8A"
|
|
||||||
android:textSize="14sp" />
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<!-- 记住登录 -->
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="20dp">
|
|
||||||
|
|
||||||
<CheckBox
|
|
||||||
android:id="@+id/cb_remember"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="记住登录"
|
|
||||||
android:textColor="#3A7A9A"
|
|
||||||
android:textSize="13sp" />
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<!-- 登录按钮 -->
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btn_login"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_marginBottom="10dp"
|
|
||||||
android:background="@drawable/shape_button_login"
|
|
||||||
android:text="登 录"
|
|
||||||
android:textColor="#FFFFFF"
|
android:textColor="#FFFFFF"
|
||||||
android:textSize="15sp"
|
android:textSize="20sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:letterSpacing="0.2" />
|
|
||||||
|
|
||||||
<!-- 正式界面登录按钮 -->
|
<TextView
|
||||||
<Button
|
android:layout_width="wrap_content"
|
||||||
android:id="@+id/btn_login_official"
|
android:layout_height="wrap_content"
|
||||||
android:layout_width="match_parent"
|
android:layout_marginBottom="15dp"
|
||||||
android:layout_height="44dp"
|
android:text="测试账号: admin 密码: 123456"
|
||||||
android:layout_marginBottom="8dp"
|
android:textColor="#CCFFFFFF"
|
||||||
android:background="@drawable/shape_button_secondary"
|
android:textSize="14sp" />
|
||||||
android:text="正式界面登录"
|
|
||||||
android:textColor="#3A7A9A"
|
|
||||||
android:textSize="14sp" />
|
|
||||||
|
|
||||||
<!-- 退出应用 -->
|
<EditText
|
||||||
<Button
|
android:id="@+id/et_username"
|
||||||
android:id="@+id/btn_exit"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="50dp"
|
||||||
android:layout_height="36dp"
|
android:layout_marginBottom="15dp"
|
||||||
android:background="@android:color/transparent"
|
android:background="@drawable/shape_edit_text"
|
||||||
android:text="退出应用"
|
android:hint="请输入用户名"
|
||||||
android:textColor="#1A4A6B"
|
android:paddingLeft="15dp"
|
||||||
android:textSize="13sp" />
|
android:paddingRight="15dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:textColorHint="#999999" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:background="@drawable/shape_edit_text"
|
||||||
|
android:hint="请输入密码"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:paddingLeft="15dp"
|
||||||
|
android:paddingRight="15dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:textColorHint="#999999" />
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="25dp">
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/cb_remember"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="记住登录"
|
||||||
|
android:textColor="#FFFFFF" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_login"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="55dp"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:background="@drawable/shape_button_login"
|
||||||
|
android:text="登录"
|
||||||
|
android:textColor="#FFFFFF"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_exit"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:text="退出应用"
|
||||||
|
android:textColor="#CCFFFFFF"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
@ -299,10 +148,10 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="20dp"
|
||||||
android:text="Copyright © 2020-2026 Telewave. All Rights Reserved."
|
android:text="Copyright © 2020-2026 Telewave. All Rights Reserved."
|
||||||
android:textColor="#1A4A6B"
|
android:textColor="#88FFFFFF"
|
||||||
android:textSize="11sp"
|
android:textSize="12sp"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true" />
|
android:focusable="true" />
|
||||||
|
|
|
||||||
|
|
@ -5,152 +5,53 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context="com.example.kingway.ptt.pttActivity">
|
tools:context="com.example.kingway.ptt.pttActivity">
|
||||||
|
<LinearLayout
|
||||||
<ScrollView
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
>
|
||||||
|
|
||||||
<LinearLayout
|
<Button
|
||||||
|
android:id="@+id/login"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:text="login"/>
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
<Button
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/pttBtn"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:text="服务器IP"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="14sp"
|
android:text="PTT" />
|
||||||
android:layout_marginBottom="4dp"/>
|
|
||||||
|
|
||||||
<AutoCompleteTextView
|
<Button
|
||||||
android:id="@+id/etIp"
|
android:id="@+id/button2"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
android:hint="服务器IP"
|
android:text="获取群组" />
|
||||||
android:inputType="text"
|
|
||||||
android:text="220.154.131.231"
|
|
||||||
android:dropDownWidth="match_parent"
|
|
||||||
android:layout_marginBottom="8dp"/>
|
|
||||||
|
|
||||||
<TextView
|
<Button
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/button3"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:text="账号"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="14sp"
|
android:text="获取成员" />
|
||||||
android:layout_marginBottom="4dp"/>
|
|
||||||
|
|
||||||
<AutoCompleteTextView
|
<Button
|
||||||
android:id="@+id/etAccount"
|
android:id="@+id/btnTmpCall"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="账号"
|
android:text="开始单呼" />
|
||||||
android:inputType="text"
|
|
||||||
android:text="test004"
|
|
||||||
android:dropDownWidth="match_parent"
|
|
||||||
android:layout_marginBottom="8dp"/>
|
|
||||||
|
|
||||||
<TextView
|
<Button
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/btnTmpCallStop"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:text="密码"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="14sp"
|
android:text="结束单呼" />
|
||||||
android:layout_marginBottom="4dp"/>
|
|
||||||
|
|
||||||
<AutoCompleteTextView
|
<Button
|
||||||
android:id="@+id/etPassword"
|
android:id="@+id/btnJumpGroup"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="密码"
|
android:text="切换群组" />
|
||||||
android:inputType="textPassword"
|
</LinearLayout>
|
||||||
android:text="123456"
|
|
||||||
android:dropDownWidth="match_parent"
|
|
||||||
android:layout_marginBottom="8dp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="群组ID"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:layout_marginBottom="4dp"/>
|
|
||||||
|
|
||||||
<AutoCompleteTextView
|
|
||||||
android:id="@+id/etGroupId"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="群组ID"
|
|
||||||
android:inputType="text"
|
|
||||||
android:text="00000001"
|
|
||||||
android:dropDownWidth="match_parent"
|
|
||||||
android:layout_marginBottom="16dp"/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/login"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="默认登录(gzzdweb01)"/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnCustomLogin"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="自定义登录"/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/pttBtn"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="PTT" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/button2"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:text="获取群组" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/button3"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="获取成员" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnTmpCall"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="开始单呼" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnTmpCallStop"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="结束单呼" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnJumpGroup"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="切换群组" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnCancel1"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="取消登录" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnCancel2"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="注销" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/login1"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="login1" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
||||||
|
|
@ -1,183 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="@drawable/bg_login">
|
|
||||||
|
|
||||||
<!-- 最小化按钮 (右上角,明显的按钮样式) -->
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/ll_minimize"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_marginTop="40dp"
|
|
||||||
android:layout_marginRight="16dp"
|
|
||||||
android:background="@drawable/btn_minimize_bg"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:paddingLeft="12dp"
|
|
||||||
android:paddingRight="12dp"
|
|
||||||
android:paddingTop="6dp"
|
|
||||||
android:paddingBottom="6dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:src="@android:drawable/ic_menu_close_clear_cancel"
|
|
||||||
android:tint="#FFFFFF" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/btn_minimize"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginLeft="4dp"
|
|
||||||
android:text="最小化"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:textSize="12sp" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- 顶部状态栏区域 -->
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/ll_top"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingTop="50dp"
|
|
||||||
android:paddingBottom="16dp">
|
|
||||||
|
|
||||||
<!-- 临时呼叫图标 -->
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="36dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:src="@android:drawable/ic_menu_call"
|
|
||||||
android:tint="#FFFFFF" />
|
|
||||||
|
|
||||||
<!-- 状态文字 -->
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_status"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:text="临时呼叫中"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:letterSpacing="0.1" />
|
|
||||||
|
|
||||||
<!-- 脉冲动画指示器 -->
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/iv_pulse"
|
|
||||||
android:layout_width="10dp"
|
|
||||||
android:layout_height="10dp"
|
|
||||||
android:layout_marginTop="6dp"
|
|
||||||
android:src="@drawable/circle_pulse" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- 中间信息卡片 -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:layout_marginLeft="24dp"
|
|
||||||
android:layout_marginRight="24dp"
|
|
||||||
android:background="@drawable/shape_login_box"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="24dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="临时群组"
|
|
||||||
android:textColor="#99FFFFFF"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:letterSpacing="0.15" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_group_name"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:text="临时群组"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:textSize="22sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
|
||||||
<View
|
|
||||||
android:layout_width="60dp"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:background="#33FFFFFF" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_talk_info"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="按下按钮讲话"
|
|
||||||
android:textColor="#CCFFFFFF"
|
|
||||||
android:textSize="14sp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_talk_status"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:text=""
|
|
||||||
android:textColor="#10B981"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<!-- 底部按钮区域 -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="48dp">
|
|
||||||
|
|
||||||
<!-- PTT 大按钮 -->
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="32dp">
|
|
||||||
|
|
||||||
<!-- 外圈光晕 -->
|
|
||||||
<View
|
|
||||||
android:id="@+id/view_glow"
|
|
||||||
android:layout_width="180dp"
|
|
||||||
android:layout_height="180dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:background="@drawable/circle_ptt_glow" />
|
|
||||||
|
|
||||||
<!-- PTT 按钮 -->
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btn_ptt"
|
|
||||||
android:layout_width="150dp"
|
|
||||||
android:layout_height="150dp"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:background="@drawable/circle_ptt_button"
|
|
||||||
android:text="按住\n说话"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:textSize="22sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<!-- 挂断按钮 -->
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btn_hangup"
|
|
||||||
android:layout_width="64dp"
|
|
||||||
android:layout_height="64dp"
|
|
||||||
android:background="@drawable/circle_hangup_button"
|
|
||||||
android:text="挂断"
|
|
||||||
android:textColor="#FFFFFF"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
</LinearLayout>
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 7.6 KiB |