1044 lines
40 KiB
Java
1044 lines
40 KiB
Java
package com.pinappletech.android.main;
|
||
|
||
import android.app.NotificationManager;
|
||
import android.content.pm.ServiceInfo;
|
||
import android.os.Handler;
|
||
import android.os.Looper;
|
||
import android.app.Service;
|
||
import android.content.Intent;
|
||
import android.os.IBinder;
|
||
import android.os.PowerManager;
|
||
import android.net.wifi.WifiManager;
|
||
import android.util.Log;
|
||
import android.os.Build;
|
||
import android.app.NotificationChannel;
|
||
import android.app.Notification;
|
||
import android.content.Context;
|
||
import android.os.RemoteException;
|
||
|
||
import java.lang.reflect.Field;
|
||
import java.lang.reflect.Method;
|
||
import java.net.DatagramPacket;
|
||
import java.net.DatagramSocket;
|
||
import java.net.InetAddress;
|
||
import java.net.NetworkInterface;
|
||
import java.net.SocketException;
|
||
import java.util.Enumeration;
|
||
import java.util.HashMap;
|
||
import java.util.HashSet;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.Set;
|
||
import java.util.concurrent.ConcurrentHashMap;
|
||
import java.util.concurrent.ExecutorService;
|
||
import java.util.concurrent.Executors;
|
||
import java.util.concurrent.ScheduledExecutorService;
|
||
import java.util.concurrent.TimeUnit;
|
||
|
||
import java.io.IOException;
|
||
import java.io.OutputStream;
|
||
import java.net.ServerSocket;
|
||
import java.net.Socket;
|
||
|
||
import com.unity3d.player.UnityPlayer;
|
||
import com.pvr.tobservice.interfaces.IToBServiceProxy;
|
||
import com.pvr.tobservice.interfaces.IIntCallback;
|
||
import com.pvr.tobservice.ToBServiceHelper;
|
||
import com.pvr.tobservice.enums.PBS_SystemInfoEnum;
|
||
|
||
import java.util.ArrayList;
|
||
|
||
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
|
||
import org.eclipse.paho.client.mqttv3.MqttCallback;
|
||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||
import org.eclipse.paho.client.mqttv3.MqttException;
|
||
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
|
||
|
||
public class Main extends Service {
|
||
|
||
private final String TAG = "PineappleService";
|
||
|
||
// ================= UDP服务 =================
|
||
// ip地址
|
||
private String localIp = "0.0.0.0";
|
||
// UDP监听端口
|
||
private int udpReceivePort = 9988;
|
||
private int udpSendPort = 9987;
|
||
private DatagramSocket udpSocket;
|
||
private boolean isRunning = false;
|
||
// 添加缓冲区大小常量定义
|
||
private final int bufferSize = 1024;
|
||
|
||
private final ExecutorService networkExecutor = Executors.newFixedThreadPool(2);
|
||
|
||
// 设备状态
|
||
private DeviceInfo deviceInfo = null;
|
||
|
||
// 包名映射
|
||
private Map<String, String> packageNamesMap = new HashMap<>();
|
||
private List<String> packageNamesList = new ArrayList<>();
|
||
private String packageNames = "{}";
|
||
|
||
// 服务context
|
||
private android.content.Context serviceContext;
|
||
|
||
// ================= 设备信息类 =================
|
||
private class DeviceInfo {
|
||
String ip;
|
||
String sn;
|
||
int power;
|
||
int status; // 0: 离线 1: 在线 2: 游玩中
|
||
String playing;
|
||
|
||
DeviceInfo(String ip, String sn, int power, int status, String playing) {
|
||
this.ip = ip;
|
||
this.sn = sn;
|
||
this.power = power;
|
||
this.status = status;
|
||
this.playing = playing;
|
||
}
|
||
}
|
||
|
||
// ================= Http 服务 =================
|
||
private ServerSocket httpServerSocket;
|
||
private ExecutorService httpExecutor = Executors.newFixedThreadPool(4);
|
||
private boolean httpServerRunning = false;
|
||
private int httpPort = 9999;
|
||
|
||
private ToBServiceHelper toBServiceHelper;
|
||
|
||
// ================= MQTT 服务 =================
|
||
private MqttClient mqttClient;
|
||
private static final String MQTT_BROKER_URL = "ws://emqx.pineappletech.cn";
|
||
private static final String MQTT_CLIENT_ID_PREFIX = "pico_";
|
||
private ScheduledExecutorService mqttScheduler;
|
||
private static final long MQTT_REPORT_INTERVAL = 20; // 20秒上报一次
|
||
private static final int MQTT_KEEP_ALIVE = 60; // KeepAlive 60秒
|
||
private static final int MQTT_CONNECTION_TIMEOUT = 30; // 连接超时30秒
|
||
private volatile boolean isMqttReconnecting = false;
|
||
private int mqttReconnectAttempts = 0;
|
||
private static final int MAX_RECONNECT_ATTEMPTS = 10;
|
||
private static final long RECONNECT_DELAY_MS = 5000; // 重连延迟5秒
|
||
|
||
// ================= 唤醒锁 =================
|
||
private PowerManager.WakeLock wakeLock;
|
||
private WifiManager.WifiLock wifiLock;
|
||
private static final String WAKE_LOCK_TAG = "PineappleService::WakeLock";
|
||
|
||
// ================= 服务生命周期 =================
|
||
@Override
|
||
public IBinder onBind(Intent intent) {
|
||
return null;
|
||
}
|
||
|
||
@Override
|
||
public void onCreate() {
|
||
Log.i(TAG, "菠萝服务启动");
|
||
|
||
// pico企业服务绑定
|
||
toBServiceHelper = ToBServiceHelper.getInstance();
|
||
toBServiceHelper.bindTobService(this);
|
||
toBServiceHelper.setBindCallBack(new ToBServiceHelper.BindCallBack() {
|
||
@Override
|
||
public void bindCallBack(Boolean status) {
|
||
// 绑定结果回调,绑定成功之后才能调用接口
|
||
Log.i(TAG, "绑定pico企业服务 : " + status);
|
||
if (status) {
|
||
try {
|
||
toBServiceHelper.getServiceBinder().pbsAppKeepAlive("com.pineapplegame.service", true, 0);
|
||
Log.i(TAG, "启动应用保活");
|
||
} catch (android.os.RemoteException e) {
|
||
Log.e(TAG, "启动应用保活失败: " + e.getMessage());
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
serviceContext = this;
|
||
deviceInfo = new DeviceInfo("0.0.0.0", "未知序列号", 0, 0, "未知游戏");
|
||
// 初始化本机IP地址
|
||
String ip = getIpAddressFromPico();
|
||
localIp = ip;
|
||
deviceInfo.ip = ip;
|
||
|
||
// 创建通知渠道
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
NotificationChannel channel = new NotificationChannel(
|
||
"pineapple_channel",
|
||
"菠萝服务",
|
||
NotificationManager.IMPORTANCE_LOW);
|
||
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||
manager.createNotificationChannel(channel);
|
||
}
|
||
|
||
Notification notification = new Notification.Builder(this, "pineapple_channel")
|
||
.setContentTitle("菠萝服务")
|
||
.setContentText("运行中...")
|
||
.setSmallIcon(android.R.drawable.ic_notification_overlay)
|
||
.build();
|
||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||
startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||
} else {
|
||
startForeground(1, notification);
|
||
}
|
||
|
||
// 启动Udp服务
|
||
startUdpReceiver();
|
||
|
||
// 启动Http服务
|
||
startHttpReceiver();
|
||
|
||
// 初始化唤醒锁
|
||
initWakeLocks();
|
||
|
||
// 启动MQTT服务
|
||
initMqttService();
|
||
|
||
// return START_STICKY;
|
||
}
|
||
|
||
/**
|
||
* 初始化唤醒锁,防止设备进入 Doze 模式导致 MQTT 断线
|
||
*/
|
||
private void initWakeLocks() {
|
||
try {
|
||
// 获取 PowerManager 并创建 WakeLock
|
||
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
|
||
wakeLock.setReferenceCounted(false);
|
||
wakeLock.acquire();
|
||
Log.i(TAG, "唤醒锁已获取");
|
||
|
||
// 获取 WifiManager 并创建 WifiLock
|
||
WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||
wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, WAKE_LOCK_TAG);
|
||
wifiLock.setReferenceCounted(false);
|
||
wifiLock.acquire();
|
||
Log.i(TAG, "WiFi 锁已获取");
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "初始化唤醒锁失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 释放唤醒锁
|
||
*/
|
||
private void releaseWakeLocks() {
|
||
try {
|
||
if (wakeLock != null && wakeLock.isHeld()) {
|
||
wakeLock.release();
|
||
Log.i(TAG, "唤醒锁已释放");
|
||
}
|
||
if (wifiLock != null && wifiLock.isHeld()) {
|
||
wifiLock.release();
|
||
Log.i(TAG, "WiFi 锁已释放");
|
||
}
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "释放唤醒锁失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public void onDestroy() {
|
||
super.onDestroy();
|
||
Log.i(TAG, "菠萝服务停止");
|
||
|
||
// 清理资源
|
||
isRunning = false;
|
||
httpServerRunning = false;
|
||
|
||
// 关闭网络执行器
|
||
networkExecutor.shutdownNow();
|
||
|
||
// 关闭UDP socket
|
||
if (udpSocket != null && !udpSocket.isClosed()) {
|
||
udpSocket.close();
|
||
}
|
||
|
||
// 关闭HTTP服务器socket
|
||
try {
|
||
if (httpServerSocket != null && !httpServerSocket.isClosed()) {
|
||
httpServerSocket.close();
|
||
}
|
||
} catch (IOException e) {
|
||
Log.e(TAG, "关闭HTTP服务器socket时出错: " + e.getMessage());
|
||
}
|
||
|
||
// 关闭HTTP执行器
|
||
if (httpExecutor != null) {
|
||
httpExecutor.shutdownNow();
|
||
}
|
||
|
||
// 断开MQTT连接
|
||
disconnectMqtt();
|
||
|
||
// 释放唤醒锁
|
||
releaseWakeLocks();
|
||
}
|
||
|
||
private String getIpAddressFromPico() {
|
||
String ip = "0.0.0.0";
|
||
try {
|
||
// 尝试通过Pico的系统信息服务获取IP地址
|
||
ip = toBServiceHelper.getServiceBinder().pbsStateGetDeviceInfo(
|
||
PBS_SystemInfoEnum.DEVICE_IP, 0);
|
||
Log.i(TAG, "通过Pico服务获取到IP地址: " + ip);
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "通过Pico服务获取IP失败: " + e.getMessage());
|
||
}
|
||
return ip;
|
||
}
|
||
|
||
// 修改 isLocalAddress 方法为 isLocalMessage,增加端口判断
|
||
private boolean isLocalMessage(InetAddress address, int port) {
|
||
// 只有当地址是本机IP且端口是8888时才屏蔽
|
||
// return localIPs.contains(address.getHostAddress()) && port == localPort;
|
||
return localIp.equals(address.getHostAddress()) && port == udpReceivePort;
|
||
}
|
||
|
||
// 根据包名获取应用名称
|
||
private String getAppName(String packageName) {
|
||
try {
|
||
android.content.pm.PackageManager pm = getPackageManager();
|
||
android.content.pm.ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0);
|
||
return pm.getApplicationLabel(appInfo).toString();
|
||
} catch (android.content.pm.PackageManager.NameNotFoundException e) {
|
||
Log.e(TAG, "应用名称获取失败,包名: " + packageName + ", 错误: " + e.getMessage());
|
||
return "未知应用";
|
||
}
|
||
}
|
||
|
||
// 获取已安装的包名
|
||
private void getInstalledApps() {
|
||
try {
|
||
// 获取包管理器
|
||
android.content.pm.PackageManager pm = getPackageManager();
|
||
// 获取已安装的应用列表
|
||
java.util.List<android.content.pm.PackageInfo> packages = pm.getInstalledPackages(0);
|
||
|
||
// 创建一个Map来存储包名和应用名称的映射
|
||
packageNamesMap.clear();
|
||
packageNamesList.clear();
|
||
|
||
for (android.content.pm.PackageInfo packageInfo : packages) {
|
||
// 过滤掉系统应用,只显示用户安装的应用
|
||
if ((packageInfo.applicationInfo.flags & android.content.pm.ApplicationInfo.FLAG_SYSTEM) == 0) {
|
||
// 只保留包名中包含"pineappletech"的应用
|
||
if (packageInfo.packageName.contains("pineappletech")
|
||
&& !packageInfo.packageName.contains("service")) {
|
||
// 获取应用名称
|
||
String appName = getAppName(packageInfo.packageName);
|
||
packageNamesMap.put(packageInfo.packageName, appName);
|
||
packageNamesList.add(packageInfo.packageName);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 将Map转换为指定格式的JSON数组字符串
|
||
StringBuilder jsonBuilder = new StringBuilder();
|
||
jsonBuilder.append("{\"apps\":[");
|
||
boolean first = true;
|
||
for (Map.Entry<String, String> entry : packageNamesMap.entrySet()) {
|
||
if (!first) {
|
||
jsonBuilder.append(",");
|
||
}
|
||
// 构建 { "name": "应用名称", "address": "包名" } 格式
|
||
jsonBuilder.append("{")
|
||
.append("\"name\":\"").append(entry.getValue()).append("\",")
|
||
.append("\"address\":\"").append(entry.getKey()).append("\"")
|
||
.append("}");
|
||
first = false;
|
||
}
|
||
jsonBuilder.append("]}");
|
||
|
||
Log.i(TAG, "=== 已安装的应用包名 ===");
|
||
Log.i(TAG, "JSON格式: " + jsonBuilder.toString());
|
||
packageNames = jsonBuilder.toString();
|
||
Log.i(TAG, "=== 应用包名列表结束 ===");
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "获取已安装应用列表失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
// 处理接收到的消息
|
||
private void handleUdpReceivedMessage(String message, InetAddress senderAddress, int senderPort) {
|
||
Log.i(TAG, "收到消息 -> " + message);
|
||
handleBoardcastMessage(message, senderAddress, senderPort);
|
||
}
|
||
|
||
private void handleBoardcastMessage(String message, InetAddress senderAddress, int senderPort) {
|
||
// 不处理来自本机相同端口的消息
|
||
if (isLocalMessage(senderAddress, senderPort)) {
|
||
return;
|
||
}
|
||
|
||
if ("DISCOVER".equals(message)) {
|
||
Log.i(TAG, "收到设备发现请求");
|
||
if (deviceInfo.status == 0) {
|
||
// 更新设备状态为在线
|
||
deviceInfo.status = 1;
|
||
}
|
||
|
||
getDeviceInfoAsJson();
|
||
|
||
// 使用主线程池来发送回复,避免并发问题
|
||
networkExecutor.execute(() -> {
|
||
DatagramSocket replySocket = null;
|
||
try {
|
||
// 创建一个新的UDP套接字用于回复
|
||
replySocket = new DatagramSocket(udpSendPort);
|
||
// 准备回复消息,包含sn号
|
||
String responseMessage = deviceInfo.sn;
|
||
byte[] data = responseMessage.getBytes("UTF-8");
|
||
DatagramPacket packet = new DatagramPacket(
|
||
data, data.length, senderAddress, 8988);
|
||
|
||
replySocket.send(packet);
|
||
Log.i(TAG, "回复设备发现请求到: " + senderAddress.getHostAddress() + ":" + 8988);
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "设备发现回复失败: " + e.getMessage());
|
||
e.printStackTrace();
|
||
} finally {
|
||
if (replySocket != null && !replySocket.isClosed()) {
|
||
replySocket.close();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
private void startUdpReceiver() {
|
||
Log.i(TAG, "启动UDP接收器");
|
||
isRunning = true;
|
||
|
||
networkExecutor.execute(() -> {
|
||
try {
|
||
udpSocket = new DatagramSocket(udpReceivePort);
|
||
udpReceivePort = udpSocket.getLocalPort(); // 记录本地端口号
|
||
byte[] buffer = new byte[bufferSize];
|
||
|
||
while (isRunning) {
|
||
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||
udpSocket.receive(packet);
|
||
|
||
String receivedData = new String(
|
||
packet.getData(), 0, packet.getLength()).trim();
|
||
|
||
// 修改为使用新的判断方法
|
||
if (!isLocalMessage(packet.getAddress(), packet.getPort())) {
|
||
handleUdpReceivedMessage(
|
||
receivedData,
|
||
packet.getAddress(),
|
||
packet.getPort());
|
||
} else {
|
||
Log.d(TAG, "忽略来自本机相同端口的消息: " +
|
||
packet.getAddress().getHostAddress() + ":" + packet.getPort());
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
if (isRunning) {
|
||
Log.e(TAG, "UDP接收错误: " + e.getMessage());
|
||
}
|
||
} finally {
|
||
if (udpSocket != null && !udpSocket.isClosed()) {
|
||
udpSocket.close();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 建议callback,单独创建。防止多次调用init后,会绑定多个callback。
|
||
public void initPicoCast() {
|
||
try {
|
||
int result = ToBServiceHelper.getInstance().getServiceBinder().pbsPicoCastInit(callback, 0);
|
||
Log.i(TAG, "pico投屏初始化 " + result);
|
||
} catch (RemoteException e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
|
||
private IIntCallback callback = new IIntCallback.Stub() {
|
||
@Override
|
||
public void callback(int result) throws RemoteException {
|
||
Log.i(TAG, "pico投屏回调 " + result);
|
||
}
|
||
};
|
||
|
||
private void startHttpReceiver() {
|
||
Log.i(TAG, "启动HTTP接收器");
|
||
httpExecutor.execute(() -> {
|
||
try {
|
||
httpServerSocket = new ServerSocket(httpPort);
|
||
httpServerRunning = true;
|
||
Log.i(TAG, "HTTP服务器启动,端口: " + httpPort);
|
||
|
||
while (httpServerRunning) {
|
||
try {
|
||
Socket clientSocket = httpServerSocket.accept();
|
||
httpExecutor.execute(() -> handleHttpRequest(clientSocket));
|
||
} catch (IOException e) {
|
||
if (httpServerRunning) {
|
||
Log.e(TAG, "处理HTTP请求时出错: " + e.getMessage());
|
||
}
|
||
}
|
||
}
|
||
} catch (IOException e) {
|
||
Log.e(TAG, "无法启动HTTP服务器: " + e.getMessage());
|
||
}
|
||
});
|
||
}
|
||
|
||
private void handleHttpRequest(Socket clientSocket) {
|
||
try {
|
||
java.io.BufferedReader in = new java.io.BufferedReader(
|
||
new java.io.InputStreamReader(clientSocket.getInputStream()));
|
||
|
||
// 解析请求行
|
||
String requestLine = in.readLine();
|
||
if (requestLine == null || requestLine.isEmpty()) {
|
||
clientSocket.close();
|
||
return;
|
||
}
|
||
|
||
// 提取请求方法和路径
|
||
String[] requestParts = requestLine.split(" ");
|
||
String method = requestParts[0];
|
||
String path = requestParts[1];
|
||
|
||
Log.i(TAG, "收到HTTP请求: " + requestLine);
|
||
|
||
// 解析请求头
|
||
Map<String, String> headers = new HashMap<>();
|
||
String headerLine;
|
||
int contentLength = 0;
|
||
while ((headerLine = in.readLine()) != null && !headerLine.isEmpty()) {
|
||
String[] parts = headerLine.split(":", 2);
|
||
if (parts.length == 2) {
|
||
headers.put(parts[0].trim(), parts[1].trim());
|
||
if ("Content-Length".equalsIgnoreCase(parts[0].trim())) {
|
||
contentLength = Integer.parseInt(parts[1].trim());
|
||
}
|
||
}
|
||
}
|
||
|
||
// 读取请求体(如果有)
|
||
StringBuilder body = new StringBuilder();
|
||
if (contentLength > 0) {
|
||
char[] buffer = new char[contentLength];
|
||
int totalRead = 0;
|
||
while (totalRead < contentLength) {
|
||
int read = in.read(buffer, totalRead, contentLength - totalRead);
|
||
if (read == -1)
|
||
break;
|
||
totalRead += read;
|
||
}
|
||
body.append(buffer, 0, totalRead);
|
||
}
|
||
|
||
Log.i(TAG, "请求体: " + body.toString());
|
||
|
||
// 处理预检请求(OPTIONS)
|
||
String responseContent = "";
|
||
int responseCode = 200;
|
||
|
||
if ("OPTIONS".equalsIgnoreCase(method)) {
|
||
// 对于预检请求,直接返回成功
|
||
responseContent = "{\"status\":\"preflight-success\"}";
|
||
} else {
|
||
// 根据请求路径和方法处理请求
|
||
// 解析请求体中的JSON数据
|
||
try {
|
||
org.json.JSONObject jsonBody = new org.json.JSONObject(body.toString());
|
||
String intent = jsonBody.optString("intent");
|
||
|
||
// 获取所有包信息
|
||
if ("getPackageInfos".equals(intent)) {
|
||
Log.i(TAG, "执行获取所有包信息");
|
||
// 修复后的代码:
|
||
getInstalledApps(); // 更新包信息
|
||
responseContent = "{\"code\":200,\"message\":\"获取成功\",\"data\":" + packageNames + "}";
|
||
}
|
||
// 播放影片
|
||
else if ("playFilm".equals(intent)) {
|
||
String packageName = jsonBody.optString("packageName");
|
||
Log.i(TAG, "执行播放: " + packageName);
|
||
// 点亮屏幕
|
||
try {
|
||
toBServiceHelper.getServiceBinder().pbsScreenOn();
|
||
Log.i(TAG, "屏幕点亮命令发送成功");
|
||
} catch (android.os.RemoteException e) {
|
||
Log.e(TAG, "调用pbsScreenOn失败: " + e.getMessage());
|
||
}
|
||
launchAppDirectly(packageName);
|
||
deviceInfo.status = 2;
|
||
deviceInfo.playing = packageNamesMap.get(packageName);
|
||
responseContent = "{\"code\":200,\"message\":\"已执行播放\"}";
|
||
}
|
||
// 停止播放
|
||
else if ("stopFilm".equals(intent)) {
|
||
Log.i(TAG, "执行停止播放");
|
||
// 点亮屏幕
|
||
try {
|
||
toBServiceHelper.getServiceBinder().pbsScreenOn();
|
||
Log.i(TAG, "屏幕点亮命令发送成功");
|
||
} catch (android.os.RemoteException e) {
|
||
Log.e(TAG, "调用pbsScreenOn失败: " + e.getMessage());
|
||
}
|
||
forceStopAllPineappleApps();
|
||
responseContent = "{\"code\":200,\"message\":\"已执行停止播放\"}";
|
||
}
|
||
// ping
|
||
else if ("ping".equals(intent)) {
|
||
Log.i(TAG, "执行ping");
|
||
responseContent = "{\"code\":200,\"message\":\"成功连接\",\"data\":\"" + getDeviceInfoAsJson()
|
||
+ "\"}";
|
||
}
|
||
|
||
} catch (org.json.JSONException e) {
|
||
Log.e(TAG, "解析请求体JSON失败: " + e.getMessage());
|
||
responseContent = "{\"status\":\"fail\",\"message\":\"解析失败\"}";
|
||
responseCode = 400;
|
||
}
|
||
}
|
||
|
||
// 发送响应
|
||
OutputStream out = clientSocket.getOutputStream();
|
||
String httpResponse = "HTTP/1.1 " + responseCode + " OK\r\n" +
|
||
"Content-Type: application/json; charset=utf-8\r\n" +
|
||
"Access-Control-Allow-Origin: *\r\n" +
|
||
"Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS\r\n" +
|
||
"Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With\r\n"
|
||
+
|
||
"Access-Control-Max-Age: 3600\r\n" +
|
||
"Content-Length: " + responseContent.getBytes("UTF-8").length + "\r\n" +
|
||
"\r\n" +
|
||
responseContent;
|
||
|
||
out.write(httpResponse.getBytes("UTF-8"));
|
||
out.flush();
|
||
out.close();
|
||
clientSocket.close();
|
||
} catch (
|
||
|
||
IOException e) {
|
||
Log.e(TAG, "处理HTTP客户端连接时出错: " + e.getMessage());
|
||
try {
|
||
clientSocket.close();
|
||
} catch (IOException ioException) {
|
||
Log.e(TAG, "关闭客户端连接时出错: " + ioException.getMessage());
|
||
}
|
||
}
|
||
}
|
||
|
||
// // 然后修改launchAppDirectly方法使用Service的Context
|
||
// public void launchAppDirectly(String packageName) {
|
||
// Log.i(TAG, "直接启动应用: " + packageName);
|
||
// try {
|
||
// // 使用Service自己的Context而不是UnityPlayer的Activity
|
||
// if (serviceContext == null) {
|
||
// Log.e(TAG, "Service Context为空");
|
||
// return;
|
||
// }
|
||
|
||
// forceStopAllPineappleApps();
|
||
|
||
// try {
|
||
// Thread.sleep(500);
|
||
// } catch (InterruptedException e) {
|
||
// // 忽略
|
||
// }
|
||
|
||
// // 使用Service Context启动应用
|
||
// android.content.Intent launchIntent = serviceContext.getPackageManager()
|
||
// .getLaunchIntentForPackage(packageName);
|
||
|
||
// if (launchIntent != null) {
|
||
// // 添加传递给目标应用的参数
|
||
// launchIntent.putExtra("service_ip", deviceInfo.ip);
|
||
// launchIntent.putExtra("service_port", httpPort);
|
||
|
||
// launchIntent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
|
||
// launchIntent.addFlags(android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||
// launchIntent.addFlags(android.content.Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
|
||
// launchIntent.addFlags(android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
|
||
// serviceContext.startActivity(launchIntent);
|
||
// Log.i(TAG, "成功启动应用: " + packageName);
|
||
// } else {
|
||
// Log.e(TAG, "无法找到应用的启动Intent: " + packageName);
|
||
// }
|
||
// } catch (Exception e) {
|
||
// Log.e(TAG, "启动应用失败: " + e.getMessage());
|
||
// e.printStackTrace();
|
||
// }
|
||
// }
|
||
|
||
// 然后修改launchAppDirectly方法使用Service的Context
|
||
public void launchAppDirectly(String packageName) {
|
||
Log.i(TAG, "直接启动应用: " + packageName);
|
||
try {
|
||
// 使用Pico企业服务启动应用
|
||
if (toBServiceHelper == null || toBServiceHelper.getServiceBinder() == null) {
|
||
Log.e(TAG, "ToB Service Helper 或 Binder 为空");
|
||
return;
|
||
}
|
||
|
||
forceStopAllPineappleApps();
|
||
|
||
try {
|
||
Thread.sleep(500);
|
||
} catch (InterruptedException e) {
|
||
// 忽略
|
||
}
|
||
|
||
// 使用Pico的企业服务API启动应用
|
||
String action = "picovr.intent.action.player"; // 根据需要调整action
|
||
String extra = "{\"uri\":\"/sdcard/test.mp4\",\"videoType\":0,\"videoSource\":1}"; // 这里需要根据实际需求调整参数
|
||
String[] categories = { android.content.Intent.CATEGORY_DEFAULT };
|
||
int[] flags = { android.content.Intent.FLAG_ACTIVITY_NEW_TASK };
|
||
int ext = 0;
|
||
|
||
// 如果packageName是实际的应用包名,可以用它替换action参数
|
||
// 目前使用您提供的Pico API格式
|
||
toBServiceHelper.getServiceBinder().pbsStartActivity(
|
||
packageName, // packageName
|
||
"com.unity3d.player.UnityPlayerActivity", // className - 留空让系统自动选择主Activity
|
||
action, // action
|
||
extra, // extra - JSON格式的额外参数
|
||
categories, // categories
|
||
flags, // flags
|
||
ext // ext
|
||
);
|
||
|
||
Log.i(TAG, "通过Pico企业服务启动应用: " + packageName + " with action: " + action);
|
||
|
||
// 更新设备状态
|
||
deviceInfo.status = 2;
|
||
deviceInfo.playing = packageNamesMap.get(packageName);
|
||
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "通过Pico服务启动应用失败: " + e.getMessage());
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
|
||
public void forceStopAllPineappleApps() {
|
||
Log.i(TAG, "强制停止所有Pineapple应用...");
|
||
|
||
// 准备参数
|
||
int[] pids = {}; // 要终止的进程ID数组
|
||
String[] packageNames = packageNamesList.toArray(new String[packageNamesList.size()]);
|
||
int ext = 0; // 扩展参数,根据文档使用
|
||
|
||
try {
|
||
toBServiceHelper.getServiceBinder().pbsKillAppsByPidOrPackageName(pids, packageNames, ext);
|
||
} catch (android.os.RemoteException e) {
|
||
Log.e(TAG, "调用pbsKillAppsByPidOrPackageName失败: " + e.getMessage());
|
||
e.printStackTrace();
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "调用pbsKillAppsByPidOrPackageName时发生未知错误: " + e.getMessage());
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
|
||
// ================= Unity通信方法 =================
|
||
public void callUnity(String gameObjectName, String methodName, String message) {
|
||
try {
|
||
UnityPlayer.currentActivity.runOnUiThread(() -> {
|
||
UnityPlayer.UnitySendMessage(gameObjectName, methodName, message);
|
||
});
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "调用Unity方法失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
// ================= 静态方法(Unity调用)=================
|
||
public static void startMyForegroundService() {
|
||
Log.i("PineappleService", "开始启动服务...");
|
||
try {
|
||
Class<?> unityPlayerClass = Class.forName("com.unity3d.player.UnityPlayer");
|
||
Field activityField = unityPlayerClass.getField("currentActivity");
|
||
Object activity = activityField.get(null);
|
||
|
||
Method getApplicationContextMethod = activity.getClass().getMethod("getApplicationContext");
|
||
Context context = (Context) getApplicationContextMethod.invoke(activity);
|
||
|
||
Intent intent = new Intent(context, Main.class);
|
||
context.startForegroundService(intent);
|
||
} catch (Exception e) {
|
||
Log.e("PineappleService", "启动服务失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
private String getDeviceInfoAsJson() {
|
||
try {
|
||
deviceInfo.ip = getIpAddressFromPico();
|
||
try {
|
||
deviceInfo.sn = toBServiceHelper.getServiceBinder().pbsStateGetDeviceInfo(
|
||
PBS_SystemInfoEnum.EQUIPMENT_SN,
|
||
0);
|
||
} catch (android.os.RemoteException e) {
|
||
Log.e(TAG, "获取设备序列号失败: " + e.getMessage());
|
||
deviceInfo.sn = "未知序列号";
|
||
}
|
||
|
||
// 获取电量信息字符串
|
||
String powerStr;
|
||
try {
|
||
powerStr = toBServiceHelper.getServiceBinder().pbsStateGetDeviceInfo(
|
||
PBS_SystemInfoEnum.ELECTRIC_QUANTITY,
|
||
0);
|
||
} catch (android.os.RemoteException e) {
|
||
Log.e(TAG, "获取电量信息失败: " + e.getMessage());
|
||
powerStr = "0";
|
||
}
|
||
|
||
// 将字符串转换为整数
|
||
try {
|
||
deviceInfo.power = Integer.parseInt(powerStr);
|
||
} catch (NumberFormatException e) {
|
||
Log.e(TAG, "解析电量信息失败: " + powerStr + ", 错误: " + e.getMessage());
|
||
deviceInfo.power = 0; // 默认值
|
||
}
|
||
|
||
org.json.JSONObject data = new org.json.JSONObject();
|
||
data.put("ip", deviceInfo.ip);
|
||
data.put("sn", deviceInfo.sn);
|
||
data.put("power", deviceInfo.power);
|
||
data.put("status", deviceInfo.status);
|
||
data.put("playing", deviceInfo.playing);
|
||
return data.toString().replace("\"", "\\\"");
|
||
} catch (org.json.JSONException e) {
|
||
Log.e(TAG, "序列化设备信息失败: " + e.getMessage());
|
||
return "{}";
|
||
}
|
||
}
|
||
|
||
// ================= MQTT 服务方法 =================
|
||
|
||
/**
|
||
* 初始化 MQTT 服务
|
||
*/
|
||
private void initMqttService() {
|
||
Log.i(TAG, "初始化 MQTT 服务...");
|
||
mqttScheduler = Executors.newSingleThreadScheduledExecutor();
|
||
connectToMqttBroker();
|
||
}
|
||
|
||
/**
|
||
* 连接到 MQTT Broker
|
||
*/
|
||
private void connectToMqttBroker() {
|
||
networkExecutor.execute(() -> {
|
||
try {
|
||
// 获取设备序列号作为客户端ID
|
||
String clientId = MQTT_CLIENT_ID_PREFIX + System.currentTimeMillis();
|
||
try {
|
||
if (toBServiceHelper != null && toBServiceHelper.getServiceBinder() != null) {
|
||
String sn = toBServiceHelper.getServiceBinder().pbsStateGetDeviceInfo(
|
||
PBS_SystemInfoEnum.EQUIPMENT_SN, 0);
|
||
if (sn != null && !sn.isEmpty() && !"未知序列号".equals(sn)) {
|
||
clientId = MQTT_CLIENT_ID_PREFIX + sn;
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
Log.w(TAG, "获取序列号失败,使用默认客户端ID: " + e.getMessage());
|
||
}
|
||
|
||
mqttClient = new MqttClient(MQTT_BROKER_URL, clientId, new MemoryPersistence());
|
||
|
||
MqttConnectOptions options = new MqttConnectOptions();
|
||
options.setCleanSession(true);
|
||
options.setConnectionTimeout(MQTT_CONNECTION_TIMEOUT);
|
||
options.setKeepAliveInterval(MQTT_KEEP_ALIVE);
|
||
options.setAutomaticReconnect(true);
|
||
// 设置最大重连间隔为60秒
|
||
options.setMaxReconnectDelay(60000);
|
||
|
||
mqttClient.setCallback(new MqttCallback() {
|
||
@Override
|
||
public void connectionLost(Throwable cause) {
|
||
Log.e(TAG, "MQTT 连接丢失: " + cause.getMessage());
|
||
// 触发重连
|
||
scheduleMqttReconnect();
|
||
}
|
||
|
||
@Override
|
||
public void messageArrived(String topic, MqttMessage message) {
|
||
Log.i(TAG, "收到 MQTT 消息 - Topic: " + topic + ", Message: " + new String(message.getPayload()));
|
||
}
|
||
|
||
@Override
|
||
public void deliveryComplete(IMqttDeliveryToken token) {
|
||
// 消息发送完成
|
||
}
|
||
});
|
||
|
||
mqttClient.connect(options);
|
||
Log.i(TAG, "MQTT 连接成功 - ClientID: " + clientId);
|
||
// 重置重连计数
|
||
mqttReconnectAttempts = 0;
|
||
isMqttReconnecting = false;
|
||
|
||
// 启动定时上报任务
|
||
startMqttReportTask();
|
||
|
||
} catch (MqttException e) {
|
||
Log.e(TAG, "MQTT 连接失败: " + e.getMessage());
|
||
// 连接失败也触发重连
|
||
scheduleMqttReconnect();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 调度 MQTT 重连
|
||
*/
|
||
private void scheduleMqttReconnect() {
|
||
if (isMqttReconnecting) {
|
||
Log.d(TAG, "MQTT 重连已在进行中,跳过");
|
||
return;
|
||
}
|
||
|
||
if (mqttReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||
Log.e(TAG, "MQTT 重连次数超过最大限制,停止重连");
|
||
return;
|
||
}
|
||
|
||
isMqttReconnecting = true;
|
||
mqttReconnectAttempts++;
|
||
|
||
long delay = Math.min(RECONNECT_DELAY_MS * mqttReconnectAttempts, 60000); // 最大延迟60秒
|
||
Log.i(TAG, "MQTT 将在 " + delay + "ms 后进行第 " + mqttReconnectAttempts + " 次重连");
|
||
|
||
networkExecutor.execute(() -> {
|
||
try {
|
||
Thread.sleep(delay);
|
||
// 清理旧连接
|
||
if (mqttClient != null) {
|
||
try {
|
||
mqttClient.disconnectForcibly();
|
||
mqttClient.close();
|
||
} catch (Exception e) {
|
||
// 忽略清理错误
|
||
}
|
||
mqttClient = null;
|
||
}
|
||
// 重新连接
|
||
isMqttReconnecting = false;
|
||
connectToMqttBroker();
|
||
} catch (InterruptedException e) {
|
||
Log.e(TAG, "MQTT 重连等待被中断: " + e.getMessage());
|
||
isMqttReconnecting = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 启动 MQTT 定时上报任务
|
||
*/
|
||
private void startMqttReportTask() {
|
||
mqttScheduler.scheduleAtFixedRate(() -> {
|
||
try {
|
||
if (mqttClient != null && mqttClient.isConnected()) {
|
||
publishDeviceStatus();
|
||
} else {
|
||
Log.w(TAG, "MQTT 未连接,跳过上报");
|
||
}
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "MQTT 上报任务异常: " + e.getMessage());
|
||
}
|
||
}, 0, MQTT_REPORT_INTERVAL, TimeUnit.SECONDS);
|
||
Log.i(TAG, "MQTT 定时上报任务已启动,间隔: " + MQTT_REPORT_INTERVAL + " 秒");
|
||
}
|
||
|
||
/**
|
||
* 发布设备状态到 MQTT
|
||
*/
|
||
private void publishDeviceStatus() {
|
||
try {
|
||
// 更新设备信息
|
||
String deviceJson = getDeviceInfoAsJsonForMqtt();
|
||
String topic = "device/" + deviceInfo.sn + "/status";
|
||
|
||
MqttMessage message = new MqttMessage(deviceJson.getBytes("UTF-8"));
|
||
message.setQos(0);
|
||
message.setRetained(false);
|
||
|
||
mqttClient.publish(topic, message);
|
||
Log.d(TAG, "MQTT 上报成功 - Topic: " + topic);
|
||
|
||
} catch (Exception e) {
|
||
Log.e(TAG, "MQTT 上报失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取设备信息 JSON(用于 MQTT 上报)
|
||
*/
|
||
private String getDeviceInfoAsJsonForMqtt() {
|
||
try {
|
||
deviceInfo.ip = getIpAddressFromPico();
|
||
try {
|
||
deviceInfo.sn = toBServiceHelper.getServiceBinder().pbsStateGetDeviceInfo(
|
||
PBS_SystemInfoEnum.EQUIPMENT_SN, 0);
|
||
} catch (android.os.RemoteException e) {
|
||
Log.e(TAG, "获取设备序列号失败: " + e.getMessage());
|
||
deviceInfo.sn = "未知序列号";
|
||
}
|
||
|
||
// 获取电量信息字符串
|
||
String powerStr;
|
||
try {
|
||
powerStr = toBServiceHelper.getServiceBinder().pbsStateGetDeviceInfo(
|
||
PBS_SystemInfoEnum.ELECTRIC_QUANTITY, 0);
|
||
} catch (android.os.RemoteException e) {
|
||
Log.e(TAG, "获取电量信息失败: " + e.getMessage());
|
||
powerStr = "0";
|
||
}
|
||
|
||
// 将字符串转换为整数
|
||
try {
|
||
deviceInfo.power = Integer.parseInt(powerStr);
|
||
} catch (NumberFormatException e) {
|
||
Log.e(TAG, "解析电量信息失败: " + powerStr + ", 错误: " + e.getMessage());
|
||
deviceInfo.power = 0;
|
||
}
|
||
|
||
org.json.JSONObject data = new org.json.JSONObject();
|
||
data.put("ip", deviceInfo.ip);
|
||
data.put("sn", deviceInfo.sn);
|
||
data.put("power", deviceInfo.power);
|
||
data.put("status", deviceInfo.status);
|
||
data.put("playing", deviceInfo.playing);
|
||
data.put("timestamp", System.currentTimeMillis());
|
||
return data.toString();
|
||
} catch (org.json.JSONException e) {
|
||
Log.e(TAG, "序列化设备信息失败: " + e.getMessage());
|
||
return "{}";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 断开 MQTT 连接
|
||
*/
|
||
private void disconnectMqtt() {
|
||
try {
|
||
if (mqttScheduler != null) {
|
||
mqttScheduler.shutdownNow();
|
||
Log.i(TAG, "MQTT 定时任务已停止");
|
||
}
|
||
|
||
if (mqttClient != null && mqttClient.isConnected()) {
|
||
mqttClient.disconnect();
|
||
mqttClient.close();
|
||
Log.i(TAG, "MQTT 连接已断开");
|
||
}
|
||
} catch (MqttException e) {
|
||
Log.e(TAG, "断开 MQTT 连接失败: " + e.getMessage());
|
||
}
|
||
}
|
||
}
|