# communication **Repository Path**: etnetwork_admin/communication ## Basic Information - **Project Name**: communication - **Description**: 简单的即使通讯demo - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-11-08 - **Last Updated**: 2026-11-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # LinkUp · 即时通讯 Demo 一个可以直接跑起来的即时通讯示例:**Android 原生客户端(Kotlin + Jetpack Compose)** + **零依赖 Node.js WebSocket 服务端** + **网页版模拟控制台**。 客户端不是“UI 壳子”:文字与语音留言走真实 WebSocket 收发,App 被划掉后由前台服务继续保活;收到消息会响铃、显示通知角标;语音请求会全屏展示通讯录里识别出的呼入人。 ![控制台](docs/06-console.png) --- ## 1. 需求实现对照 | 需求 | 实现 | | --- | --- | | ① 文字通信 | WebSocket 长连接;消息发送状态(发送中 / 成功 / 失败)、离线队列、重连自动补发、本地持久化 | | ② 语音非实时通信 | `MediaRecorder` 录制 AAC/M4A 语音留言,Base64 走 WebSocket;接收方落盘后可随时播放;离线自动排队 | | ③ 关闭后常驻 + 图标角标 + 声音提醒 | 前台服务持有 WebSocket(`specialUse` FGS)+ `START_STICKY` + 开机自启 + 15 分钟守护;高优先级通知渠道、铃声/振动;语音请求带全屏 Intent;通知角标 + OEM 数字角标适配 | | ④ App 与通讯录人员绑定,识别呼入人 | 首次启动选择“本机身份”;服务端按手机号路由;来电时用通讯录把号码解析为姓名、分组、头像、备注;陌生号码降级显示 | | ⑤ 通讯录 Mock + 分组 | 16 位联系人、4 个分组(家人 / 同事 / 朋友 / 客户),支持搜索、分组标题、身份卡、快捷发起聊天/语音请求 | --- ## 2. 界面截图 | 绑定身份 | 分组通讯录 | | --- | --- | | ![绑定身份](docs/01-onboarding.png) | ![通讯录](docs/02-contacts.png) | | 文字 + 语音会话 | 语音请求(前台) | | --- | --- | | ![会话](docs/03-chat-voice.png) | ![来电](docs/04-incoming-call.png) | | 图标角标 + 通知 | 划掉任务后仍能收到 | | --- | --- | | ![角标](docs/05-notification-badge.png) | ![后台送达](docs/07-background-delivery.png) | | 锁屏全屏来电 | 模拟控制台 | | --- | --- | | ![锁屏来电](docs/08-lockscreen-call.png) | ![控制台](docs/06-console.png) | > 截图来自 Android 17 模拟器(Medium Phone,1080×2400)。通知角标在 Pixel 启动器上显示为圆点,长按图标可看数量;三星/小米/华为等启动器会显示数字(见 `BadgeHelper`)。 --- ## 3. 快速开始 ### 环境要求 - Node.js **≥ 18**(服务端零依赖,无需 `npm install`) - JDK **17+** - Android SDK:Platform **35**、Build-Tools **36**(项目已带 Gradle Wrapper 8.11.1) - Android 模拟器(API 26+)或真机 ### 一步启动 ```bash # 终端 1:启动服务端 + 控制台 ./scripts/start-server.sh # 打开 http://localhost:8080 # 终端 2:编译并安装 ./scripts/build-apk.sh ./scripts/install.sh ``` 安装后打开 LinkUp: 1. 选择一个“本机身份”(例如 **林晓 / 13800000001**)。 2. 建议点击“开启通知权限”。 3. 点击“绑定设备并进入”。 4. 顶部出现绿色“已连接”,即表示 WebSocket 已连上 `ws://10.0.2.2:8080/ws`。 > 真机调试:手机和电脑连同一个局域网,在 App 的「我的 → 连接」中把地址改成 `ws://<电脑局域网IP>:8080/ws`,保存并重连。 > > 也可以使用端口反向代理:`adb reverse tcp:8080 tcp:8080`,然后把地址改为 `ws://127.0.0.1:8080/ws`。 > > 如果本机 Android SDK 不在默认路径,请修改 `android/local.properties` 中的 `sdk.dir`(或删除该文件并设置 `ANDROID_HOME`)。 ### 部署到局域网 / 云服务器 - **局域网演示(最快)**:手机与电脑同一 Wi-Fi,App 服务器地址填 `ws://<电脑IP>:8080/ws`。当前机器示例 IP 为 `192.168.3.167`。 - **公网部署**:云服务器 + Nginx/Caddy + 域名 + `wss://`,以及 Android 签名 APK 的完整步骤见 **[DEPLOY.md](DEPLOY.md)**。 - 部署文件在 `deploy/`:systemd、Nginx、Caddy、Dockerfile、docker-compose。 > ⚠️ 当前服务端没有鉴权,HTTP API 也开放,仅适合局域网/内网 Demo;公网部署前请先按 DEPLOY.md 的安全清单加鉴权与 TLS。 ### 用网页控制台模拟“另一个联系人” 1. 控制台左侧“接收设备”选择 **林晓 · 在线**。 2. “发送人”选择 **王磊(同事)**。 3. 发送文字、发送语音留言,或点击“发起语音请求”。 控制台会实时显示在线设备、离线队列和事件日志。 --- ## 4. 演示脚本(可选) ```bash # 文字:王磊 → 林晓 node scripts/demo-incoming.js text 13800002001 13800000001 "下午 3 点评审,别迟到~" # 语音留言(服务端生成 3.6 秒示例音频) node scripts/demo-incoming.js voice 13900001001 13800000001 # 语音请求(设备在线时响铃 + 全屏/通知接听) node scripts/demo-incoming.js call 13800002002 13800000001 ``` 服务端同时提供 HTTP API,方便自动化测试: ```bash curl -X POST http://localhost:8080/api/send \ -H 'Content-Type: application/json' \ -d '{"from":"13800002001","to":"13800000001","text":"hello"}' curl -X POST http://localhost:8080/api/call \ -H 'Content-Type: application/json' \ -d '{"from":"13800002002","to":"13800000001"}' curl http://localhost:8080/api/state curl http://localhost:8080/api/health ``` --- ## 5. 项目结构 ``` communication/ ├── android/ # Android 客户端 │ ├── app/src/main/java/com/demo/linkup/ │ │ ├── MainActivity.kt # 入口;通知点击/接听跳转 │ │ ├── data/ │ │ │ ├── Models.kt # Contact / Message / 状态模型 │ │ │ ├── MockContacts.kt # 16 位联系人、4 个分组、3 个身份 │ │ │ └── AppRepository.kt # 单一数据源:消息、未读、来电、持久化、通知编排 │ │ ├── net/MessagingClient.kt # OkHttp WebSocket:心跳、重连、协议编解码 │ │ ├── service/ │ │ │ ├── MessagingService.kt # 前台服务:常驻长连接、看门狗 │ │ │ ├── NotificationHelper.kt# 通知渠道 / 角标 / 全屏来电 / 接听动作 │ │ │ ├── BadgeHelper.kt # 三星/小米/华为等 OEM 数字角标 │ │ │ ├── RingtonePlayer.kt # 循环铃声 + 振动 │ │ │ ├── BootReceiver.kt # 开机/升级后拉起服务 │ │ │ ├── KeepAliveReceiver.kt # 15 分钟守护广播 │ │ │ └── CallActionReceiver.kt# 通知“拒绝”动作 │ │ ├── audio/ # 录音 / 播放 │ │ └── ui/ # Compose 页面与主题 │ └── app/src/main/res/raw/ # 铃声 / 消息提示音 ├── server/ │ ├── server.js # 零依赖 WebSocket + HTTP API + WAV 生成 │ ├── public/index.html # 深色网页控制台 │ └── package.json ├── scripts/ # 启动、构建、安装、模拟消息 ├── docs/ # 演示截图 └── README.md ``` --- ## 6. 常驻、通知与角标机制 ### 前台服务 - `MessagingService` 在 App 启动后通过 `startForegroundService` 启动,持有 WebSocket;不是由 Activity 持有。 - `android:stopWithTask="false"`:从最近任务划掉卡片后服务继续运行。 - `START_STICKY` + `onTaskRemoved()` 1.5 秒后的 AlarmManager 兜底重启。 - `BootReceiver` 监听 `BOOT_COMPLETED` / `MY_PACKAGE_REPLACED`,重启后自动恢复连接。 - `KeepAliveReceiver` 每 15 分钟触发一次服务看护。 - 设置页提供“后台电池优化”白名单入口,降低国产 ROM 杀后台概率。 > **边界**:用户主动“强行停止”(应用信息 → 强行停止)后,Android 不允许任何后台服务自启,必须重新打开 App;这是系统限制。省电模式/厂商清理策略也可能影响常驻。 ### 通知与角标 - 三个通知渠道: - `linkup_service_v1`:低优先级常驻连接通知,无声音、无角标。 - `linkup_messages_v1`:高优先级,自定义提示音 `message.wav`,振动,开启角标。 - `linkup_calls_v1`:高优先级,来电通知,全屏 Intent,配合循环铃声与振动。 - 每条消息通知都带 `setNumber(未读总数)` 与 `setShowBadge(true)`,普通 Android 启动器会显示通知圆点;`BadgeHelper` 会额外向三星/索尼/HTC/小米/华为启动器广播数字角标。 - 全屏来电: - 设备锁屏/息屏:`IncomingCallActivity`(`showWhenLocked` + `turnScreenOn`)覆盖锁屏,直接显示接听界面。 - 设备解锁且 App 在后台:系统展示高优先级 heads-up 通知,带“拒绝 / 接听”动作;点击“接听”会打开对应会话。 - App 在前台:不弹通知(避免遮挡),直接展示同款全屏 Compose 来电界面 + 铃声/振动;角标仍会更新。 - 30 秒无人接听:自动结束铃声,写入“未接的语音请求”系统消息,生成静默未接通知并保留未读角标。 ### 通知点击 - 消息通知 → 打开对应聊天,自动清除该会话未读。 - 来电通知“接听” → `MainActivity` 收到 `accept_call`,完成接听并直接进入会话。 - 来电通知“拒绝” → `CallActionReceiver` 发送拒绝回执并停止铃声。 --- ## 7. 语音留言 1. 聊天页点击麦克风 → 动态申请 `RECORD_AUDIO`。 2. `MediaRecorder` 以 16 kHz / 32 kbps / 单声道 AAC 录制到 `filesDir/voice/*.m4a`。 3. 发送时读取文件转 Base64,通过 WebSocket `voice` 消息传输(同时带 `durationMs`、`mime`)。 4. 接收方解码落盘,气泡内显示时长与波形;点击播放/暂停,使用 `MediaPlayer`。 5. 对方离线时服务端将完整语音包放入该身份的队列,重新上线后补发。 > Demo 为控制包体积直接传输 Base64;生产环境应使用对象存储 + 预签名 URL,并做压缩、加密与大小限制。 --- ## 8. 身份绑定与呼入识别 - 首次启动必须从 3 个 Mock 身份中选择一个(林晓 / 陈可 / 苏航),绑定关系保存在 `SharedPreferences`。 - 身份的手机号是本设备的服务端路由键;服务端使用 `hello.user.phone` 注册在线连接。 - 收到文字/语音/语音请求时,客户端用 `from.phone` 在通讯录中反查: - 命中:显示姓名、分组标签、头像、备注签名; - 未命中:显示号码 + “陌生人 / 未保存在通讯录”。 - 因此“识别呼入人”完全由本地通讯录决定,服务端不需要下发姓名,也不怕服务端名称被伪造。 - 「我的 → 更换身份」可随时重新绑定;重新绑定后服务会自动断开旧身份并建立新连接。 ### Mock 联系人 | 分组 | 成员 | | --- | --- | | 家人 | 妈妈、爸爸、妹妹、舅舅 | | 同事 | 王磊、李娜、陈晨、赵敏、孙浩 | | 朋友 | 周远、苏菲、高鹏、唐糖 | | 客户 | 刘总、何经理、郑女士 | 本机身份:林晓 `13800000001`、陈可 `13800000002`、苏航 `13800000003`。 > 可选:再开一台模拟器安装同一个 APK,绑定 **陈可**,即可体验两个 App 之间真实的文字/语音/来电互通(服务端按手机号路由)。 --- ## 9. WebSocket 协议 连接:`ws://:8080/ws`(OkHttp 内部转为 http(s) URL) 客户端 → 服务端: ```json { "type": "hello", "user": { "id": "13800000001", "name": "林晓", "phone": "13800000001" } } { "type": "text", "to": "13800002001", "clientMsgId": "uuid", "text": "你好", "ts": 1730000000000 } { "type": "voice", "to": "13800002001", "clientMsgId": "uuid", "audio": "", "mime": "audio/mp4", "durationMs": 4200, "ts": 1730000000000 } { "type": "call", "to": "13800002001", "callId": "uuid" } { "type": "call_accept", "to": "13800002001", "callId": "uuid" } { "type": "call_decline", "to": "13800002001", "callId": "uuid" } ``` 服务端 → 客户端: ```json { "type": "welcome", "serverTime": 1730000000000 } { "type": "text", "id": "srv_1", "from": { "phone": "...", "name": "..." }, "text": "你好", "ts": 1730000000000 } { "type": "voice", "id": "srv_2", "from": { ... }, "audio": "", "mime": "audio/wav", "durationMs": 3600, "ts": 1730000000000 } { "type": "call", "callId": "call_1", "from": { ... } } { "type": "call_response", "callId": "call_1", "action": "accept", "from": { ... } } { "type": "call_unavailable" } { "type": "ack", "clientMsgId": "uuid" } ``` - `ack` 用于把“发送中”改为“已发送”。 - 文字/语音在接收方离线时进入内存队列(每身份最多 50 条),上线后按顺序补发;语音请求不排队,离线直接返回 `call_unavailable`。 - 服务端每 20 秒由 OkHttp ping 维持连接;断线指数退避重连(1–30 秒)。 --- ## 10. 权限 | 权限 | 用途 | | --- | --- | | `INTERNET` / `ACCESS_NETWORK_STATE` | WebSocket | | `RECORD_AUDIO` | 录制语音留言 | | `POST_NOTIFICATIONS` | Android 13+ 通知 | | `FOREGROUND_SERVICE` + `FOREGROUND_SERVICE_SPECIAL_USE` | 常驻消息服务 | | `RECEIVE_BOOT_COMPLETED` | 开机恢复连接 | | `USE_FULL_SCREEN_INTENT` | 锁屏全屏来电 | | `VIBRATE` / `WAKE_LOCK` | 来电振动 / 唤醒 | | `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` | 引导加入电池白名单 | `usesCleartextTraffic=true` 仅为本地 `ws://` Demo 方便,生产环境应使用 `wss://` 并移除。 --- ## 11. 验证清单(已实测) - [x] 控制台模拟联系人向设备发送文字/语音,聊天页实时出现气泡;发送状态 `✓`。 - [x] 录制 9 秒语音并发送,服务端收到 Base64,气泡显示 `0:09`、可播放。 - [x] 控制台发送 WAV 语音留言,App 落盘、显示波形、可播放。 - [x] App 在前台时收到其他会话消息/语音,弹通知并更新未读;当前会话内不打扰。 - [x] 回到桌面收到消息:heads-up 通知 + 声音渠道 + 启动器角标圆点。 - [x] 后台收到语音请求:heads-up 通知带“拒绝/接听”,锁屏场景全屏来电界面。 - [x] 通知“接听”打开对应会话;通知“拒绝”发送回执并清除来电。 - [x] 30 秒未接:生成未接通知与系统消息,未读数增加。 - [x] 从最近任务划掉卡片(`dumpsys activity recents` 任务消失)后进程与服务仍在,继续收到消息。 - [x] 杀进程/重启 App 后,本地会话、未读、身份绑定均恢复。 - [x] 键盘弹出时聊天头部与输入框保持可见(`adjustResize`)。 --- ## 12. 已知限制(Demo 范围) 1. **不是生产级 IM**:没有账号体系、鉴权、TLS、消息加密、数据库和消息去重事务;服务端队列在内存中,重启即丢失。 2. **语音是留言,不是实时通话**:需求中的“语音非实时”已实现;“接听”是对讲式请求的 UI/通知演示,不建立 RTC 音频流。 3. **Base64 音频**:方便演示,代价是体积膨胀 33%;生产建议上传对象存储后只发 URL。 4. **全屏 Intent 行为随系统版本变化**:Android 14+ 解锁状态下通常展示 heads-up 通知而非直接全屏,这是系统策略;锁屏/息屏时仍会全屏。通知动作可完成接听。 5. **角标显示取决于启动器**:Pixel 为圆点,三星/小米/华为等可显示数字;`BadgeHelper` 已做 best-effort 适配,无法覆盖所有 ROM。 6. **后台保活受系统/厂商限制**:强行停止、极端省电策略会终止服务;Demo 提供电池白名单与开机自启,但不能保证 100%。 7. **Mock 数据**:通讯录固定写在 `MockContacts.kt` 与服务端 `server.js` 中,两边通过手机号保持一致。 --- ## 13. 故障排查 | 现象 | 处理 | | --- | --- | | 一直显示“离线 / 正在自动重连” | 确认服务端已启动;模拟器地址用 `10.0.2.2`;真机用局域网 IP;或 `adb reverse` + `127.0.0.1` | | 收不到通知/声音 | Android 13+ 授予通知权限;检查系统通知渠道;不要开启“静音/勿扰”;模拟器可能因 `-no-audio` 无声 | | 锁屏来电不显示全屏 | 确认 `USE_FULL_SCREEN_INTENT`;部分系统需在“通知 → 全屏通知”中允许;解锁时用 heads-up 接听即可 | | 点击通知进入 App 但没有会话 | 确认通知来自对应会话;App 数据被清理后旧通知会失效 | | 麦克风无法录音 | 授予 `RECORD_AUDIO`;模拟器需启用音频输入;确认没有其他应用占用麦克风 | | 划掉任务后没消息 | 检查电池优化白名单;部分 ROM 需允许“自启动/后台活动”;强行停止后必须手动打开 | | 图标没有数字 | 启动器限制;本 Demo 使用通知角标,OEM 数字角标为 best-effort | | 控制台 0 在线 | 手机未绑定/服务未连接;在 App「我的」查看连接状态;控制台每 2 秒刷新 | --- ## 14. 技术栈 - Android:Kotlin 2.0.21、Jetpack Compose BOM 2024.09.00、Material 3、OkHttp 4.12.0、Coroutines 1.9.0 - 构建:AGP 8.9.0 / Gradle 8.11.1 / compileSdk 35 / minSdk 26 / targetSdk 35 - 服务端:Node.js ≥18,原生 `http` + 手写 WebSocket 帧解析,无第三方依赖 - 音频:`MediaRecorder`(AAC/M4A)+ `MediaPlayer`;服务端动态生成 PCM WAV 测试音频 - 通知:Notification Channels、Full-Screen Intent、Action PendingIntent、OEM Badge Broadcast/ContentProvider --- ## 15. 常用命令 ```bash ./scripts/start-server.sh # 启动服务端 + 控制台 ./scripts/build-apk.sh # 构建 Debug APK ./scripts/install.sh # 安装、授权、启动 node scripts/demo-incoming.js call # 模拟来电(默认王磊 → 林晓) # 检查服务状态 adb shell dumpsys activity services com.demo.linkup | grep -E 'isForeground|startRequested' adb shell dumpsys notification --noredact | grep com.demo.linkup adb logcat -s LinkUpWs LinkUpRepo LinkUpRing # 可选:端口反向代理 adb reverse tcp:8080 tcp:8080 ``` --- 如果这个 Demo 对你有帮助,直接拿去改成自己的业务:替换 `MockContacts.kt` 为系统通讯录读取、把服务端换成你的 IM 网关,前台的文字/语音/来电三条链路都可以复用。