This commit is contained in:
2026-06-27 13:41:44 +08:00
parent 4a28c5923f
commit 7cbf86c42c
2 changed files with 400 additions and 1 deletions
@@ -126,13 +126,15 @@ public class IncomingCallActivity extends Activity {
rowLp.rightMargin = dp(13); rowLp.rightMargin = dp(13);
btnRow.setLayoutParams(rowLp); btnRow.setLayoutParams(rowLp);
// 红色:挂断 // 红色:挂断(也进 app,让 JS 处理 decline 逻辑)
final String finalCaller = caller; final String finalCaller = caller;
ImageView btnDecline = makeCircleBtn(0xFFe53935, ImageView btnDecline = makeCircleBtn(0xFFe53935,
android.R.drawable.ic_menu_close_clear_cancel); android.R.drawable.ic_menu_close_clear_cancel);
btnDecline.setOnClickListener(v -> { btnDecline.setOnClickListener(v -> {
resultDelivered = true; resultDelivered = true;
IncomingCallNotificationHelper.cancel(this);
IncomingCallNotificationHelper.deliverCallResult(this, "decline"); IncomingCallNotificationHelper.deliverCallResult(this, "decline");
launchMainApp(finalCaller);
finish(); finish();
}); });
btnRow.addView(btnDecline); btnRow.addView(btnDecline);
@@ -160,8 +162,10 @@ public class IncomingCallActivity extends Activity {
@Override public void onFinish() { @Override public void onFinish() {
if (!resultDelivered) { if (!resultDelivered) {
resultDelivered = true; resultDelivered = true;
IncomingCallNotificationHelper.cancel(IncomingCallActivity.this);
IncomingCallNotificationHelper.deliverCallResult( IncomingCallNotificationHelper.deliverCallResult(
IncomingCallActivity.this, "decline"); IncomingCallActivity.this, "decline");
launchMainApp(finalCaller);
} }
finish(); finish();
} }
+395
View File
@@ -0,0 +1,395 @@
# rnwalletman 接入文档
## 1. Android 宿主接入
### 1.1 新建 Service 子类
宿主 App 必须继承 `BaseProxyService` 并实现 `registerWallet`
```java
package com.yourapp;
import android.content.Context;
import android.content.SharedPreferences;
import com.rnwalletman.BaseProxyService;
import org.json.JSONObject;
import okhttp3.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class AppProxyService extends BaseProxyService {
private static final OkHttpClient HTTP = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
@Override
protected void registerWallet(Context ctx, String walletId, String walletType,
String phone, JSONObject params) throws Exception {
SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
String baseUrl = prefs.getString("rebind_baseUrl", null); // JS 侧通过 syncRebindConfig 写入
int userId = prefs.getInt("rebind_userId", 0);
JSONObject body = new JSONObject();
body.put("walletType", walletType);
body.put("params", params);
Request req = new Request.Builder()
.url(baseUrl + "/register")
.header("X-User-ID", String.valueOf(userId))
.header("Content-Type", "application/json")
.post(RequestBody.create(body.toString(), MediaType.parse("application/json")))
.build();
try (Response resp = HTTP.newCall(req).execute()) {
String body2 = resp.body() != null ? resp.body().string() : "";
JSONObject json = new JSONObject(body2);
if (!json.optBoolean("success", false)) {
throw new IOException(json.optString("message", "register failed"));
}
}
}
}
```
### 1.2 AndroidManifest.xml
```xml
<!-- 前台服务 -->
<service
android:name=".AppProxyService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="false" />
<!-- 来电 Activity(仅 fullscreen 模式需要) -->
<activity
android:name=".IncomingCallActivity"
android:exported="false"
android:theme="@style/AppCallTheme"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:screenOrientation="portrait" />
<!-- 来电通知按钮接收器 -->
<receiver
android:name=".CallActionReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.yourapp.CALL_ACCEPT" />
<action android:name="com.yourapp.CALL_DECLINE" />
</intent-filter>
</receiver>
<!-- FCM 消息接收 -->
<service
android:name="com.rnwalletman.ProxyFcmService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- 权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
```
### 1.3 Application.onCreate 注册
```java
@Override
public void onCreate() {
super.onCreate();
// 指定宿主 Service 类
BaseProxyService.setServiceClass(AppProxyService.class);
// 指定来电全屏 Activity(有来电功能时)
BaseProxyService.setCallActivityClass(IncomingCallActivity.class);
}
```
---
## 2. FCM 消息格式
服务端发 **data-only** FCM(无 `notification` 块),data 字段:
| 字段 | 类型 | 说明 |
|------|------|------|
| `cmd` | string | 命令类型(见下表) |
| `msg` | string | 可选,通知文案(非空则弹系统通知) |
| `params` | string (JSON) | 命令参数 |
### 命令列表
#### `wake_proxy`
唤醒后台 WebSocket 服务。`params` 为空 `{}`
---
#### `rebind_wallet`
触发钱包 token 重绑(AIDL 拉取 → `registerWallet` 回调)。
`params` 字段:
| 字段 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `walletId` | string | ✅ | — | 钱包 ID |
| `walletType` | string | ✅ | — | `phonepe` / `paytm` / `mobikwik` 等 |
| `phone` | string | — | `""` | 绑定手机号 |
| `retry` | bool | — | `false` | 是否允许重试 |
| `tryGtkAnyway` | bool | — | `false` | PhonePe 强制拉 GTK |
示例:
```json
{
"cmd": "rebind_wallet",
"params": "{\"walletId\":\"abc123\",\"walletType\":\"phonepe\",\"phone\":\"9876543210\",\"retry\":true}"
}
```
---
#### `incoming_call`
触发来电通知 / 全屏来电界面。
`params` 字段:
| 字段 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| `caller` | string | — | `"Unknown"` | 来电显示名称 |
| `ringCount` | int | — | `1` | 呼叫次数(≥1 |
| `ringDuration` | int | — | `30` | 每次响铃时长(秒,≥5 |
| `mode` | string | — | `"fullscreen"` | `"fullscreen"``"notification"` |
| `extra` | object | — | — | 透传给 JS 的任意字段 |
示例:
```json
{
"cmd": "incoming_call",
"params": "{\"caller\":\"客服\",\"ringCount\":2,\"ringDuration\":30,\"mode\":\"fullscreen\",\"extra\":{\"orderId\":\"ORD001\"}}"
}
```
---
## 3. JS/TS 层 API
### 3.1 启动服务
```typescript
import { proxyBackgroundService } from 'rnwalletman';
await proxyBackgroundService.start({
wsUrl: 'wss://your-server/ws',
clientId: 'device-unique-id',
userId: 12345,
heartbeatInterval: 20000, // ms,默认 20000
reconnectInterval: 5000, // ms
onConnected: () => {},
onDisconnected: () => {},
onError: (err) => {},
onCustomCommand: (msg) => {},
// 可选:上报 FCM token 到服务端
registerFcmToken: async (clientId, fcmToken) => {
await api.post('/fcm/register', { clientId, fcmToken });
},
});
```
### 3.2 配置重绑(native 侧 AIDL 用)
```typescript
await proxyBackgroundService.syncRebindConfig({
baseUrl: 'https://your-server',
userId: 12345,
userToken: 'jwt-token',
onRebound: () => { /* 刷新 UI */ },
});
```
### 3.3 来电结果回调
接听 / 拒接 / 超时 均通过此回调返回,**与通知点击回调相互独立**:
```typescript
proxyBackgroundService.setCallResultHandler((payload) => {
const { action, caller, mode, ringDuration, ringCount, extra } = payload;
// action: 'accept' | 'decline'
if (action === 'accept') {
// 进入游戏 / 业务逻辑
}
});
```
payload 类型:
| 字段 | 类型 | 说明 |
|------|------|------|
| `action` | `'accept' \| 'decline'` | 操作结果 |
| `caller` | string | 来电显示名 |
| `mode` | string | `fullscreen` / `notification` |
| `ringDuration` | number | 响铃时长(秒) |
| `ringCount` | number | 呼叫次数 |
| `extra` | object? | 服务端透传的额外参数 |
### 3.4 FCM 通知点击回调
```typescript
proxyBackgroundService.setNotificationTapHandler((payload) => {
const { cmd, params } = payload;
// cmd: 'rebind_wallet' 等
});
```
### 3.5 其他方法
```typescript
await proxyBackgroundService.stop();
// 引导用户忽略电池优化(Android)
ProxyBackgroundService.openBatteryOptimizationSettings();
```
---
## 4. JS 事件(DeviceEventEmitter
| 事件名 | payload | 说明 |
|--------|---------|------|
| `ProxyServiceConnected` | — | WS 已连接 |
| `ProxyServiceDisconnected` | — | WS 断开 |
| `ProxyServiceError` | `{ data: string }` | 错误信息 |
| `ProxyServiceCustomCommand` | `{ data: string (JSON) }` | 服务端自定义消息 |
| `ProxyServiceTick` | — | 每 ~60s 一次心跳 tick |
| `ProxyServiceRebound` | `{ data: walletId }` | 重绑成功 |
| `ProxyServiceNotificationTap` | `{ data: JSON }` | FCM 通知被点击 |
| `ProxyServiceFcmToken` | `{ data: token }` | FCM token 更新 |
| `ProxyServiceCallResult` | `{ data: JSON }` | 来电结果(accept/decline |
---
## 5. 服务端发来电接口(phecda_hall gateway
```
POST http://gateway:12881/fcm/send-call
Content-Type: application/json
{
"clientId": "device-client-id", // 或 "userId": 12345
"caller": "客服",
"ringCount": 1,
"ringDuration": 30,
"mode": "fullscreen", // "fullscreen" | "notification"
"extra": { "orderId": "ORD001" } // 可选,透传到 JS
}
```
FCM token 注册:
```
POST http://gateway:12881/fcm/register
Content-Type: application/json
{
"clientId": "device-client-id",
"fcmToken": "firebase-device-token"
}
```
> **Firebase 服务账号**:放到 `config/fcm-service-account.json`,或设环境变量 `FCM_SERVICE_ACCOUNT=/path/to/file.json`
---
## 6. walletman 服务端 HTTP 接口(rnpay,端口 16000
### FCM 管理
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/fcm/register` | 注册设备 FCM token |
| POST | `/fcm/send-wake` | 发送唤醒指令 |
| POST | `/fcm/send-rebind` | 发送钱包重绑指令 |
| POST | `/fcm/send-call` | 发送来电推送 |
| GET | `/fcm/clients` | 列出所有已注册设备 |
#### `POST /fcm/register`
```json
{ "clientId": "device-id", "fcmToken": "firebase-token" }
```
#### `POST /fcm/send-wake`
```json
{ "clientId": "device-id" }
```
#### `POST /fcm/send-rebind`
```json
{
"clientId": "device-id", // 可选,不传则按 walletId 查找绑定设备
"walletId": "abc123", // 必填
"walletType": "phonepe", // 可选,不传则自动查找
"phone": "9876543210", // 可选
"retry": false, // 是否重试(phonepe 强制 true
"notify": false // true=弹系统通知,false=静默重绑
}
```
#### `POST /fcm/send-call`
```json
{
"clientId": "device-id", // 必填
"caller": "客服", // 默认 "Customer Service"
"ringCount": 1, // 默认 1
"ringDuration": 30, // 秒,默认 30
"mode": "fullscreen", // "fullscreen" | "notification"
"extra": { "key": "val" } // 可选,透传到 JS
}
```
---
## 7. WebSocket 协议(`wss://server/ws`
### 客户端 → 服务端
所有消息格式:
```json
{ "type": "<类型>", "messageId": "<唯一ID>", "clientId": "<设备ID>", "data": {} }
```
| type | data 字段 | 说明 |
|------|-----------|------|
| `register` | `{ userId, fcmToken }` | 连接后首条消息,注册设备身份 |
| `ping` | — | 心跳,服务端回 `pong` |
| `echo` | 任意 | 测试回显 |
| `proxyReady` | — | 通知服务端 TCP 代理隧道就绪 |
| `proxyData` | `{ data: base64 }` | 转发代理数据(按 `messageId` 路由) |
| `proxyClose` | — | 通知代理数据发送完毕 |
### 服务端 → 客户端
| type | data 字段 | 说明 |
|------|-----------|------|
| `response` | `{ success, message, data }` | 通用应答 |
| `echo` | 原样 | 回显 |
| `proxyRequest` | `{ host, port }` | 服务端请求建立 TCP 代理隧道 |
| `proxyData` | `{ data: base64 }` | 服务端 → 客户端代理数据 |
| `proxyClose` | — | 服务端通知关闭代理隧道 |
### register 示例
```json
{
"type": "register",
"messageId": "init-001",
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"data": {
"userId": 12345,
"fcmToken": "firebase-device-token"
}
}
```