Compare commits

..

12 Commits

11 changed files with 559 additions and 195 deletions

View File

@ -92,12 +92,13 @@
</div>
<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;">
<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="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" style="margin:0; padding:10px; font-size:13px; background: #0ea5e9; grid-column: span 2;" onclick="testES6Features()">ES6 引擎特性测试</button>
</div>
</div>
@ -418,9 +419,19 @@
}
// 上传日志
var isUploading = false;
function callUploadLogs() {
if (isUploading) {
alert("正在上传中请耐心等待30秒内请勿重复点击...");
return;
}
if (window.android && window.android.uploadLogs) {
isUploading = true;
alert("开始上传日志,请耐心等待...");
window.android.uploadLogs();
setTimeout(function() {
isUploading = false;
}, 30000);
} else {
alert("上传日志接口不可用");
}
@ -460,7 +471,68 @@
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;
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() {
var tag = document.getElementById('inject-status');
if (tag) {

View File

@ -97,7 +97,7 @@ public class CallBackResolution {
case "80"://返回群组信息
Log.d(TAG, "at_OEM_AT_cb get group info: " + lisHex);
String strGroup = "";
StringBuilder strGroup = new StringBuilder();
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 groupCount = Integer.valueOf((lisHex.get(10) + "" + lisHex.get(11)), 16);
@ -108,9 +108,9 @@ public class CallBackResolution {
size = (size / 2) - 1;
for (int i = 0; i < size; i++) {
if (i == 0) {
strGroup += ngx + lisHex.get(i + numB) + "" + lisHex.get(i + numB - 1);
strGroup.append(ngx).append(lisHex.get(i + numB)).append(lisHex.get(i + numB - 1));
} else {
strGroup += ngx + lisHex.get(i + numB + 1) + "" + lisHex.get(i + numB);
strGroup.append(ngx).append(lisHex.get(i + numB + 1)).append(lisHex.get(i + numB));
numB = numB + 1;
}
}
@ -118,7 +118,7 @@ public class CallBackResolution {
e.printStackTrace();
} finally {
String tempTxt = "";
tempTxt = unicodeToString(strGroup);
tempTxt = unicodeToString(strGroup.toString());
Log.i(TAG, " 组名称 " + tempTxt);
CallBackUtil.callBackGroupInfo(groupId, tempTxt, groupNo, groupCount);
Log.d(TAG, "at_OEM_AT_cb group info groupId: " + groupId + ", groupNo: " + groupNo+", groupCount: " +groupCount);
@ -129,7 +129,7 @@ public class CallBackResolution {
break;
case "81"://返回群成员信息
String groupMemberName = "";
StringBuilder groupMemberName = new StringBuilder();
String status = lisHex.get(1);
boolean haveVideo = lisHex.get(10).equals("01") ? true : false;
String memberId = lisHex.get(6) + "" + lisHex.get(7) + "" + lisHex.get(8) + "" + lisHex.get(9);
@ -141,16 +141,16 @@ public class CallBackResolution {
size = (size / 2) - 1;
for (int i = 0; i < size; i++) {
if (i == 0) {
groupMemberName += ngx + lisHex.get(i + numB) + "" + lisHex.get(i + numB - 1);
groupMemberName.append(ngx).append(lisHex.get(i + numB)).append(lisHex.get(i + numB - 1));
} else {
groupMemberName += ngx + lisHex.get(i + numB + 1) + "" + lisHex.get(i + numB);
groupMemberName.append(ngx).append(lisHex.get(i + numB + 1)).append(lisHex.get(i + numB));
numB = numB + 1;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
String tempTxt = unicodeToString(groupMemberName);
String tempTxt = unicodeToString(groupMemberName.toString());
GroupMemberInfoDto infoDto = new GroupMemberInfoDto(memberId, tempTxt, status, memberNo, haveVideo);
if (memberNo == 1) memberInfoDtos.clear();
memberInfoDtos.add(infoDto);
@ -461,10 +461,11 @@ public class CallBackResolution {
} catch (Exception e) {
e.printStackTrace();
} finally {
String tempTxt = "";
StringBuilder sb = new StringBuilder();
for (int k = 0; k < strings.size(); k++) {
tempTxt += unicode(strings.get(k));
sb.append(unicode(strings.get(k)));
}
String tempTxt = sb.toString();
Log.i(TAG,"at_Play_TTS_cb " + tempTxt);
if (tempTxt.contains("已登录")) {
String showName = tempTxt.substring(3, tempTxt.length());
@ -477,6 +478,7 @@ public class CallBackResolution {
};
executor.scheduleAtFixedRate(task, 0, 40, TimeUnit.SECONDS);
} else if (tempTxt.contains("账号已更新")) {
com.stand.standapp.MainActivity.showPttUpdateDialog();
} else if (tempTxt.contains("账号或密码错误")) {
CallBackUtil.callBackLogin(false, "");
}else if (tempTxt.contains("请配置账号")) {

View File

@ -68,7 +68,19 @@ public class CallBackUtil {
* state true-成功 false-失败
*/
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) {}
}
/**
@ -101,9 +113,17 @@ public class CallBackUtil {
* GroupInfoDto 群组信息
* */
public static void callBackGroupInfo (ArrayList < GroupInfoDto > groupInfoDtos) {
for (GroupInfoDto groupInfoDto : groupInfoDtos) {
Log.d(TAG, "callBackGroupInfo: " + groupInfoDto.toString());
}
try {
org.json.JSONArray arr = new org.json.JSONArray();
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) {}
}
/**
@ -112,44 +132,46 @@ public class CallBackUtil {
* talkId 讲话用户ID
* talkName 讲话用户名字
* */
public static void callBackNotifySpker ( int speckType, String groupId, String
groupName, String talkId, String talkName){
public static void callBackNotifySpker ( int speckType, String groupId, String 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) {}
}
/**
* 对方讲话通知
* playStatus 1表示开始讲话, 0 表示结束讲话
* speckType 自己讲话状态 0 表示自己无法讲话 1 表示自己可以中断讲话人的讲话可以进行强插讲话
* groupId 所在群组Id
* groupName 所在群组名称
* talkId 讲话用户Id
* talkName 讲话用户名称
* */
public static void callBackNotifyPlayStatus ( int playStatus, int speckType, String
groupId, String groupName, String talkId, String talkName){
public static void callBackNotifyPlayStatus ( int playStatus, int speckType, String groupId, String groupName, String talkId, String talkName){
try {
org.json.JSONObject obj = new org.json.JSONObject();
obj.put("playStatus", playStatus);
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('PlayStatus', " + obj.toString() + ");}");
} catch (Exception e) {}
}
/**
*临时呼叫信息通知
* tempCallNotify
* 临时呼叫xxx表示被单呼呼叫名xxx
* 退出临时呼叫表示退出单呼
* 呼叫成功表示单呼成功
* 呼叫失败表示单呼失败
* */
public static void callBackTempCallNotify (String 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) {}
}
/**
* 用户进入群组信息通知
* userGroupId 用户所在的群组ID
* userGroupName 用户所在的群组名称
* */
public static void callBackNotifyUpdateGroup (String userGroupId, String 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) {}
}
/**
@ -160,9 +182,16 @@ public class CallBackUtil {
* longitude 经度
* 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, 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) {}
}
/**
@ -242,7 +271,13 @@ public class CallBackUtil {
* longitude 经度
* */
public static void callBackEnotifyAlarm (String memberName,double latitude, double 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) {}
}
/**

View File

@ -1,10 +1,14 @@
package com.example.kingway.ptt;
import android.content.SharedPreferences;
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;
@ -12,29 +16,44 @@ import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
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 {
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
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ptt);
pttBtn = (Button)findViewById(R.id.pttBtn);
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);
XXPermissions.with(this)
.permission(Permission.RECORD_AUDIO)
.request(null);
pttBtn.setOnTouchListener(new View.OnTouchListener()
{
pttBtn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent)
{
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN)
{
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
pttBtn.setBackgroundColor(Color.RED);
SendAtUtil.speakPlay();
}
else if(motionEvent.getAction() == MotionEvent.ACTION_UP)
{
} else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
pttBtn.setBackgroundColor(Color.BLUE);
SendAtUtil.speakRelease();
}
@ -42,7 +61,6 @@ public class pttActivity extends AppCompatActivity {
}
});
findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -50,16 +68,31 @@ public class pttActivity extends AppCompatActivity {
}
});
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("gzzdweb01", "123456", "61.243.1.123", 1, 1);
SendAtUtil.toLogin("test004", "123456", "220.154.131.231", 1, 1);
}
});
/** 获取群组 */
findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -67,7 +100,6 @@ public class pttActivity extends AppCompatActivity {
}
});
/** 获取群成员 */
findViewById(R.id.button3).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -75,7 +107,6 @@ public class pttActivity extends AppCompatActivity {
}
});
/** 切换群组 */
findViewById(R.id.btnJumpGroup).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -90,7 +121,6 @@ public class pttActivity extends AppCompatActivity {
}
});
/** 切换群组 */
findViewById(R.id.btnCancel2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@ -98,4 +128,44 @@ public class pttActivity extends AppCompatActivity {
}
});
}
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);
}
}

View File

@ -65,9 +65,7 @@ class LoginActivity : AppCompatActivity() {
// 4. 退出按钮
btnExit.setOnClickListener {
fullCleanup()
finishAffinity()
android.os.Process.killProcess(android.os.Process.myPid())
com.stand.standapp.utils.AppKiller.killApp(this)
}
// 5. 彩蛋:点击底部版权 10 次进入开发者模式
@ -148,18 +146,17 @@ class LoginActivity : AppCompatActivity() {
} catch (e: Exception) {
Timber.tag("PTT").e(e, "PTT init error")
}
Thread {
Toast.makeText(this@LoginActivity, "PTT初始化中...", Toast.LENGTH_SHORT).show()
// 使用 Handler 替代硬编码 Thread.sleep避免阻塞或随意开辟线程
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
try {
Thread.sleep(500)
runOnUiThread {
Toast.makeText(this@LoginActivity, "PTT初始化中", Toast.LENGTH_SHORT).show()
}
Thread.sleep(3000)
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()
}, 1500)
// 2. 启动硬件 GPIO 监听
try {
@ -242,57 +239,9 @@ class LoginActivity : AppCompatActivity() {
.setTitle("提示")
.setMessage("确定要退出应用吗?")
.setPositiveButton("确定") { _, _ ->
fullCleanup()
finishAffinity()
Process.killProcess(Process.myPid())
com.stand.standapp.utils.AppKiller.killApp(this)
}
.setNegativeButton("取消", null)
.show()
}
private fun fullCleanup() {
pttLogoutAndDestroy()
}
private fun pttLogoutAndDestroy() {
try {
com.example.kingway.ptt.SendAtUtil.toCancelLogin()
} catch (e: Exception) {
Timber.tag("LoginActivity").e(e, "PTT cancel failed")
}
try {
com.example.kingway.ptt.SendAtUtil.toLogout()
Timber.tag("LoginActivity").i("PTT logout sent")
} catch (e: Exception) {
Timber.tag("LoginActivity").e(e, "PTT logout failed")
}
try {
com.stand.standapp.utils.GpioManager.stopListening()
} catch (e: Exception) {
Timber.tag("LoginActivity").e(e, "GPIO stop failed")
}
try {
com.stand.standapp.utils.GpioManager.stopListening()
} catch (e: Exception) {
Timber.tag("LoginActivity").e(e, "GPIO stop failed")
}
try {
// 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")
}
}
}

View File

@ -58,19 +58,25 @@ class MainActivity : AppCompatActivity() {
val ctx = instance ?: return@runOnUiThread
androidx.appcompat.app.AlertDialog.Builder(ctx)
.setTitle("账号冲突")
.setMessage("您的 PTT 账号已在其他设备登录,当前设备已被迫下线。\n请点击确认退出并重新登录")
.setMessage("您的 PTT 账号已在其他设备登录,当前设备已被迫下线。\n请点击确认重启应用")
.setCancelable(false)
.setPositiveButton("确认") { _, _ ->
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())
com.stand.standapp.utils.AppKiller.killApp(ctx, restart = true)
}
.show()
}
}
@JvmStatic
fun showPttUpdateDialog() {
instance?.runOnUiThread {
val ctx = instance ?: return@runOnUiThread
androidx.appcompat.app.AlertDialog.Builder(ctx)
.setTitle("账号更新")
.setMessage("您的 PTT 账号信息已被更新,需要重新登录以应用更改。\n请点击确认重启应用。")
.setCancelable(false)
.setPositiveButton("确认") { _, _ ->
com.stand.standapp.utils.AppKiller.killApp(ctx, restart = true)
}
.show()
}
@ -225,17 +231,81 @@ class MainActivity : AppCompatActivity() {
val result = handleBridgeCall(payload, prompt.defaultValue ?: "")
return GeckoResult.fromValue(prompt.confirm(result))
}
// 默认处理(显示对话框)
return null
val res = GeckoResult<GeckoSession.PromptDelegate.PromptResponse>()
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(
session: GeckoSession,
prompt: GeckoSession.PromptDelegate.AlertPrompt
): GeckoResult<GeckoSession.PromptDelegate.PromptResponse>? {
Timber.tag("WebConsole").d("Alert: ${prompt.message}")
val res = GeckoResult<GeckoSession.PromptDelegate.PromptResponse>()
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
}
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
}
}
}
@ -263,9 +333,7 @@ class MainActivity : AppCompatActivity() {
.setTitle("提示")
.setMessage("确定要退出应用吗?")
.setPositiveButton("确定") { _, _ ->
pttLogoutAndDestroy()
finishAffinity()
android.os.Process.killProcess(android.os.Process.myPid())
com.stand.standapp.utils.AppKiller.killApp(this)
}
.setNegativeButton("取消", null)
.show()

View File

@ -57,6 +57,9 @@ class MyApplication : Application() {
super.onCreate()
mApp = this
pageName = packageName
com.stand.standapp.utils.AppKiller.setupUncaughtExceptionHandler(this)
initPTT()
// 第一步:初始化日志
LogManager.init(this)

View File

@ -27,8 +27,13 @@ object PrinterManager {
mContext?.let {
if (!hasPaper) {
android.widget.Toast.makeText(it, "⚠️ 警告:打印机缺纸!", android.widget.Toast.LENGTH_LONG).show()
} else {
android.widget.Toast.makeText(it, "打印机状态:正常 (有纸)", android.widget.Toast.LENGTH_SHORT).show()
}
}
// 通知前端页面
com.stand.standapp.MainActivity.executeJs("if(window.onPaperStateChanged) { window.onPaperStateChanged($hasPaper); }")
}
true
}

View File

@ -0,0 +1,77 @@
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(200)
} 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() + 500, 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)
}
}
}

View File

@ -12,6 +12,7 @@ import timber.log.Timber
*/
object GpioManager {
private var mZysjSystemManager: ZysjSystemManager? = null
@Volatile
private var mIsRunning = false
private var mThread: Thread? = null
private var lastGpioValue = 1 // 默认松开状态 (1)
@ -54,6 +55,7 @@ object GpioManager {
fun stopListening() {
mIsRunning = false
mThread?.interrupt()
mThread = null
}

View File

@ -5,71 +5,152 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.kingway.ptt.pttActivity">
<LinearLayout
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
>
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent">
<Button
android:id="@+id/login"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="login"/>
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/pttBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="PTT" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="服务器IP"
android:textSize="14sp"
android:layout_marginBottom="4dp"/>
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="获取群组" />
<AutoCompleteTextView
android:id="@+id/etIp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="服务器IP"
android:inputType="text"
android:text="220.154.131.231"
android:dropDownWidth="match_parent"
android:layout_marginBottom="8dp"/>
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="获取成员" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="账号"
android:textSize="14sp"
android:layout_marginBottom="4dp"/>
<Button
android:id="@+id/btnTmpCall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开始单呼" />
<AutoCompleteTextView
android:id="@+id/etAccount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="账号"
android:inputType="text"
android:text="test004"
android:dropDownWidth="match_parent"
android:layout_marginBottom="8dp"/>
<Button
android:id="@+id/btnTmpCallStop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="结束单呼" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="密码"
android:textSize="14sp"
android:layout_marginBottom="4dp"/>
<Button
android:id="@+id/btnJumpGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="切换群组" />
<AutoCompleteTextView
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="密码"
android:inputType="textPassword"
android:text="123456"
android:dropDownWidth="match_parent"
android:layout_marginBottom="8dp"/>
<Button
android:id="@+id/btnCancel1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="取消登录" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="群组ID"
android:textSize="14sp"
android:layout_marginBottom="4dp"/>
<Button
android:id="@+id/btnCancel2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="注销" />
<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/login1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="login1" />
</LinearLayout>
<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>