集成ptt

This commit is contained in:
fengge 2026-04-02 19:28:13 +08:00
parent 9ab25e0a2f
commit a79f0ca2a1
21 changed files with 1684 additions and 3 deletions

7
.gitignore vendored
View File

@ -13,3 +13,10 @@
.externalNativeBuild
.cxx
local.properties
/startopencode.bat
/.idea/
/第三方/ptt/
/第三方/安卓demo和说明/POC SOS扩展指令手册V1.0.docx
/第三方/安卓demo和说明/POC 多用户单呼扩展.docx
/第三方/安卓demo和说明/POC应用扩展指令手册V4.0(TTS功能只需看第7部分).pdf
/第三方/安卓demo和说明/ptt.rar

View File

@ -4,10 +4,10 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-03-14T21:27:21.683378200Z">
<DropdownSelection timestamp="2026-04-02T11:09:56.107873100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\36549\.android\avd\Pixel_Tablet_2.avd" />
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\36549\.android\avd\Medium_Phone.avd" />
</handle>
</Target>
</DropdownSelection>

6
.idea/encodings.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/startopencode.bat" charset="GBK" />
</component>
</project>

84
AGENTS.md Normal file
View File

@ -0,0 +1,84 @@
# StandAPP 智能调度终端 - 开发指南 (AGENTS.md)
本文档为 StandAPP 项目的开发规范与技术参考,旨在帮助开发者快速理解项目架构、构建流程及核心业务逻辑。
## 1. 项目概况
StandAPP 是一款基于 Kotlin 和 Jetpack Compose 开发的 Android 应用。集成了 `AgentWeb`(网页交互)、打印服务、自动更新以及核心的 **PTT (Push-to-Talk) 对讲模块**
- **主包名:** `com.stand.standapp`
- **PTT 包名:** `com.example.kingway.ptt` (严禁修改,与 Native 层硬绑定)
- **技术栈:** Kotlin, Jetpack Compose, Gradle (Kotlin DSL), OkHttp, AgentWeb, NDK (C++).
- **SDK 版本:** Compile SDK 36, Target SDK 34, Min SDK 24.
## 2. 构建、检查与测试
### 构建命令
- **编译 Debug APK:** `./gradlew assembleDebug`
- **清理项目:** `./gradlew clean`
- **编译 Release APK:** `./gradlew assembleRelease`
### 检查与测试
- **运行 Lint 检查:** `./gradlew lint`
- **运行单元测试:** `./gradlew test`
- **运行单项测试:** `./gradlew :app:testDebugUnitTest --tests "包名.类名"`
## 3. 代码风格与规范
- **缩进:** 4 个空格。
- **命名:** 类名使用 `PascalCase`;函数和变量使用 `camelCase`;常量使用 `SCREAMING_SNAKE_CASE`
- **Compose:** 优先使用 Material 3 组件Composable 函数首字母大写Modifier 必须作为第一个可选参数。
- **错误处理:** 必须使用 `try-catch` 包裹 IO、网络、JSON 解析等易错操作,并使用 `Log.e` 记录异常。
## 4. PTT (对讲) 核心业务逻辑解析
PTT 模块是本项目的通讯核心,依赖底层 `libptt.so` 原生引擎。其业务流程高度依赖 **十六进制 AT 指令** 交互。
### 4.1 核心组件与关键方法
#### **A. 指令发送:`SendAtUtil` (指令工厂)**
封装了所有发往 Native 引擎的控制指令。
- `toLogin(account, pwd, ip, location, tempCall)`: **登录接口**。将账号、密码、服务器 IP、GPS 开启标志及单呼标志拼接为 `010000` 开头的十六进制指令。
- `speakPlay()`: **开始对讲**。发送 `0B0000` 指令,触发底层 POC 引擎进入发送模式并准备处理语音数据。
- `speakRelease()`: **停止对讲**。发送 `0C0000` 指令通知引擎结束通话,并同步停止本地录音线程。
- `sendTCP()` / `sendUDP()`: **心跳维持**。登录成功后,必须每 **40秒** 调用一次。`570000` 为 TCP 心跳,`580000` 为 UDP 心跳。
- `getGroupInfo()`: **查询群组**。发送 `0D0000` 指令。
- `getGroupMemberInfo(groupId)`: **查询成员**。发送 `0E0000` + 群组 ID。
- `jumpGroup(groupId)`: **切换群组**。发送 `090000` + 群组 ID用于即时切换监听的群组。
#### **B. 指令解析:`CallBackResolution` (响应处理器)**
解析 Native 引擎通过 JNI 异步返回的十六进制数据。
- `at_OEM_AT_cb(lisHex)`: **解析入口**。识别首字节指令码:
- `82`: 登录/离线状态变更。
- `80`: 接收群组详细信息ID、名称、成员数
- `81`: 接收群成员在线状态及视频能力。
- `83`: 语音发起者信息(当前谁在说话)。
- `8b`: 播音状态变更(引擎开始或停止播放对方声音)。
- `8d`: 经纬度数据通知。
- `at_Play_TTS_cb(sData)`: **TTS 状态解析**。解析中文状态描述。当识别到“已登录”时,需触发心跳定时器。
- `isLoginConflict(state)`: **冲突检测**。逻辑1 分钟内若状态在“离线”和“登录中”切换超过 5 次,则判断为登录冲突。
#### **C. 录音管理:`AudioRecordManager` (音频采集)**
- `startRecord()`: 启动录音循环。设置优先级为 `THREAD_PRIORITY_URGENT_AUDIO`
- `stopRecord()`: 释放 `AudioRecord` 资源。
- **技术参数**: 采样率 8000Hz, 16bit, 单声道。每读取 320 字节 PCM 原始数据即调用 `pttNativeSendRecordCmd` 推送至 Native 层。
#### **D. JNI 桥接:`pttNative` (底层交互)**
- `OEM_Play_cb(byte[] bytes)`: **播放回调**。底层接收到的音频流会实时回调此方法,数据需直接写入 `AudioTrack` 实例进行播放。
- `pttNativePocTaskStart()`: **引擎初始化**
### 4.2 业务技术流程
1. **初始化**: 调用 `MyApplication.init(context)` -> 加载 SO 库 -> 启动 Native 任务。
2. **认证**: 调用 `toLogin` -> 异步等待 `82` 回调或 TTS “已登录”提示。
3. **在线维持**: 登录成功后启动周期为 40s 的任务发送 TCP/UDP 心跳。
4. **对讲过程**:
- **按下 PTT**: `speakPlay` -> 收到成功反馈 -> `AudioRecordManager` 持续采音并推送。
- **松开 PTT**: `speakRelease` -> 停止录音 -> 发送结束指令。
5. **UI 更新**: 引擎回调均在 **子线程**。所有 UI 操作Toast、Dialog、TextView 更新)必须切回主线程。
## 5. 开发约束与安全
1. **包名与类名**: `com.example.kingway.ptt.pttNative` 严禁改名或移动,否则会导致 JNI 方法找不到实现导致崩溃。
2. **权限管理**: 必须先通过 `XXPermissions` 获取 `RECORD_AUDIO` 权限后再执行对讲。
3. **线程规范**: 回调处理严禁阻塞。耗时操作必须异步执行。
---
*本文档由 AI 助手维护,旨在确保业务逻辑的一致性。*

View File

@ -15,6 +15,10 @@ android {
versionName = "1.0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters.addAll(listOf("armeabi-v7a", "arm64-v8a"))
}
}
buildTypes {
@ -45,6 +49,7 @@ dependencies {
implementation("com.github.Justson:Downloader:v5.0.4-androidx")
implementation("com.google.android.material:material:1.12.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.github.getActivity:XXPermissions:18.2")
implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)

View File

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@ -39,6 +40,12 @@
android:label="@string/app_name"
android:theme="@style/Theme.StandAPP" />
<activity
android:name="com.example.kingway.ptt.pttActivity"
android:exported="false"
android:label="PTT示例"
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@ -0,0 +1,177 @@
package com.example.kingway.ptt;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;
import java.util.Arrays;
@SuppressWarnings("ResultOfMethodCallIgnored")
public class AudioRecordManager {
private final String TAG = "AudioRecordManager";
private static final int FREQUENCY = 8000;
private AudioRecord mRecorder;
private Thread recordThread;
private boolean isStart = false;
private static AudioRecordManager mInstance;
private int bufferSize = 320;
private AudioRecordManager() {
initAudio();
}
public void initAudio() {
int audioSource = MediaRecorder.AudioSource.MIC;
mRecorder = new AudioRecord(audioSource,
FREQUENCY,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize * 2);
}
public void setRecording(){
if (mRecorder == null) initAudio();
if(mRecorder.getRecordingState() != 3) mRecorder.startRecording();
}
public void stopAudio(){
if (mRecorder != null){
mRecorder.stop();
}
}
public int getState(){
if (mRecorder == null) initAudio();
return mRecorder.getRecordingState();
}
public void destroyAudio(){
if (mRecorder != null){
mRecorder.stop();
mRecorder.release();
mRecorder = null;
}
}
/**
* 获取单例引用
*/
public static AudioRecordManager getInstance() {
if (mInstance == null) {
synchronized (AudioRecordManager.class) {
if (mInstance == null) {
mInstance = new AudioRecordManager();
}
}
}
return mInstance;
}
/**
* 销毁线程方法
*/
private void destroyThread() {
try {
if (null != recordThread && Thread.State.RUNNABLE == recordThread.getState()) {
try {
recordThread.interrupt();
} catch (Exception e) {
recordThread = null;
}
}
recordThread = null;
} catch (Exception e) {
e.printStackTrace();
} finally {
recordThread = null;
}
}
/**
* 启动录音线程
*/
private void startThread() {
destroyThread();
isStart = true;
if (mRecorder == null) {
initAudio();
}
if (recordThread == null) {
recordThread = new Thread(recordRunnable);
recordThread.start();
}
}
/**
* 录音线程
*/
private Runnable recordRunnable = new Runnable() {
@Override
public void run() {
try {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
int bytesRecord;
byte[] tempBuffer = new byte[bufferSize];
if (mRecorder.getState() != AudioRecord.STATE_INITIALIZED) {
stopRecord();
return;
}
mRecorder.startRecording();
while (isStart) {
if (null != mRecorder) {
bytesRecord = mRecorder.read(tempBuffer, 0, bufferSize);
if (bytesRecord == AudioRecord.ERROR_INVALID_OPERATION
|| bytesRecord == AudioRecord.ERROR_BAD_VALUE) {
continue;
}
if (bytesRecord != 0 && bytesRecord != -1) {
Log.i(TAG, "tempBuffer->" + Arrays.toString(tempBuffer));
MyApplication.getAppInstance().getpNative().pttNativeSendRecordCmd(tempBuffer);
} else {
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
/**
* 启动录音
*/
public void startRecord() {
try {
stopRecord();
startThread();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 停止录音
*/
public void stopRecord() {
try {
isStart = false;
destroyThread();
if (mRecorder != null) {
try {
if (mRecorder.getState() == AudioRecord.STATE_INITIALIZED) {
mRecorder.stop();
}
mRecorder.release();
mRecorder = null;
} catch (Exception e) { }
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,470 @@
package com.example.kingway.ptt;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**解析接收到的AT指令*/
public class CallBackResolution {
private static String TAG = "CallBackResolution";
private static String indexGroupId = "";//用户所在群组Id
private static String indexGroupName = "";//用户所在群组名称
private static List<GroupMemberInfoDto> memberInfoDtos = new ArrayList<>();
private static int speckType = 1;
private static String talkId = "";
private static String talkName = "";
private static ArrayList<GroupInfoDto> groupInfos = new ArrayList<>();
private static String oldLoginSate = "00";
private static int logNum = 0;
private static long offlineTime = 0;
private static boolean loginState = false;
public static void at_OEM_AT_cb(List<String> lisHex){
if (lisHex.size() == 0) {
return;
}
String ngx = "\\u";
try {
Log.i(TAG, "lisHex-->" + lisHex.toString());
String firstHex = lisHex.get(0);
String errorNum = lisHex.get(1);
Log.d(TAG, "at_OEM_AT_cb firstHex: " + firstHex + ", errorNum: " + errorNum);
switch (firstHex){
case "82"://登录状态通知指令
String loginId = lisHex.get(2) + "" + lisHex.get(3) + "" + lisHex.get(4) + "" + lisHex.get(5);
CallBackUtil.callBackLoinState(errorNum, loginId);
loginState = isLoginConflict(errorNum);
CallBackUtil.callBackLoginConflict(loginState);
break;
case "a2"://账号有视频功能
CallBackUtil.callBackHaveVideo();
break;
case "0d"://查询群组返回状态
if (errorNum.equals("00")){
CallBackUtil.callBackGroupInfo(groupInfos);
}
//CallBackUtil.callBackOnGroupSuccess(errorNum.equals("01")? false : true);
break;
case "0e"://查询群组成员返回状态
CallBackUtil.callBackOnMemberSuccess(errorNum.equals("01")? false : true, memberInfoDtos);
break;
case "0b"://发起讲话返回状态
Log.d(TAG, "at_OEM_AT_cb ptt speak...");
CallBackUtil.callBackSpeakPlaySuccess(errorNum.equals("00")? true : false);
break;
case "0c"://结束讲话返回状态
Log.d(TAG, "at_OEM_AT_cb speak end...");
CallBackUtil.callBackSpeakReleaseSuccess(errorNum.equals("00")? true : false);
break;
case "80"://返回群组信息
Log.d(TAG, "at_OEM_AT_cb get group info: " + lisHex);
String strGroup = "";
String groupId = lisHex.get(6) + "" + lisHex.get(7) + "" + lisHex.get(8) + "" + lisHex.get(9);
int groupNo = Integer.valueOf((lisHex.get(4) + "" + lisHex.get(5)), 16);
int groupCount = Integer.valueOf((lisHex.get(10) + "" + lisHex.get(11)), 16);
try {
int numB = 13;
int size = lisHex.size() - 12;
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);
} else {
strGroup += ngx + lisHex.get(i + numB + 1) + "" + lisHex.get(i + numB);
numB = numB + 1;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
String tempTxt = "";
tempTxt = unicodeToString(strGroup);
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);
if (1 == groupNo) groupInfos.clear();
GroupInfoDto groupInfo = new GroupInfoDto(groupId, tempTxt, groupNo, groupCount);
groupInfos.add(groupInfo);
}
break;
case "81"://返回群成员信息
String groupMemberName = "";
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);
int memberNo = Integer.valueOf((lisHex.get(4) + "" + lisHex.get(5)), 16);
try {
int numB = 12;
int size = lisHex.size() - 11;
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);
} else {
groupMemberName += ngx + lisHex.get(i + numB + 1) + "" + lisHex.get(i + numB);
numB = numB + 1;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
String tempTxt = unicodeToString(groupMemberName);
GroupMemberInfoDto infoDto = new GroupMemberInfoDto(memberId, tempTxt, status, memberNo, haveVideo);
if (memberNo == 1) memberInfoDtos.clear();
memberInfoDtos.add(infoDto);
}
break;
case "83"://讲话用户信息通知
if (lisHex.get(1).equals("00")) {
speckType = 0;
} else {
speckType = 1;
}
talkId = lisHex.get(2) + "" + lisHex.get(3) + "" + lisHex.get(4) + "" + lisHex.get(5);
List<String> stringsSpker = new ArrayList<>();
try {
int numA = 6;
int numB = 7;
int size = lisHex.size() - 6;
size = (size / 2) - 1;
for (int i = 0; i < size; i++) {
if (i == 0) {
stringsSpker.add(ngx + lisHex.get(i + numB) + "" + lisHex.get(i + numA));
} else {
int start = i + numB;
int end = i + numB + 1;
stringsSpker.add(ngx + lisHex.get(end) + "" + lisHex.get(start));
numB = numB + 1;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
String tempTxt = "";
for (int i = 0; i < stringsSpker.size(); i++) {
tempTxt += unicode(stringsSpker.get(i));
}
talkName = tempTxt;
}
break;
case "8b"://通知音频播放的状态
int notifyPlayStatus = Integer.parseInt(lisHex.get(2));
if (notifyPlayStatus == 0){
talkId = "";
talkName = "";
}
CallBackUtil.callBackNotifyPlayStatus(notifyPlayStatus, speckType, indexGroupId, indexGroupName, talkId, talkName);
break;
case "84"://提示信息通知
List<String> stringsTalk = new ArrayList<>();
try {
int numA = 2;
int numB = 3;
int size = lisHex.size() - 2;
size = (size / 2) - 1;
for (int i = 0; i < size; i++) {
if (i == 0) {
stringsTalk.add(ngx + lisHex.get(i + numB) + "" + lisHex.get(i + numA));
} else {
int start = i + numB;
int end = i + numB + 1;
stringsTalk.add(ngx + lisHex.get(end) + "" + lisHex.get(start));
numB = numB + 1;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
String tempTxt = "";
for (int i = 0; i < stringsTalk.size(); i++) {
tempTxt += unicode(stringsTalk.get(i));
}
Log.i(TAG, " 临时讲话 信息通知的指令 " + tempTxt);
CallBackUtil.callBackTempCallNotify(tempTxt);
}
break;
case "86"://用户进入群组信息通知
String userGroupId = lisHex.get(2) + "" + lisHex.get(3) + "" + lisHex.get(4) + "" + lisHex.get(5);
List<String> stringsJumpGroup = new ArrayList<>();
try {
int numA = 6;
int numB = 7;
int size = lisHex.size() - 6;
size = (size / 2) - 1;
for (int i = 0; i < size; i++) {
if (i == 0) {
stringsJumpGroup.add(ngx + lisHex.get(i + numB) + "" + lisHex.get(i + numA));
} else {
int start = i + numB;
int end = i + numB + 1;
stringsJumpGroup.add(ngx + lisHex.get(end) + "" + lisHex.get(start));
numB = numB + 1;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
String userGroupName = "";
for (int i = 0; i < stringsJumpGroup.size(); i++) {
userGroupName += unicode(stringsJumpGroup.get(i));
}
Log.i(TAG,"进入群组 " + userGroupName);
indexGroupId = userGroupId;
indexGroupName = userGroupName;
CallBackUtil.callBackNotifyUpdateGroup(userGroupId, userGroupName);
}
break;
case "8d"://用户定位信息通知
if (lisHex.get(1).equals("00")) {
String str = "";
String locationMemberId = lisHex.get(2) + "" + lisHex.get(3) + "" + lisHex.get(4) + "" + lisHex.get(5);
for (int i = 0; i < lisHex.size() - 1; i++) {
if (i > 5) {
str = str + "" + lisHex.get(i).toString();
}
}
String address = hexString2String(str);
address = address.substring(0,address.lastIndexOf(":"));
try {
String[] strLis = address.split(",");
double latitude = Double.parseDouble(strLis[1]);
double longitude = Double.parseDouble(strLis[0]);
String time = strLis[2];
CallBackUtil.callBackNotifyLocationStatus(0, locationMemberId, latitude, longitude, time);
} catch (Exception e) {
Log.e(TAG, "获取定位Exception-->" + e.getMessage());
}
} else {
CallBackUtil.callBackNotifyLocationStatus(1, "", 0.0, 0.0, "");
}
break;
case "11"://上报位置信息
CallBackUtil.callBackNotifyUploadLocation(errorNum.equals("00")? true : false);
break;
case "02"://获取写码账号信息
String str = "";
for (int i = 4; i < lisHex.size() - 2; i++) {
str = str + "" + lisHex.get(i);
}
String cmdTxt = hexString2String(str);
CallBackUtil.callBackNotifyCodeInfo(cmdTxt);
break;
case "48"://获取写码密码信息
String strPwd = "";
for (int i = 4; i < lisHex.size() - 2; i++) {
strPwd = strPwd + "" + lisHex.get(i);
}
String cmdPwdTxt = hexString2String(strPwd);
CallBackUtil.callBackNotifyPwdInfo(cmdPwdTxt);
break;
case "45"://进入写码模式通知
CallBackUtil.callBackNotifyCodeOpenStatus(errorNum.equals("00")? true : false);
break;
case "52"://退出写码模式操作
CallBackUtil.callBackNotifyCodeExiteStatus(errorNum.equals("00")? true : false);
break;
case "47"://上传经销商密码
CallBackUtil.callBackNotifyUploadAgentPwd(errorNum.equals("00")? true : false);
break;
case "53"://上报本机告警信号
CallBackUtil.callBackRequestReportAlarm(errorNum.equals("00")? true : false);
break;
case "54"://停止本机告警信号
CallBackUtil.callBackRequestStopAlarm(errorNum.equals("00")? true : false);
break;
case "8e"://收到群成员告警信号
if (lisHex.get(1).equals("00")) {
List<String> strName = new ArrayList<>();
double latitude = 0.0, longitude = 0.0;
try {
String string = "";
for (int i = 4; i < lisHex.size(); i++) {
string += lisHex.get(i).toString();
}
String address = hexString2String(string);
Log.i(TAG, "address-->" + address);
String[] strLis = address.split(",");
latitude = Double.parseDouble(strLis[0]);
longitude = Double.parseDouble(strLis[1]);
int size = lisHex.size() - (8 + strLis[0].length() + strLis[1].length());
size = size / 2;
int numA = 4 + strLis[0].length() + strLis[1].length() + 3;
int numB = 4 + strLis[0].length() + strLis[1].length() + 4;
for (int i = 0; i < size; i++) {
if (i == 0) {
strName.add(ngx + lisHex.get(i + numB) + "" + lisHex.get(i + numA));
} else {
int start = i + numB;
int end = i + numB + 1;
strName.add(ngx + lisHex.get(end) + "" + lisHex.get(start));
numB = numB + 1;
}
}
} catch (Exception e) {
} finally {
String tempTxt = "";
for (int i = 0; i < strName.size(); i++) {
if (strName.get(i).equals("\\u0000")) break;
tempTxt += unicode(strName.get(i));
}
CallBackUtil.callBackEnotifyAlarm(tempTxt, latitude, longitude);
}
}
break;
case "8f"://收到群成员停止警告信息
CallBackUtil.callBackEnotifyStopAlarm();
break;
case "78"://上传NFC信息
CallBackUtil.callBackRequestNfc(errorNum.equals("00")? true : false);
break;
}
}catch (Exception e){
Log.e(TAG,"at_OEM_AT_cb Exception " + e.getMessage());
}
}
public static boolean isLoginConflict(String state) {
if (oldLoginSate.equals("00") && state.equals("01")) {
logNum++;
oldLoginSate = state;
} else if (oldLoginSate.equals("01") && state.equals("00")) {
logNum++;
oldLoginSate = state;
}
if (1 == logNum) {
offlineTime = System.currentTimeMillis();
}
if (logNum > 4) {
if (System.currentTimeMillis() - offlineTime >= 60 * 1000) {
return true;
}
}
return false;
}
public static void at_Play_TTS_cb(String sData){
try {
String[] txt = sData.split("\"");
String data = txt[1];
String ngx = "\\u";
List<String> strings = new ArrayList<>();
try {
String str = "";
for (int i = 0; i < data.length(); i++) {
if (i == 0) {
str = ngx + data.charAt(i);
} else if (i % 4 == 0) {
strings.add(str);
str = ngx + data.charAt(i);
} else if (i == data.length() - 1) {
str = str + data.charAt(i);
strings.add(str);
} else {
str = str + data.charAt(i);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
String tempTxt = "";
for (int k = 0; k < strings.size(); k++) {
tempTxt += unicode(strings.get(k));
}
if (tempTxt.contains("已登录")) {
String showName = tempTxt.substring(3, tempTxt.length());
CallBackUtil.callBackLogin(true, showName);
java.util.concurrent.ScheduledExecutorService executor = java.util.concurrent.Executors.newScheduledThreadPool(1);
Runnable task = () -> {
SendAtUtil.sendUDP();
SendAtUtil.sendTCP();
};
executor.scheduleAtFixedRate(task, 0, 40, java.util.concurrent.TimeUnit.SECONDS);
} else if (tempTxt.contains("账号或密码错误")) {
CallBackUtil.callBackLogin(false, "");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static String unicode(String str) {
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(\\\\u(\\p{XDigit}{4}))");
java.util.regex.Matcher matcher = pattern.matcher(str);
char ch;
while (matcher.find()) {
String group = matcher.group(2);
ch = (char) Integer.parseInt(group, 16);
String group1 = matcher.group(1);
str = str.replace(group1, ch + "");
}
return str;
}
public static String unicodeToString(String hex) {
int t = hex.length() / 6;
StringBuilder str = new StringBuilder();
for (int i = 0; i < t; i++) {
String s = hex.substring(i * 6, (i + 1) * 6);
String s1 = s.substring(2, 4) + "00";
String s2 = s.substring(4);
int n = Integer.valueOf(s1, 16) + Integer.valueOf(s2, 16);
char[] chars = Character.toChars(n);
str.append(new String(chars));
}
return str.toString();
}
public static String hexString2String(String src) {
String temp = "";
for (int i = 0; i < src.length() / 2; i++) {
temp = temp + (char) Integer.valueOf(src.substring(i * 2, i * 2 + 2), 16).byteValue();
}
return temp;
}
}

View File

@ -0,0 +1,263 @@
package com.example.kingway.ptt;
import android.os.Handler;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**指令返回类
* 说明操作UI请在主线程进行
* */
public class CallBackUtil {
private static String TAG = "TYT_CallBackUtil";
/**
* 返回登录
* loginState 是否登录成功
* name 用户名
*/
public static void callBackLogin(boolean loginState, String name) {
Log.i(TAG, "callBackLogin loginState " + loginState + " name " + name);
}
/**
* 登录状态通知
* state
* 0 : 离线
* 1 : 登陆中
* 2 : 登陆成功
* 3 : 注销中
* 说明第一次登录 通知状态 先01再02
* 离线再上线 通知状态 先00再01
* <p>
* loginId 用户ID登录成功id有效
*/
public static void callBackLoinState(String state, String loginId) {
Log.d(TAG, "callBackLoinState state: " + state + ", loginId: " + loginId);
}
/**
* 判断账号登录是否冲突
* true登录冲突
* false登录不冲突
* */
public static void callBackLoginConflict(boolean loginConflict) {
Log.d(TAG, "callBackLoginConflict: " + loginConflict);
}
/**
* 登录账号有视频功能通知
*/
public static boolean callBackHaveVideo() {
return true;
}
/**
* 查询群组返回
* state true-成功 false-失败
*/
public static void callBackOnGroupSuccess(boolean state) {
}
/**
* 查询群组成员返回
* state true-成功 false-失败
*/
public static void callBackOnMemberSuccess(boolean state, List<GroupMemberInfoDto> dtos) {
}
/**
* 应用发起讲话返回
* state true-成功 false-失败
*/
public static void callBackSpeakPlaySuccess(boolean state) {
Log.d(TAG, "callBackSpeakPlaySuccess state: " + state);
if (state) AudioRecordManager.getInstance().startRecord();
}
/**
* 应用结束讲话返回
* state true-成功 false-失败
*/
public static void callBackSpeakReleaseSuccess(boolean state) {
}
/**
* 返回群组信息
* groupId 群组ID
* groupName 群组名称
* groupNo 表示此组为第几个组
* groupCount 表示此组用户成员数
*/
public static void callBackGroupInfo(String groupId, String groupName, int groupNo, int groupCount) {}
/**
* 返回群组集合信息
* GroupInfoDto 群组信息
* */
public static void callBackGroupInfo (ArrayList < GroupInfoDto > groupInfoDtos) {
for (GroupInfoDto groupInfoDto : groupInfoDtos) {
Log.d(TAG, "callBackGroupInfo: " + groupInfoDto.toString());
}
}
/**
* 讲话用户信息通知
* speckType 讲话状态 0 表示自己无法讲话 1 表示自己可以中断讲话人的讲话可以进行强插讲话
* talkId 讲话用户ID
* talkName 讲话用户名字
* */
public static void callBackNotifySpker ( int speckType, String groupId, String
groupName, String talkId, String talkName){
}
/**
* 对方讲话通知
* 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){
}
/**
*临时呼叫信息通知
* tempCallNotify
* 临时呼叫xxx表示被单呼呼叫名xxx
* 退出临时呼叫表示退出单呼
* 呼叫成功表示单呼成功
* 呼叫失败表示单呼失败
* */
public static void callBackTempCallNotify (String tempCallNotify){
}
/**
* 用户进入群组信息通知
* userGroupId 用户所在的群组ID
* userGroupName 用户所在的群组名称
* */
public static void callBackNotifyUpdateGroup (String userGroupId, String userGroupName){
}
/**
* 用户定位信息通知
* status 0获取成功 1获取失败
* userId 用户ID
* latitude 纬度
* longitude 经度
* time 时间
*/
public static void callBackNotifyLocationStatus ( int status, String userId,double latitude,
double longitude, String time){
}
/**
* 用户上报定位信息通知
* status true成功 false失败
* */
public static void callBackNotifyUploadLocation ( boolean status){
}
/**
* 空中写账号信息通知
* codeInfo ip=106.15.8.60;id=Android3;gps=1;inv=1
* ip=106.15.8.60 用户ip106.15.8.60
* id=Android3 用户账号Android3
* gps=11勾选定位0不勾选定位
* inv=11勾选单呼0不勾选单呼
* */
public static void callBackNotifyCodeInfo (String codeInfo){
}
/**
* 空中写密码信息通知
* pwdInfo age=123456;pwd=111111
* age=123456 经销商密码123456
* pwd=111111 账号密码111111
* */
public static void callBackNotifyPwdInfo (String pwdInfo){
}
/**
* 进入写码模式通知
* status true成功false失败
* */
public static void callBackNotifyCodeOpenStatus ( boolean status){
}
/**
* 退出写码模式通知
* status true成功false失败
* */
public static void callBackNotifyCodeExiteStatus ( boolean status){
}
/**
* 上传经销商密码通知
* status true成功false失败
* */
public static void callBackNotifyUploadAgentPwd ( boolean status){
}
/**
* 上报本机告警信号通知
* status true成功false失败
* */
public static void callBackRequestReportAlarm ( boolean status){
}
/**
* 停止本机告警信号通知
* status true成功false失败
* */
public static void callBackRequestStopAlarm ( boolean status){
}
/**
* 收到群成员警告通知
* memberName 群成员名称
* latitude 纬度
* longitude 经度
* */
public static void callBackEnotifyAlarm (String memberName,double latitude, double longitude){
}
/**
* 收到群成员停止警告通知
* */
public static void callBackEnotifyStopAlarm () {
}
/**
* 上传NFC信息通知
* status true成功false失败
* */
public static void callBackRequestNfc ( boolean status){
}
}

View File

@ -0,0 +1,69 @@
package com.example.kingway.ptt;
/**
* <pre>
* desc :
* </pre>
*/
public class GroupInfoDto {
private int groupNo; //表示此组为第一个组
private int groupCount; //表示此组有 3 个用户成员
private String groupId;
private String groupName;
public GroupInfoDto() {
}
public GroupInfoDto(String groupId, String groupName, int groupNo, int groupCount) {
this.groupId = groupId;
this.groupName = groupName;
this.groupNo = groupNo;
this.groupCount = groupCount;
}
public int getGroupNo() {
return groupNo;
}
public void setGroupNo(int groupNo) {
this.groupNo = groupNo;
}
public int getGroupCount() {
return groupCount;
}
public void setGroupCount(int groupCount) {
this.groupCount = groupCount;
}
public String getGroupId() {
return groupId == null ? "" : groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getGroupName() {
return groupName == null ? "" : groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
@Override
public String toString() {
return "GroupInfoDto{" +
"groupNo=" + groupNo +
", groupCount=" + groupCount +
", groupId='" + groupId + '\'' +
", groupName='" + groupName + '\'' +
'}';
}
}

View File

@ -0,0 +1,80 @@
package com.example.kingway.ptt;
import java.io.Serializable;
/**
* <pre>
* desc : 成员
* </pre>
*/
public class GroupMemberInfoDto implements Serializable {
private static final long serialVersionUID = 1L;
private int memberNo; //表示此成员为第几个用户
private String memberId;
private String gmemberName;
private String status;
private boolean haveVideo;//此成员是否有视频功能
public GroupMemberInfoDto(){}
public GroupMemberInfoDto(String memberId, String gmemberName, String status, int memberNo, boolean haveVideo) {
this.memberId = memberId;
this.gmemberName = gmemberName;
this.memberNo = memberNo;
this.status = status;
this.haveVideo = haveVideo;
}
public int getMemberNo() {
return memberNo;
}
public void setMemberNo(int memberNo) {
this.memberNo = memberNo;
}
public String getStatus() {
return status == null ? "" : status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMemberId() {
return memberId == null ? "" : memberId;
}
public void setMemberId(String memberId) {
this.memberId = memberId;
}
public String getGmemberName() {
return gmemberName == null ? "" : gmemberName;
}
public void setGmemberName(String gmemberName) {
this.gmemberName = gmemberName;
}
public boolean isHaveVideo() {
return haveVideo;
}
public void setHaveVideo(boolean haveVideo) {
this.haveVideo = haveVideo;
}
@Override
public String toString() {
return "GroupMemberInfoDto{" +
"memberNo=" + memberNo +
", memberId='" + memberId + '\'' +
", gmemberName='" + gmemberName + '\'' +
", status='" + status + '\'' +
", haveVideo=" + haveVideo +
'}';
}
}

View File

@ -0,0 +1,45 @@
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;
pNative = new pttNative();
pNative.pttNativeSetPackageName(pageName);
pNative.pttNativeSetAtCallback();
pNative.pttNativeSetPlayCallback();
pNative.pttNativeSetPalyStartCallback();
pNative.pttNativeSetPlayStopCallback();
pNative.pttNativeSetRecordStartCallback();
pNative.pttNativeSetRecordStopCallback();
pNative.pttNativeSetTTSCallback();
pNative.pttNativePocTaskStart();
}
}

View File

@ -0,0 +1,96 @@
package com.example.kingway.ptt;
/**
* 指令发送类
* */
public class SendAtUtil {
/**
* 账号登录接口
* account 账号
* pwd 密码
* ip 服务器ip
* location 是否开启定位 1-开启0关闭
* tempCall 是否开启单呼 1开启单呼0关闭单呼
* */
public static void toLogin(String account, String pwd, String ip, int location, int tempCall){
String ipHex = str2HexStr("ip=" + ip + ";");
String accountHex = str2HexStr("id=" + account + ";");
String pwdHex = str2HexStr("pwd=" + pwd + ";");
String locationHex = str2HexStr("gps=" + location + ";");
String tempCallHex = str2HexStr("inv=" + tempCall + ";");
String loginAt = "010000" + ipHex + "" + accountHex + "" + pwdHex + locationHex + tempCallHex + "\r\n";
MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd(loginAt);
MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("00000000\r\n");
}
/**
* 按下PTT开始呼叫
* */
public static void speakPlay(){
MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("0B0000");
}
/** 松开PTT结束呼叫*/
public static void speakRelease(){
AudioRecordManager.getInstance().stopRecord();
MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("0C0000");
}
/** 发送TCP包 登录成功后需要定时发送TCP包*/
public static void sendTCP(){
MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("570000\r\n");
}
/** 发送UDP包 登录后需要定时发送UDP包*/
public static void sendUDP(){
MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("580000\r\n");
}
/** 请求群组信息 */
public static void getGroupInfo(){
MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("0D0000");
}
/** 请求群组成员信息
* groupId 十六进制群组Id
* */
public static void getGroupMemberInfo(String groupId){
MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("0E0000" + groupId);
}
/** 切换群组
* groupId 十六进制群组Id
* */
public static void jumpGroup(String groupId){
MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("090000" + groupId);
}
/** 请求好友信息 */
public static void getFriendInfo(){
MyApplication.getAppInstance().getpNative().pttNativeSendAtCmd("0E000000000000\r\n");
}
/**
* 字符串转换成十六进制字符串
*
* @param str 待转换的ASCII字符串
* @return String
*/
public static String str2HexStr(String str) {
char[] chars = "0123456789ABCDEF".toCharArray();
StringBuilder sb = new StringBuilder("");
byte[] bs = str.getBytes();
int bit;
for (int i = 0; i < bs.length; i++) {
bit = (bs[i] & 0x0f0) >> 4;
sb.append(chars[bit]);
bit = bs[i] & 0x0f;
sb.append(chars[bit]);
}
return sb.toString().trim();
}
}

View File

@ -0,0 +1,83 @@
package com.example.kingway.ptt;
import android.graphics.Color;
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.XXPermissions;
import com.stand.standapp.R;
public class pttActivity extends AppCompatActivity {
private Button pttBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ptt);
pttBtn = (Button) findViewById(R.id.pttBtn);
// 初始化 PTT
MyApplication.init(this);
XXPermissions.with(this)
.permission(Permission.RECORD_AUDIO)
.request(null);
pttBtn.setOnTouchListener(new View.OnTouchListener() {
@Override
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) {
pttBtn.setBackgroundColor(Color.BLUE);
SendAtUtil.speakRelease();
}
return false;
}
});
findViewById(R.id.login).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SendAtUtil.toLogin("test012", "123456", "106.15.8.60", 1, 1);
}
});
/** 获取群组 */
findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SendAtUtil.getGroupInfo();
}
});
/** 获取群成员 */
findViewById(R.id.button3).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SendAtUtil.getGroupMemberInfo("0002b199");
}
});
/** 切换群组 */
findViewById(R.id.btnJumpGroup).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SendAtUtil.jumpGroup("0002b199");
}
});
}
}

View File

@ -0,0 +1,208 @@
package com.example.kingway.ptt;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
public class pttNative {
private static String TAG = "pttNative";
static {
System.loadLibrary("ptt");
}
public static char Char2ASCII(char c)
{
return (char) (c <= 9 ? (c+0x30) : (c+0x37));
}
public static char Ascii2Char(char nib_h, char nib_l)
{
nib_h = (nib_h <= '9')?((char)(nib_h-0x30)):(((nib_h<='F')?((char)(nib_h-0x37)):((char)(nib_h-0x57))));
nib_l = (nib_l <= '9')?((char)(nib_l-0x30)):(((nib_l<='F')?((char)(nib_l-0x37)):((char)(nib_l-0x57))));
return (char)((nib_h<<4)+nib_l);
}
//Callback Function for Recv the message from POC Lib
public void OEM_AT_cb(String sData)
{
Log.e(TAG,"Poc2 AT Callback: " + sData);
int len, i;
char[] bytes = sData.toCharArray();
List<String> lisHex = new ArrayList<>();
if (bytes.length > 5 && bytes[0] == '+' && bytes[1] == 'P' && bytes[2] == 'O' && bytes[3] == 'C' && bytes[4] == ':') {
len = bytes.length - 5;
if (len >= 2) {
for (i = 0; i < len - 1; i = i + 2) {
String temp = Integer.toHexString(Ascii2Char(bytes[i + 5], bytes[i + 6]));
lisHex.add(temp.length() == 1 ? "0" + temp : temp);
}
}
}
CallBackResolution.at_OEM_AT_cb(lisHex);
}
public void OEM_Play_cb(byte[] bytes){
if (mAudioTrack == null) {
initAudioTrack();
}
if (mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING){
mAudioTrack.play();
}
mAudioTrack.write(bytes, 0, bytes.length);
}
public void OEM_Play_Start_cb()
{
Log.e("Play Start!!!!\r\n","");
}
public void OEM_Play_Stop_cb()
{
Log.e("Play Stop!!!!\r\n","");
}
public void OEM_Record_Start_cb()
{
Log.e("Record Start!!!!\r\n","");
}
public void OEM_Record_Stop_cb()
{
Log.e("Record Stop!!!!\r\n","");
}
public void OEM_Play_TTS_cb(String sData)
{
Log.e(TAG , "Play TTS " + sData);
CallBackResolution.at_Play_TTS_cb(sData);
}
//Define the Function of the AT Command send
private native void OEM_AT_Send(String strAtCmd);
private native int OEM_Set_AT_CallBack(String cb);
private native void OEM_Push_Record_Data(byte[] data);
private native int OEM_Set_Play_CallBack(String cb);
private native int OEM_Set_Play_Start_Callback(String cb);
private native int OEM_Set_Play_Stop_Callback(String cb);
private native int OEM_Set_Record_Start_Callback(String cb);
private native int OEM_Set_Record_Stop_Callback(String cb);
private native int OEM_Set_TTS_Callback(String cb);
private native void OEM_Poc_Task_Start();
private native int OEM_Set_Package_Name(String name);
public void pttNativeSendRecordCmd(byte[] data) {
OEM_Push_Record_Data(data);
}
public void pttNativeSendAtCmd(String strAtCmd)
{
Log.i(TAG,"strAtCmd " + strAtCmd);
OEM_AT_Send(strAtCmd);
}
public void pttNativePocTaskStart()
{
OEM_Poc_Task_Start();
}
public int pttNativeSetAtCallback()
{
return OEM_Set_AT_CallBack("OEM_AT_cb");
}
public int pttNativeSetPlayCallback()
{
return OEM_Set_Play_CallBack("OEM_Play_cb");
}
public int pttNativeSetPalyStartCallback()
{
return OEM_Set_Play_Start_Callback("OEM_Play_Start_cb");
}
public int pttNativeSetPlayStopCallback()
{
return OEM_Set_Play_Stop_Callback("OEM_Play_Stop_cb");
}
public int pttNativeSetRecordStartCallback()
{
return OEM_Set_Record_Start_Callback("OEM_Record_Start_cb");
}
public int pttNativeSetRecordStopCallback()
{
return OEM_Set_Record_Stop_Callback("OEM_Record_Stop_cb");
}
public int pttNativeSetTTSCallback()
{
return OEM_Set_TTS_Callback("OEM_Play_TTS_cb");
}
public int pttNativeSetPackageName(String name)
{
return OEM_Set_Package_Name(name);
}
public pttNative() {
initAudioTrack();
}
private AudioTrack mAudioTrack;
public void initAudioTrack() {
int mAudioMinBufSize = AudioTrack.getMinBufferSize(8000, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT);
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build();
AudioFormat audioFormat = new AudioFormat.Builder()
.setSampleRate(8000)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.build();
mAudioTrack = new AudioTrack(
audioAttributes,
audioFormat,
mAudioMinBufSize * 4,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE
);
mAudioTrack.setVolume(1.0f);
}
public void destory() {
if (mAudioTrack != null) {
try {
if (mAudioTrack.getState() == AudioRecord.STATE_INITIALIZED) {
mAudioTrack.stop();
}
mAudioTrack.release();
mAudioTrack = null;
} catch (Exception e) {}
}
}
}

View File

@ -16,6 +16,7 @@ class LoginActivity : AppCompatActivity() {
val etPassword = findViewById<EditText>(R.id.et_password)
val cbRemember = findViewById<CheckBox>(R.id.cb_remember)
val btnLogin = findViewById<Button>(R.id.btn_login)
val btnPttExample = findViewById<Button>(R.id.btn_ptt_example)
val ivSettings = findViewById<ImageView>(R.id.iv_settings)
// 1. 加载保存的状态和凭据
@ -63,6 +64,16 @@ class LoginActivity : AppCompatActivity() {
Toast.makeText(this, "账号或密码错误", Toast.LENGTH_SHORT).show()
}
}
// 4. PTT 示例按钮点击事件
btnPttExample.setOnClickListener {
try {
val intent = Intent(this, com.example.kingway.ptt.pttActivity::class.java)
startActivity(intent)
} catch (e: Exception) {
Toast.makeText(this, "打开 PTT 示例失败: ${e.message}", Toast.LENGTH_SHORT).show()
}
}
}
/**
@ -96,7 +107,10 @@ class LoginActivity : AppCompatActivity() {
AlertDialog.Builder(this)
.setTitle("提示")
.setMessage("确定要退出应用吗?")
.setPositiveButton("确定") { _, _ -> super.onBackPressed() }
.setPositiveButton("确定") { _, _ ->
@Suppress("DEPRECATION")
super.onBackPressed()
}
.setNegativeButton("取消", null)
.show()
}

Binary file not shown.

Binary file not shown.

View File

@ -106,11 +106,21 @@
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_ptt_example"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@drawable/shape_button_login"
android:text="PTT 示例"
android:textColor="#FFFFFF"
android:textSize="16sp" />
</LinearLayout>
<!-- 底部版权 -->

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.kingway.ptt.pttActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
>
<Button
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="login"/>
<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="切换群组" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Binary file not shown.