all = new ArrayList<>(commands);
+ commands.clear();
+ return all;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/receiver/BootCompletedReceiver.java b/app/src/main/java/com/pukka/jiecheng/voice/receiver/BootCompletedReceiver.java
new file mode 100644
index 0000000..7f91db6
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/receiver/BootCompletedReceiver.java
@@ -0,0 +1,78 @@
+/**
+ * @file BootCompletedReceiver.java
+ * @description 开机完成广播接收器。监听 BOOT_COMPLETED 与 LOCKED_BOOT_COMPLETED,拉起 :voice 进程的 VoiceKeepAliveService 前台服务,从设备开机起就持续监听 OEM 创维语音广播。
+ * @author ainetv
+ * @date 2026-08-31
+ */
+package com.pukka.jiecheng.voice.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.content.ContextCompat;
+
+import com.pukka.jiecheng.MainActivity;
+import com.pukka.jiecheng.voice.service.VoiceKeepAliveService;
+
+/**
+ * 开机完成广播接收器。
+ *
+ * Manifest 静态注册,受系统权限 {@code RECEIVE_BOOT_COMPLETED} 保护。
+ * 双重职责:
+ *
+ * - 拉起 {@link VoiceKeepAliveService} 进入 :voice 进程,从设备开机起就持续
+ * 监听 OEM 创维语音广播;
+ * - 注册 {@link KeepAlivePingReceiver#scheduleFirstPing} 自广播兜底——
+ * 一旦主路径因 STOPPED / ROM 拦截 / 5s ANR 失败,
+ * {@link KeepAlivePingReceiver} 将在 5 秒后接力重启 :voice 进程。
+ *
+ *
+ * Android 8+ 关键约束:从 {@code BOOT_COMPLETED} 后台广播启服务必须走
+ * {@link ContextCompat#startForegroundService(Context, Intent)},服务在 5 秒内必须
+ * 调 {@code startForeground(...)},否则 ANR 崩溃。
+ */
+public class BootCompletedReceiver extends BroadcastReceiver {
+
+ private static final String LOG_TAG = "VoiceCommand";
+
+ @Override
+ public void onReceive(Context ctx, Intent intent) {
+ if (ctx == null || intent == null) return;
+ String action = intent.getAction();
+ Log.i(LOG_TAG, "voice keepalive service start requested, action=" + action);
+ boolean isBootCompleted = Intent.ACTION_BOOT_COMPLETED.equals(action);
+ if (!isBootCompleted) {
+ return;
+ }
+ try {
+ // 启动顺序关键:必须先 cold-launch MainActivity,再启 FGS。
+ // 1) Receiver 由系统启动,startActivity 不受 Android 12+
+ // Background Activity Start 限制;
+ // 2) MainActivity 起来后 → IndexActivity 在栈顶 → 主进程 process state=HOME
+ // (前台),后续主进程内任何 startActivity 都合法;
+ // 3) 再启 VoiceKeepAliveService(manifest 已去 :voice 后缀,跑在主进程),
+ // FGS 内注册 SkyworthVoiceAdapter 监听 OEM 创维 ACTION_VOD 广播。
+ // 如果先启 Service 再启 Activity:Service.onCreate 时主进程没前台 Activity,
+ // FGS 类型 specialUse 不在 Android 12+ 豁免列表,自己 startActivity 会被拦。
+ Context appCtx = ctx.getApplicationContext();
+ Context targetCtx = appCtx != null ? appCtx : ctx;
+
+// Intent launcher = new Intent(targetCtx, MainActivity.class);
+// launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+// ctx.startActivity(launcher);
+
+ ContextCompat.startForegroundService(targetCtx,
+ new Intent(targetCtx, VoiceKeepAliveService.class));
+
+ // 自广播兜底:万一主路径因 STOPPED / 5s 超时 / ROM 拦截失败,
+ // 5s 后 KeepAlivePingReceiver 会自动接力重启 :voice。
+ // 自广播是同包 explicit broadcast,不受 STOPPED 状态限制。
+ KeepAlivePingReceiver.scheduleFirstPing(targetCtx);
+ } catch (Throwable t) {
+ Log.w(LOG_TAG, "voice keepalive startup failed: " + t.getMessage());
+ }
+ }
+}
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/receiver/KeepAlivePingReceiver.java b/app/src/main/java/com/pukka/jiecheng/voice/receiver/KeepAlivePingReceiver.java
new file mode 100644
index 0000000..f6cb4b5
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/receiver/KeepAlivePingReceiver.java
@@ -0,0 +1,146 @@
+/**
+ * @file KeepAlivePingReceiver.java
+ * @description 自广播兜底接收器,处理 com.pukka.jiecheng.action.KEEPALIVE_BOOT 自定义 action。
+ * 解决 STOPPED 状态 / ROM 拦截 BOOT_COMPLETED 时 :voice 进程无法自启的问题。
+ * @author ainetv
+ * @date 2026-09-01
+ */
+package com.pukka.jiecheng.voice.receiver;
+
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+
+import androidx.core.content.ContextCompat;
+
+import com.pukka.jiecheng.voice.service.VoiceKeepAliveService;
+
+/**
+ * 自广播兜底接收器。
+ *
+ *
背景:系统 {@code BOOT_COMPLETED} 在以下场景下不会被派发:
+ *
+ * - 应用处于 STOPPED 状态(force-stop / 装机后从未启动 / 部分 ROM OTA 后)
+ * - OEM ROM 白名单未包含本包名(机顶盒常见)
+ * - Receiver 5 秒内未完成 {@code startForeground} 导致 ANR 崩溃
+ *
+ *
+ * 解决路径:监听同包内 explicit broadcast {@link #ACTION_KEEPALIVE_BOOT},
+ * 通过 {@code AlarmManager} 注册周期性 PendingIntent 给自己投递。
+ * 同包 explicit broadcast 不受 STOPPED 状态限制(Android 官方文档
+ * §Background execution limits),所以即便主路径失败,仍能由本 receiver 接力
+ * 拉起 {@link VoiceKeepAliveService}。
+ *
+ *
触发链路:
+ *
+ * BOOT_COMPLETED 或 任意应用入口(MyApplication.onCreate 等)
+ * └─ AlarmManager.setExactAndAllowWhileIdle → 5s 后
+ * └─ KEEPALIVE_BOOT 自广播(同包显式投递)
+ * └─ KeepAlivePingReceiver.onReceive
+ * ├─ 检查 :voice 进程,未存活则 startForegroundService
+ * └─ 注册下一次 24h 周期 ping 续命
+ *
+ *
+ * Manifest 静态注册,{@code exported=false}(不暴露给外部,仅接收同包显式 Intent)。
+ */
+public class KeepAlivePingReceiver extends BroadcastReceiver {
+
+ public static final String ACTION_KEEPALIVE_BOOT =
+ "com.pukka.jiecheng.action.KEEPALIVE_BOOT";
+
+ /** Ping 间隔:24 小时。AlarmManager 周期唤醒自广播保持 :voice 长期存活。 */
+ public static final long PING_INTERVAL_MS = 24L * 60L * 60L * 1000L;
+
+ /** 首次 ping 延迟:BOOT_COMPLETED 之后给系统留 5s 缓冲,避免与主路径抢进程。 */
+ public static final long FIRST_PING_DELAY_MS = 5L * 1000L;
+
+ private static final String LOG_TAG = "VoiceCommand";
+ private static final int REQ_CODE_PING = 0x774B4150; // "vKAP" in ascii
+
+ @Override
+ public void onReceive(Context ctx, Intent intent) {
+ if (ctx == null || intent == null) return;
+ if (!ACTION_KEEPALIVE_BOOT.equals(intent.getAction())) {
+ return;
+ }
+ Context appCtx = ctx.getApplicationContext();
+ Context targetCtx = appCtx != null ? appCtx : ctx;
+
+ // 1) 检查 :voice 进程是否存活;未存活则启动 FGS
+ if (!isVoiceProcessAlive(targetCtx)) {
+ Log.i(LOG_TAG, "keepalive ping: voice process dead, start FGS");
+ try {
+ ContextCompat.startForegroundService(targetCtx,
+ new Intent(targetCtx, VoiceKeepAliveService.class));
+ } catch (Throwable t) {
+ Log.w(LOG_TAG, "keepalive ping: start FGS failed: " + t.getMessage());
+ }
+ } else {
+ Log.i(LOG_TAG, "keepalive ping: voice process alive, skip start");
+ }
+
+ // 2) 注册下一次 ping,保证链路自续
+ scheduleNextPing(targetCtx, PING_INTERVAL_MS);
+ }
+
+ /**
+ * 由 {@link BootCompletedReceiver} 或 {@code MyApplication} 调用,注册首次 ping。
+ * 5s 后派发一次 KEEPALIVE_BOOT 自广播到本 receiver。
+ */
+ public static void scheduleFirstPing(Context ctx) {
+ scheduleNextPing(ctx, FIRST_PING_DELAY_MS);
+ }
+
+ /**
+ * 注册下一次 ping。{@code AlarmManager.setExactAndAllowWhileIdle} 在 Doze 模式下
+ * 也能保证触发,但 doze 期间有 ~9 分钟延迟——对 24h 周期而言可接受。
+ */
+ public static void scheduleNextPing(Context ctx, long delayMs) {
+ if (ctx == null) return;
+ try {
+ Intent ping = new Intent(ACTION_KEEPALIVE_BOOT);
+ ping.setPackage(ctx.getPackageName()); // 显式同包,绕过 STOPPED 限制
+ int flags = PendingIntent.FLAG_UPDATE_CURRENT;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ flags |= PendingIntent.FLAG_IMMUTABLE;
+ }
+ PendingIntent pi = PendingIntent.getBroadcast(
+ ctx, REQ_CODE_PING, ping, flags);
+ AlarmManager am = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
+ if (am == null) return;
+ long triggerAt = SystemClock.elapsedRealtime() + delayMs;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ am.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ triggerAt, pi);
+ } else {
+ am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAt, pi);
+ }
+ Log.i(LOG_TAG, "keepalive ping scheduled, delay=" + delayMs + "ms");
+ } catch (Throwable t) {
+ Log.w(LOG_TAG, "keepalive ping schedule failed: " + t.getMessage());
+ }
+ }
+
+ /**
+ * 检测 :voice 进程是否在运行。失败保守返回 false,让上层尝试启动。
+ */
+ private boolean isVoiceProcessAlive(Context ctx) {
+ try {
+ ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
+ if (am == null) return false;
+ String voiceProcess = ctx.getPackageName() + ":voice";
+ for (ActivityManager.RunningAppProcessInfo p : am.getRunningAppProcesses()) {
+ if (voiceProcess.equals(p.processName)) return true;
+ }
+ return false;
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceCommandReceiver.java b/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceCommandReceiver.java
new file mode 100644
index 0000000..f920f0b
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceCommandReceiver.java
@@ -0,0 +1,104 @@
+package com.pukka.jiecheng.voice.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.pukka.jiecheng.voice.log.VoiceCommandLogger;
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.protocol.VoiceProtocolConstants;
+import com.pukka.jiecheng.voice.service.VoiceCommandService;
+public class VoiceCommandReceiver extends BroadcastReceiver {
+
+ /**
+ * 白名单(文档 §6.2)。
+ *
MVP 阶段内置一个联调专用 sourceApp({@code com.test.voicehelper}),
+ * 便于 {@code test/voice_helper/} 脚本可联调;正式接入第三方语音厂商时按需扩展此列表
+ * 并配合 manifest 中的 signature permission 拦截非白名单。
+ *
+ */
+ public static final String[] DEFAULT_WHITELIST = new String[]{
+ "com.test.voicehelper",
+ };
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null) return;
+ String action = intent.getAction();
+ if (!VoiceProtocolConstants.ACTION_VOICE_COMMAND.equals(action)) {
+ return;
+ }
+
+ String requestId = intent.getStringExtra(VoiceProtocolConstants.EXTRA_REQUEST_ID);
+ String sourceApp = intent.getStringExtra(VoiceProtocolConstants.EXTRA_SOURCE_APP);
+ String sourcePlatform = intent.getStringExtra(VoiceProtocolConstants.EXTRA_SOURCE_PLATFORM);
+ String commandType = intent.getStringExtra(VoiceProtocolConstants.EXTRA_COMMAND_TYPE);
+ String payload = intent.getStringExtra(VoiceProtocolConstants.EXTRA_PAYLOAD);
+ String callbackAction = intent.getStringExtra(VoiceProtocolConstants.EXTRA_CALLBACK_ACTION);
+ long timestamp = intent.getLongExtra(VoiceProtocolConstants.EXTRA_TIMESTAMP, 0L);
+
+ if (TextUtils.isEmpty(requestId) || TextUtils.isEmpty(commandType)) {
+ VoiceCommandLogger.dropped("missing required extras", "requestId=" + requestId);
+ return;
+ }
+
+ VoiceCommandLogger.received(requestId, sourceApp, sourcePlatform, commandType, payload);
+
+ Log.w(VoiceProtocolConstants.LOG_TAG,
+ "[VoiceCommandReceiver] onReceive: A ");
+// if (!isWhitelisted(sourceApp)) {
+// VoiceCommandLogger.dropped("sourceApp not in whitelist",
+// "sourceApp=" + sourceApp + " requestId=" + requestId);
+// return;
+// }
+
+ VoiceCommandService service = VoiceCommandService.getInstance(context);
+ VoiceCommand cmd;
+ try {
+ cmd = service.getCoordinator().parse(commandType, sourcePlatform, payload);
+ } catch (IllegalArgumentException ex) {
+ VoiceCommandLogger.dropped("parse failed", ex.getMessage());
+ return;
+ }
+ Log.w(VoiceProtocolConstants.LOG_TAG,
+ "[VoiceCommandReceiver] onReceive: ");
+ VoiceCommand cmdWithExtras = VoiceCommand.builder()
+ .commandType(cmd.getCommandType())
+ .requestId(requestId)
+ .sourceApp(sourceApp)
+ .sourcePlatform(sourcePlatform)
+ .callbackAction(callbackAction)
+ .timestamp(timestamp)
+ .payload(cmd.getPayload())
+ .build();
+ VoiceCommandLogger.parsed(cmdWithExtras);
+
+ if (!service.isActivated()) {
+ service.enqueueForColdStart(cmdWithExtras);
+ try {
+ Intent launcher = new Intent();
+ launcher.setClassName(context,
+ "com.pukka.jiecheng.index.IndexActivity");
+ launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ context.startActivity(launcher);
+ } catch (Throwable t) {
+ Log.w(VoiceProtocolConstants.LOG_TAG,
+ "[VoiceCommandReceiver] wakeup failed: " + t.getMessage());
+ }
+ } else {
+ service.handle(cmdWithExtras);
+ }
+ }
+
+ private boolean isWhitelisted(String sourceApp) {
+ return true;
+// if (TextUtils.isEmpty(sourceApp)) return false;
+// for (String w : DEFAULT_WHITELIST) {
+// if (w.equals(sourceApp)) return true;
+// }
+// return false;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceInternalReceiver.java b/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceInternalReceiver.java
new file mode 100644
index 0000000..56f4156
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceInternalReceiver.java
@@ -0,0 +1,82 @@
+package com.pukka.jiecheng.voice.receiver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.pukka.jiecheng.voice.log.VoiceCommandLogger;
+import com.pukka.jiecheng.voice.parser.SkyworthVodParser;
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.protocol.VoiceExtras;
+import com.pukka.jiecheng.voice.service.VoiceCommandService;
+
+/**
+ * 主进程内部接收器,转发 :voice 进程跨进程投递的语音命令。
+ *
+ * 接收 :voice 进程 {@link SkyworthVodParser} 归一化后的原始 OEM 字段,
+ * 在主进程内完成解析 + 路由:
+ *
+ * - 主进程已激活({@code svc.isActivated() == true})→ 直接 {@code svc.handle(cmd)}
+ * - 主进程未激活 → 仅 cold launch MainActivity(带 {@code __voice_pending} + 4 个 OEM extras),
+ * 由 MainActivity.goToActivity() 入队到 coldStartQueue(§4.1/§4.2 共用单点入队)
+ *
+ *
+ * 设计要点(避免双入队):receiver 只 cold-launch,不再调用
+ * {@code svc.enqueueForColdStart(cmd)}——把入队职责完全收敛到 MainActivity.goToActivity。
+ * §4.2 hot path 在主进程已激活时直接 handle;§4.2 cold path(主进程活但未激活)通过
+ * MainActivity 入队一次。两种 cold path 共用 MainActivity.enqueuePendingVoiceCommand
+ * 这一个入队点,避免双入队。
+ *
+ *
详见 {@code docs/2026-08-28-语音冷启动链路设计方案.md} §4.2。
+ */
+public class VoiceInternalReceiver extends BroadcastReceiver {
+
+ /** 主进程内部广播 action,与 OEM action 完全解耦。 */
+ public static final String ACTION_VOICE_FORWARD =
+ "com.pukka.jiecheng.voice.action.INTERNAL_FORWARD";
+
+ private static final String LOG_TAG = "VoiceCommand";
+ private static final String MAIN_ACTIVITY_CLASS = "com.pukka.jiecheng.MainActivity";
+
+ @Override
+ public void onReceive(Context ctx, Intent intent) {
+ if (ctx == null || intent == null) return;
+ if (!ACTION_VOICE_FORWARD.equals(intent.getAction())) return;
+
+ VoiceCommandService service = VoiceCommandService.getInstance(ctx);
+ VoiceCommand cmd;
+ try {
+ cmd = SkyworthVodParser.parseFromForward(service, intent);
+ } catch (Throwable t) {
+ Log.w(LOG_TAG, "voice internal parse failed: " + t.getMessage());
+ return;
+ }
+ if (cmd == null) {
+ VoiceCommandLogger.dropped("voice internal parse returned null", "");
+ return;
+ }
+
+ if (service.isActivated()) {
+ service.handle(cmd);
+ return;
+ }
+
+ // 主进程未激活:cold-launch MainActivity,4 个 OEM extras 全部透传。
+ // 入队职责由 MainActivity.goToActivity.enqueuePendingVoiceCommand 单点承担。
+ Intent launcher = new Intent();
+ launcher.setClassName(ctx, MAIN_ACTIVITY_CLASS);
+ launcher.setPackage(ctx.getPackageName());
+ launcher.putExtra(VoiceExtras.EXTRA_VOICE_PENDING, true);
+ launcher.putExtra(VoiceExtras.EXTRA_OEM_FILM_NAME, cmd.getPayloadValue("filmName"));
+ launcher.putExtra(VoiceExtras.EXTRA_OEM_AUTOPLAY, cmd.getPayloadValue("autoPlay"));
+ launcher.putExtra(VoiceExtras.EXTRA_OEM_REQUEST_ID, cmd.getRequestId());
+ launcher.putExtra(VoiceExtras.EXTRA_OEM_SOURCE_APP, cmd.getSourceApp());
+ launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ try {
+ ctx.startActivity(launcher);
+ } catch (Throwable t) {
+ Log.w(LOG_TAG, "voice internal cold launch failed: " + t.getMessage());
+ }
+ }
+}
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/router/BridgeRegistry.java b/app/src/main/java/com/pukka/jiecheng/voice/router/BridgeRegistry.java
new file mode 100644
index 0000000..d40798f
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/router/BridgeRegistry.java
@@ -0,0 +1,26 @@
+package com.pukka.jiecheng.voice.router;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+public final class BridgeRegistry {
+
+ private static final AtomicReference ACTIVE = new AtomicReference<>();
+
+ private BridgeRegistry() {}
+
+ public static void register(PlaybackControlBridge bridge) {
+ ACTIVE.set(bridge);
+ }
+
+ public static void unregister(PlaybackControlBridge bridge) {
+ ACTIVE.compareAndSet(bridge, null);
+ }
+
+ public static PlaybackControlBridge get() {
+ return ACTIVE.get();
+ }
+
+ public static void clear() {
+ ACTIVE.set(null);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/router/CommandRouter.java b/app/src/main/java/com/pukka/jiecheng/voice/router/CommandRouter.java
new file mode 100644
index 0000000..288454d
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/router/CommandRouter.java
@@ -0,0 +1,143 @@
+package com.pukka.jiecheng.voice.router;
+
+import com.pukka.jiecheng.voice.guard.AppStateGuard;
+import com.pukka.jiecheng.voice.protocol.VoiceCallbackResult;
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.protocol.VoiceCommandType;
+
+public class CommandRouter {
+
+ private final PageNavigator navigator;
+ private final PlaybackControlBridge bridge;
+ private final AppStateGuard guard;
+
+ public CommandRouter(PageNavigator navigator, PlaybackControlBridge bridge, AppStateGuard guard) {
+ this.navigator = navigator;
+ this.bridge = bridge;
+ this.guard = guard;
+ }
+
+ public RouteResult route(VoiceCommand cmd) {
+ if (cmd == null) {
+ return RouteResult.failure("", VoiceCallbackResult.CODE_INVALID_PAYLOAD, "command is null");
+ }
+ String requestId = cmd.getRequestId() == null ? "" : cmd.getRequestId();
+
+ if (guard != null && !guard.isActivated()) {
+ return RouteResult.failure(requestId,
+ VoiceCallbackResult.CODE_DEVICE_NOT_ACTIVATED,
+ "device not activated");
+ }
+
+ VoiceCommandType type = cmd.getCommandType();
+ if (type == null) {
+ return RouteResult.failure(requestId,
+ VoiceCallbackResult.CODE_INVALID_PAYLOAD, "commandType is null");
+ }
+
+ boolean needExitPlayback = isNavigation(type) && navigator != null && navigator.isPlaying();
+
+ switch (type) {
+ case OPEN_FILM_DETAIL:
+ if (navigator == null) {
+ return RouteResult.failure(requestId, VoiceCallbackResult.CODE_INVALID_PAYLOAD,
+ "navigator is null");
+ }
+ if (needExitPlayback && bridge != null) bridge.exitPlayback();
+ navigator.openFilmDetail(cmd);
+ return RouteResult.ok(requestId);
+
+ case OPEN_CATEGORY:
+ if (navigator == null) {
+ return RouteResult.failure(requestId, VoiceCallbackResult.CODE_INVALID_PAYLOAD,
+ "navigator is null");
+ }
+ if (needExitPlayback && bridge != null) bridge.exitPlayback();
+ navigator.openCategory(cmd);
+ return RouteResult.ok(requestId);
+
+ case OPEN_SEARCH:
+ case SEARCH_FILM:
+ if (navigator == null) {
+ return RouteResult.failure(requestId, VoiceCallbackResult.CODE_INVALID_PAYLOAD,
+ "navigator is null");
+ }
+ if (needExitPlayback && bridge != null) bridge.exitPlayback();
+ navigator.openSearch(cmd);
+ return RouteResult.ok(requestId);
+
+ case GO_HOME:
+ if (navigator == null) {
+ return RouteResult.failure(requestId, VoiceCallbackResult.CODE_INVALID_PAYLOAD,
+ "navigator is null");
+ }
+ if (needExitPlayback && bridge != null) bridge.exitPlayback();
+ navigator.goHome(cmd);
+ return RouteResult.ok(requestId);
+
+ case PLAYBACK_RESUME:
+ case PLAYBACK_PAUSE:
+ case PLAYBACK_ENTER_FULLSCREEN:
+ case PLAYBACK_EXIT_FULLSCREEN:
+ case PLAYBACK_SEEK_FORWARD:
+ case PLAYBACK_SEEK_BACKWARD:
+ case PLAYBACK_SEEK_TO:
+ case PLAYBACK_PREVIOUS:
+ case PLAYBACK_NEXT:
+ case PLAYBACK_SELECT_INDEX:
+ case PLAYBACK_PAGE_NEXT:
+ case PLAYBACK_PAGE_PREV:
+ case PLAYBACK_GOTO_PAGE:
+ if (bridge == null || !bridge.isAvailable()) {
+ return RouteResult.failure(requestId,
+ VoiceCallbackResult.CODE_NOT_IN_PLAYER,
+ "no active playback bridge");
+ }
+ bridge.dispatch(cmd);
+ return RouteResult.ok(requestId);
+
+ case OPEN_ACTOR:
+ case OPEN_DIRECTOR:
+ default:
+ return RouteResult.failure(requestId,
+ VoiceCallbackResult.CODE_INVALID_PAYLOAD,
+ "unsupported commandType: " + type);
+ }
+ }
+
+ private static boolean isNavigation(VoiceCommandType type) {
+ switch (type) {
+ case OPEN_FILM_DETAIL:
+ case OPEN_CATEGORY:
+ case OPEN_ACTOR:
+ case OPEN_DIRECTOR:
+ case OPEN_SEARCH:
+ case SEARCH_FILM:
+ case GO_HOME:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public static final class RouteResult {
+ private final VoiceCallbackResult result;
+
+ private RouteResult(VoiceCallbackResult result) {
+ this.result = result;
+ }
+
+ public static RouteResult ok(String requestId) {
+ return new RouteResult(VoiceCallbackResult.success(requestId,
+ VoiceCallbackResult.CODE_OK, "ok"));
+ }
+
+ public static RouteResult failure(String requestId, String code, String message) {
+ return new RouteResult(VoiceCallbackResult.failure(requestId, code, message));
+ }
+
+ public VoiceCallbackResult getResult() { return result; }
+
+ public boolean isSuccess() { return result != null && result.isSuccess(); }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/router/DefaultPageNavigator.java b/app/src/main/java/com/pukka/jiecheng/voice/router/DefaultPageNavigator.java
new file mode 100644
index 0000000..972b88b
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/router/DefaultPageNavigator.java
@@ -0,0 +1,165 @@
+package com.pukka.jiecheng.voice.router;
+
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+
+import com.migu.bean.SearchResultBean;
+import com.migu.callback.AijiaCallback;
+import com.migu.httputils.HomeHttpUtils;
+import com.pukka.jiecheng.index.IndexActivity;
+import com.pukka.jiecheng.list.ListActivity;
+import com.pukka.jiecheng.search.SearchActivity;
+import com.pukka.jiecheng.utils.ActivityMonitor;
+import com.pukka.jiecheng.video.VideoDetailActivity;
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+
+import java.util.List;
+
+public class DefaultPageNavigator implements PageNavigator {
+
+ public static final String EXTRA_CATEGORY_ID = "category_id";
+ public static final String EXTRA_CATEGORY_NAME = "category_name";
+ public static final String EXTRA_SEARCH_KEYWORD = "search_keyword";
+ public static final String EXTRA_AUTO_SEARCH = "auto_search";
+
+ private final Context appContext;
+
+ public DefaultPageNavigator(Context appContext) {
+ this.appContext = appContext;
+ }
+
+ @Override
+ public void openFilmDetail(VoiceCommand cmd) {
+ String filmId = cmd.getPayloadValue("filmId");
+ String filmName = cmd.getPayloadValue("filmName");
+ boolean autoPlay = "true".equalsIgnoreCase(cmd.getPayloadValue("autoPlay"));
+
+ exitPlaybackIfNeeded();
+
+ // 语音场景下第三方可能只给出影片名甚至把名字塞进 filmId,此时若透传给
+ // VideoDetailActivity,详情页会用 /content/getcontentlist.do 按 contentcode 查找,
+ // 把"镖人"当 code 命中不到。改为先调 HomeHttpUtils.getVideoResult(/search/getsearchlist.do)
+ // 拿到真实 contentcode 再开详情;搜索无果时打开 SearchActivity 让用户从结果里挑选,
+ // 不再退化到 /content/getcontentlist.do 走错接口。
+ String keyword = !TextUtils.isEmpty(filmName) ? filmName : filmId;
+ if (TextUtils.isEmpty(keyword)) {
+ return;
+ }
+ searchAndOpenFilmDetail(keyword, autoPlay);
+ }
+
+ /**
+ * 按影片名调用 /search/getsearchlist.do 检索,取第一条结果的 contentcode 作为详情页 contentId;
+ * 命中为空或网络失败时打开 SearchActivity(自动搜索)兜底,全程不再走 /content/getcontentlist.do。
+ */
+ private void searchAndOpenFilmDetail(final String keyword, final boolean autoPlay) {
+ HomeHttpUtils.getVideoResult(keyword, "", 1, new AijiaCallback.VideoResultCallback() {
+ @Override
+ public void onResponse(SearchResultBean response) {
+ List list =
+ response == null ? null : response.getResult();
+ if (list != null && !list.isEmpty()) {
+ SearchResultBean.ResultBean first = list.get(0);
+ String contentId = first.getContentcode();
+ if (!TextUtils.isEmpty(contentId)) {
+ String resolvedName = TextUtils.isEmpty(first.getName())
+ ? keyword : first.getName();
+ startVideoDetail(contentId, resolvedName, autoPlay);
+ return;
+ }
+ }
+ // 搜索无结果或第一条 contentcode 为空:打开 SearchActivity 让用户从列表挑选
+ openSearchActivity(keyword);
+ }
+
+ @Override
+ public void onFail(String value) {
+ // 搜索失败:打开 SearchActivity 兜底,不再走错误的详情接口
+ openSearchActivity(keyword);
+ }
+ });
+ }
+
+ private void startVideoDetail(String filmId, String filmName, boolean autoPlay) {
+ if (autoPlay) {
+ // 语音自动播片:正常进入详情页,数据加载完成后模拟点击全屏按钮
+ // (走 fullVideo() 完整链路,替代原 startFull 的全屏启动模式)
+ VideoDetailActivity.startAutoFullScreen(appContext, filmId, "",
+ VideoDetailActivity.ModuleSourceType.SEARCH_PAGE);
+ } else {
+ VideoDetailActivity.start(appContext, filmId, "");
+ }
+ }
+
+ private void openSearchActivity(String keyword) {
+ Intent intent = new Intent(appContext, SearchActivity.class);
+ intent.putExtra(EXTRA_SEARCH_KEYWORD, keyword == null ? "" : keyword);
+ intent.putExtra(EXTRA_AUTO_SEARCH, true);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ appContext.startActivity(intent);
+ }
+
+ @Override
+ public void openCategory(VoiceCommand cmd) {
+ String categoryId = cmd.getPayloadValue("categoryId");
+ String categoryName = cmd.getPayloadValue("categoryName");
+
+ exitPlaybackIfNeeded();
+
+ Intent intent = new Intent(appContext, ListActivity.class);
+ intent.putExtra(EXTRA_CATEGORY_ID, categoryId == null ? "" : categoryId);
+ intent.putExtra(EXTRA_CATEGORY_NAME, categoryName == null ? "" : categoryName);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ appContext.startActivity(intent);
+ }
+
+ @Override
+ public void openSearch(VoiceCommand cmd) {
+ String keyword = firstNonEmpty(
+ cmd.getPayloadValue("filmName"),
+ cmd.getPayloadValue("actorName"),
+ cmd.getPayloadValue("directorName"),
+ cmd.getPayloadValue("category"));
+
+ exitPlaybackIfNeeded();
+
+ Intent intent = new Intent(appContext, SearchActivity.class);
+ intent.putExtra(EXTRA_SEARCH_KEYWORD, keyword == null ? "" : keyword);
+ intent.putExtra(EXTRA_AUTO_SEARCH, true);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ appContext.startActivity(intent);
+ }
+
+ @Override
+ public void goHome(VoiceCommand cmd) {
+ exitPlaybackIfNeeded();
+ Intent intent = new Intent(appContext, IndexActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ appContext.startActivity(intent);
+ }
+
+ @Override
+ public boolean isPlaying() {
+ return ActivityMonitor.getInstance() != null
+ && ActivityMonitor.getInstance().getLastAct() instanceof VideoDetailActivity;
+ }
+
+ @Override
+ public void exitPlaybackIfNeeded() {
+ if (isPlaying()) {
+ PlaybackControlBridge bridge = BridgeRegistry.get();
+ if (bridge != null) {
+ bridge.exitPlayback();
+ }
+ }
+ }
+
+ private static String firstNonEmpty(String... values) {
+ if (values == null) return null;
+ for (String v : values) {
+ if (!TextUtils.isEmpty(v)) return v;
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/router/PageNavigator.java b/app/src/main/java/com/pukka/jiecheng/voice/router/PageNavigator.java
new file mode 100644
index 0000000..5039758
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/router/PageNavigator.java
@@ -0,0 +1,18 @@
+package com.pukka.jiecheng.voice.router;
+
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+
+public interface PageNavigator {
+
+ void openFilmDetail(VoiceCommand cmd);
+
+ void openCategory(VoiceCommand cmd);
+
+ void openSearch(VoiceCommand cmd);
+
+ void goHome(VoiceCommand cmd);
+
+ boolean isPlaying();
+
+ void exitPlaybackIfNeeded();
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/router/PlaybackControlBridge.java b/app/src/main/java/com/pukka/jiecheng/voice/router/PlaybackControlBridge.java
new file mode 100644
index 0000000..18d3486
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/router/PlaybackControlBridge.java
@@ -0,0 +1,12 @@
+package com.pukka.jiecheng.voice.router;
+
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+
+public interface PlaybackControlBridge {
+
+ boolean isAvailable();
+
+ void dispatch(VoiceCommand cmd);
+
+ void exitPlayback();
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandCoordinator.java b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandCoordinator.java
new file mode 100644
index 0000000..18c885e
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandCoordinator.java
@@ -0,0 +1,90 @@
+package com.pukka.jiecheng.voice.service;
+
+import android.content.Context;
+
+import com.pukka.jiecheng.voice.guard.AppStateGuard;
+import com.pukka.jiecheng.voice.guard.DefaultAppStateGuard;
+import com.pukka.jiecheng.voice.parser.SkyworthVodParser;
+import com.pukka.jiecheng.voice.parser.VendorAParser;
+import com.pukka.jiecheng.voice.parser.VendorBParser;
+import com.pukka.jiecheng.voice.parser.VoicePlatformParser;
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.queue.CommandQueue;
+import com.pukka.jiecheng.voice.router.BridgeRegistry;
+import com.pukka.jiecheng.voice.router.CommandRouter;
+import com.pukka.jiecheng.voice.router.DefaultPageNavigator;
+import com.pukka.jiecheng.voice.router.PageNavigator;
+import com.pukka.jiecheng.voice.router.PlaybackControlBridge;
+import com.pukka.jiecheng.voice.throttle.CommandThrottle;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class VoiceCommandCoordinator {
+
+ private final Context appContext;
+ private final VoicePlatformParser parserA;
+ private final VoicePlatformParser parserB;
+ private final VoicePlatformParser parserSkyworth;
+ private final AppStateGuard guard;
+ private final PageNavigator navigator;
+ private final CommandRouter router;
+ private final CommandQueue coldStartQueue;
+ private final CommandThrottle throttle;
+
+ private final Map parsersByPlatform = new HashMap<>();
+
+ public VoiceCommandCoordinator(Context appContext) {
+ this(appContext, new DefaultPageNavigator(appContext), new DefaultAppStateGuard(appContext));
+ }
+
+ public VoiceCommandCoordinator(Context appContext, PageNavigator navigator, AppStateGuard guard) {
+ this.appContext = appContext;
+ this.navigator = navigator;
+ this.guard = guard;
+ this.parserA = new VendorAParser();
+ this.parserB = new VendorBParser();
+ this.parserSkyworth = new SkyworthVodParser();
+ parsersByPlatform.put("vendorA", parserA);
+ parsersByPlatform.put("vendorB", parserB);
+ parsersByPlatform.put("skyworth", parserSkyworth);
+
+ PlaybackControlBridge bridge = new RegistryBackedBridge();
+ this.router = new CommandRouter(navigator, bridge, guard);
+ this.coldStartQueue = new CommandQueue();
+ this.throttle = new CommandThrottle();
+ }
+
+ public VoicePlatformParser resolveParser(String platform) {
+ if (platform == null) return parserA;
+ VoicePlatformParser p = parsersByPlatform.get(platform);
+ return p != null ? p : parserA;
+ }
+
+ public VoiceCommand parse(String commandType, String sourcePlatform, String payloadJson) {
+ return resolveParser(sourcePlatform).parse(commandType, payloadJson);
+ }
+
+ public CommandRouter getRouter() { return router; }
+ public CommandQueue getColdStartQueue() { return coldStartQueue; }
+ public CommandThrottle getThrottle() { return throttle; }
+ public AppStateGuard getGuard() { return guard; }
+ public PageNavigator getNavigator() { return navigator; }
+
+ public List drainColdStart() {
+ return coldStartQueue.drainAll();
+ }
+
+ private static class RegistryBackedBridge implements PlaybackControlBridge {
+ @Override public boolean isAvailable() { return BridgeRegistry.get() != null; }
+ @Override public void dispatch(VoiceCommand cmd) {
+ PlaybackControlBridge active = BridgeRegistry.get();
+ if (active != null) active.dispatch(cmd);
+ }
+ @Override public void exitPlayback() {
+ PlaybackControlBridge active = BridgeRegistry.get();
+ if (active != null) active.exitPlayback();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandService.java b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandService.java
new file mode 100644
index 0000000..cddf954
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandService.java
@@ -0,0 +1,117 @@
+package com.pukka.jiecheng.voice.service;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.pukka.jiecheng.voice.log.VoiceCommandLogger;
+import com.pukka.jiecheng.voice.protocol.VoiceCallbackResult;
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.protocol.VoiceProtocolConstants;
+import com.pukka.jiecheng.voice.router.CommandRouter;
+import com.pukka.jiecheng.voice.throttle.CommandThrottle;
+
+import java.util.List;
+
+public class VoiceCommandService {
+
+ private static final String TAG = VoiceProtocolConstants.LOG_TAG;
+
+ private static volatile VoiceCommandService instance;
+
+ private final Context appContext;
+ private final VoiceCommandCoordinator coordinator;
+ private final Handler mainHandler;
+
+ private VoiceCommandService(Context appContext) {
+ this.appContext = appContext.getApplicationContext();
+ this.coordinator = new VoiceCommandCoordinator(this.appContext);
+ this.mainHandler = new Handler(Looper.getMainLooper());
+ }
+
+ public static VoiceCommandService getInstance(Context appContext) {
+ if (instance == null) {
+ synchronized (VoiceCommandService.class) {
+ if (instance == null) {
+ instance = new VoiceCommandService(appContext);
+ }
+ }
+ }
+ return instance;
+ }
+
+ public VoiceCommandCoordinator getCoordinator() { return coordinator; }
+
+ public void handle(VoiceCommand cmd) {
+ if (cmd == null) return;
+ long start = System.currentTimeMillis();
+
+ CommandThrottle throttle = coordinator.getThrottle();
+ VoiceCommand merged = throttle.accept(cmd);
+
+ CommandRouter router = coordinator.getRouter();
+ CommandRouter.RouteResult result = router.route(merged);
+
+ VoiceCommandLogger.executing(merged);
+ VoiceCommandLogger.finished(merged, result.getResult(),
+ System.currentTimeMillis() - start, currentPage());
+
+ sendCallback(merged, result.getResult());
+ }
+
+ public void enqueueForColdStart(VoiceCommand cmd) {
+ if (cmd == null) return;
+ coordinator.getColdStartQueue().enqueue(cmd);
+ VoiceCommandLogger.enqueued(cmd.getRequestId());
+ }
+
+ public void replayColdStartQueue() {
+ List drained = coordinator.drainColdStart();
+ for (VoiceCommand cmd : drained) {
+ VoiceCommandLogger.replaying(cmd.getRequestId());
+ handle(cmd);
+ }
+ }
+
+ public boolean isActivated() {
+ return coordinator.getGuard().isActivated();
+ }
+
+ private void sendCallback(VoiceCommand cmd, VoiceCallbackResult result) {
+ if (cmd == null || result == null) return;
+ String callbackAction = cmd.getCallbackAction();
+ if (TextUtils.isEmpty(callbackAction)) {
+ Log.i(TAG, "[callback-skip] no callbackAction for requestId=" + cmd.getRequestId());
+ return;
+ }
+ Intent intent = new Intent(callbackAction);
+ intent.putExtra("requestId", cmd.getRequestId());
+ intent.putExtra("success", result.isSuccess());
+ intent.putExtra("resultCode", result.getResultCode());
+ intent.putExtra("resultMessage", result.getResultMessage());
+ intent.putExtra("data", result.getData());
+ intent.putExtra("payloadJson", result.toJsonString());
+ intent.setPackage(VoiceProtocolConstants.APP_PACKAGE);
+ try {
+ appContext.sendBroadcast(intent);
+ } catch (Throwable t) {
+ Log.w(TAG, "sendCallback failed: " + t.getMessage());
+ }
+ }
+
+ private String currentPage() {
+ try {
+ Class> clz = Class.forName("com.pukka.jiecheng.utils.ActivityMonitor");
+ Object monitor = clz.getField("mAppStatusManager").get(null);
+ if (monitor == null) return "";
+ Object activity = clz.getMethod("getLastAct").invoke(monitor);
+ if (activity == null) return "";
+ return ((android.app.Activity) activity).getClass().getSimpleName();
+ } catch (Throwable t) {
+ return "";
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceKeepAliveService.java b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceKeepAliveService.java
new file mode 100644
index 0000000..7980dc3
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceKeepAliveService.java
@@ -0,0 +1,103 @@
+package com.pukka.jiecheng.voice.service;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+
+import com.pukka.jiecheng.R;
+import com.pukka.jiecheng.voice.adapter.SkyworthVoiceAdapter;
+import com.pukka.jiecheng.voice.window.VoiceOverlayHelper;
+
+/**
+ * :voice 进程前台保活服务。
+ *
+ * 持有 OEM 创维语音广播接收器({@link SkyworthVoiceAdapter}),进程独立于主进程,
+ * 主进程被 LMK 或 {@code appExit} 杀掉后仍然存活,确保语音接收链路不中断。
+ *
+ *
进程职责:仅做"翻译 + 转发",所有业务执行都跑在主进程。
+ * 详见 {@code docs/2026-08-28-语音冷启动链路设计方案.md}。
+ *
+ *
生命周期:
+ *
+ * - {@code onCreate} → 建静音通知 channel → {@code startForeground}
+ * - {@code onStartCommand} → 注册 OEM 接收器;返回 {@code START_STICKY} 保证系统杀后回拉
+ * - {@code onDestroy} → 反注册接收器
+ *
+ */
+public class VoiceKeepAliveService extends Service {
+
+ private static final String LOG_TAG = "VoiceCommand";
+ private static final int NOTIF_ID = 0x1001;
+ private static final String CHANNEL_ID = "voice_keepalive";
+
+ private SkyworthVoiceAdapter adapter;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ createNotificationChannel();
+ Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setContentTitle(getString(R.string.voice_keepalive_title))
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .build();
+ Log.i(LOG_TAG, "voice keepalive foreground service started");
+ startForeground(NOTIF_ID, notification);
+ // 让 :voice 进程始终持有 visible window,便于 SkyworthVoiceAdapter 收到广播
+ // 后直接 ctx.startActivity 走 Background Activity Start 的 visible-window 豁免。
+ // VoiceOverlayHelper 内部幂等:权限缺失 / OEM 拦截 → 沉默失败回退到通知兜底。
+ VoiceOverlayHelper.showOverlay(this);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (adapter == null) {
+ adapter = new SkyworthVoiceAdapter();
+ IntentFilter filter = new IntentFilter(SkyworthVoiceAdapter.ACTION_VOD);
+ registerReceiver(adapter, filter);
+ }
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (adapter != null) {
+ try {
+ unregisterReceiver(adapter);
+ } catch (Throwable t) {
+ Log.w(LOG_TAG, "voice keepalive receiver unregister failed: " + t.getMessage());
+ }
+ adapter = null;
+ }
+ // 服务停止时清理悬浮窗,避免泄漏 1px View 占住系统 WindowManager 槽位
+ VoiceOverlayHelper.hideOverlay();
+ super.onDestroy();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ getString(R.string.voice_keepalive_channel),
+ NotificationManager.IMPORTANCE_MIN);
+ channel.setShowBadge(false);
+ NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ if (nm != null) {
+ nm.createNotificationChannel(channel);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottle.java b/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottle.java
new file mode 100644
index 0000000..f78b328
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottle.java
@@ -0,0 +1,82 @@
+package com.pukka.jiecheng.voice.throttle;
+
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.protocol.VoiceCommandType;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class CommandThrottle {
+
+ public static final long DEFAULT_WINDOW_MS = 2000L;
+
+ private final long windowMs;
+ private final CommandThrottleClock clock;
+
+ private final Map latestByType = new HashMap<>();
+
+ public CommandThrottle() {
+ this(DEFAULT_WINDOW_MS, new SystemClock());
+ }
+
+ public CommandThrottle(long windowMs) {
+ this(windowMs, new SystemClock());
+ }
+
+ public CommandThrottle(long windowMs, CommandThrottleClock clock) {
+ this.windowMs = windowMs;
+ this.clock = clock;
+ }
+
+ public VoiceCommand accept(VoiceCommand cmd) {
+ if (cmd == null) return null;
+ latestByType.put(cmd.getCommandType(), new Entry(cmd, clock.now()));
+ return cmd;
+ }
+
+ public VoiceCommand drain() {
+ if (latestByType.isEmpty()) return null;
+ Entry latest = null;
+ VoiceCommandType latestType = null;
+ for (Map.Entry e : latestByType.entrySet()) {
+ if (latest == null || e.getValue().acceptedAt > latest.acceptedAt) {
+ latest = e.getValue();
+ latestType = e.getKey();
+ }
+ }
+ if (latestType != null) latestByType.remove(latestType);
+ return latest == null ? null : latest.cmd;
+ }
+
+ public List drainAll() {
+ if (latestByType.isEmpty()) return new ArrayList<>();
+ List entries = new ArrayList<>(latestByType.values());
+ entries.sort((a, b) -> Long.compare(b.acceptedAt, a.acceptedAt));
+ latestByType.clear();
+ List result = new ArrayList<>(entries.size());
+ for (Entry e : entries) result.add(e.cmd);
+ return result;
+ }
+
+ public static boolean isSameThrottleKey(VoiceCommandType a, VoiceCommandType b) {
+ return a == b;
+ }
+
+ private static final class Entry {
+ final VoiceCommand cmd;
+ final long acceptedAt;
+
+ Entry(VoiceCommand cmd, long acceptedAt) {
+ this.cmd = cmd;
+ this.acceptedAt = acceptedAt;
+ }
+ }
+
+ private static final class SystemClock implements CommandThrottleClock {
+ @Override public long now() {
+ return System.currentTimeMillis();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottleClock.java b/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottleClock.java
new file mode 100644
index 0000000..c60f17d
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottleClock.java
@@ -0,0 +1,5 @@
+package com.pukka.jiecheng.voice.throttle;
+
+public interface CommandThrottleClock {
+ long now();
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/voice/window/VoiceOverlayHelper.java b/app/src/main/java/com/pukka/jiecheng/voice/window/VoiceOverlayHelper.java
new file mode 100644
index 0000000..2891f11
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/voice/window/VoiceOverlayHelper.java
@@ -0,0 +1,141 @@
+package com.pukka.jiecheng.voice.window;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManager;
+
+/**
+ * 语音冷启动用 1px 悬浮窗助手。
+ *
+ * 目的:在 :voice 进程挂一个 {@code TYPE_APPLICATION_OVERLAY} 1×1 透明 View,
+ * 让 AMS 判定该进程持有 visible window。Android 12+ 的 Background Activity Start
+ * 限制对"calling app has a visible window"显式豁免({@code ActivityTaskManager}
+ * 的 {@code isCallingUidHasAnyVisibleWindow} 判定),后续
+ * {@link android.content.Context#startActivity(android.content.Intent)} 不再被
+ * Abort。冷启动 Activity 完成后调用 {@link #hideOverlay()} 移除即可。
+ *
+ *
设计原则:
+ *
+ * - 幂等:多次 {@link #showOverlay(Context)} 只 addView 一次
+ * - 失败短路:首次 addView 失败(如 SYSTEM_ALERT_WINDOW 未授予/OEM 拦截)
+ * 缓存状态,后续不再重试,避免重复 Toast / 日志刷屏
+ * - 不抢焦点:FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE | FLAG_LAYOUT_NO_LIMITS
+ * - 零副作用:1×1 像素 + 透明 + 屏幕外(gravity=-1,-1),肉眼无感
+ *
+ *
+ * 权限前置条件:{@code Settings.canDrawOverlays(ctx)}。该权限是标准权限,
+ * 不需要运行时申请;部分 OEM ROM 自动授予,未授予时该 helper 走沉默失败路径,
+ * 上层走 FullScreenIntent 通知兜底。
+ */
+public final class VoiceOverlayHelper {
+
+ private static final String LOG_TAG = "VoiceCommand";
+
+ private static volatile boolean overlayShown = false;
+ private static volatile boolean overlayDisabled = false;
+
+ private static WindowManager wm;
+ private static View overlayView;
+ private static WindowManager.LayoutParams overlayLp;
+
+ private VoiceOverlayHelper() {}
+
+ /**
+ * 挂 1px 悬浮窗。幂等;已挂则直接返回 true。
+ *
+ * @return true 表示当前 visible window 由本 helper 提供,可用于上层判定走
+ * startActivity 优先路径;false 表示 SYSTEM_ALERT_WINDOW 未授权 /
+ * OEM 拦截,应回退到通知 FullScreenIntent 路径。
+ */
+ public static synchronized boolean showOverlay(Context ctx) {
+ if (overlayShown) return true;
+ if (overlayDisabled) return false;
+ Context appCtx = ctx.getApplicationContext();
+ if (appCtx == null) {
+ overlayDisabled = true;
+ return false;
+ }
+ if (!Settings.canDrawOverlays(appCtx)) {
+ Log.w(LOG_TAG, "voice overlay: SYSTEM_ALERT_WINDOW not granted, fallback to notify");
+ overlayDisabled = true;
+ return false;
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // TYPE_APPLICATION_OVERLAY 是 Android 6+ 才有的类型;低版本设备此 helper 无效
+ overlayDisabled = true;
+ return false;
+ }
+ try {
+ wm = (WindowManager) appCtx.getSystemService(Context.WINDOW_SERVICE);
+ if (wm == null) {
+ overlayDisabled = true;
+ return false;
+ }
+ overlayLp = new WindowManager.LayoutParams(
+ 1, 1,
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+ | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ PixelFormat.TRANSLUCENT);
+ overlayLp.gravity = Gravity.TOP | Gravity.START;
+ overlayLp.x = -1;
+ overlayLp.y = -1;
+
+ overlayView = new View(appCtx);
+ overlayView.setBackgroundColor(Color.TRANSPARENT);
+
+ wm.addView(overlayView, overlayLp);
+ overlayShown = true;
+ Log.i(LOG_TAG, "voice overlay shown (1px TYPE_APPLICATION_OVERLAY)");
+ return true;
+ } catch (Throwable t) {
+ Log.w(LOG_TAG, "voice overlay addView failed: " + t.getMessage());
+ overlayShown = false;
+ overlayView = null;
+ overlayLp = null;
+ wm = null;
+ // SecurityException / WindowManager.BadTokenException 等都不要重试
+ overlayDisabled = true;
+ return false;
+ }
+ }
+
+ /**
+ * 移除悬浮窗。幂等;未挂 / 已失败过都安全返回。
+ */
+ public static synchronized void hideOverlay() {
+ if (!overlayShown || wm == null || overlayView == null) return;
+ try {
+ wm.removeView(overlayView);
+ } catch (Throwable t) {
+ // IllegalArgumentException 等:可能已被系统移除,吞掉即可
+ Log.w(LOG_TAG, "voice overlay removeView failed: " + t.getMessage());
+ }
+ overlayShown = false;
+ overlayView = null;
+ overlayLp = null;
+ wm = null;
+ }
+
+ /**
+ * 供调试 / 上层判定。
+ */
+ public static boolean isShown() {
+ return overlayShown;
+ }
+
+ /**
+ * 供上层判定 helper 是否因权限 / OEM 拦截被永久禁用。
+ */
+ public static boolean isDisabled() {
+ return overlayDisabled;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/pukka/jiecheng/web/SimpleWebActivity.java b/app/src/main/java/com/pukka/jiecheng/web/SimpleWebActivity.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/ActivationCodeInputView.kt b/app/src/main/java/com/pukka/jiecheng/widget/ActivationCodeInputView.kt
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/ActivationFocusRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/widget/ActivationFocusRecyclerView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/ActivationKeyItem.kt b/app/src/main/java/com/pukka/jiecheng/widget/ActivationKeyItem.kt
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/ActivationKeypadAdapter.kt b/app/src/main/java/com/pukka/jiecheng/widget/ActivationKeypadAdapter.kt
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/CornerBadgeView.java b/app/src/main/java/com/pukka/jiecheng/widget/CornerBadgeView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/CustomerCodeView.java b/app/src/main/java/com/pukka/jiecheng/widget/CustomerCodeView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/D3ContentLinearLayoutManager.java b/app/src/main/java/com/pukka/jiecheng/widget/D3ContentLinearLayoutManager.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/D3NavLinerLayoutManagerTv.java b/app/src/main/java/com/pukka/jiecheng/widget/D3NavLinerLayoutManagerTv.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/D3RecyclerView.java b/app/src/main/java/com/pukka/jiecheng/widget/D3RecyclerView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/DeviceNotActivatedView.kt b/app/src/main/java/com/pukka/jiecheng/widget/DeviceNotActivatedView.kt
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/DeviceUnregisteredView.java b/app/src/main/java/com/pukka/jiecheng/widget/DeviceUnregisteredView.java
deleted file mode 100644
index bd8a087..0000000
--- a/app/src/main/java/com/pukka/jiecheng/widget/DeviceUnregisteredView.java
+++ /dev/null
@@ -1,117 +0,0 @@
-package com.pukka.jiecheng.widget;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.widget.Button;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import androidx.annotation.Nullable;
-
-import com.pukka.jiecheng.R;
-
-public class DeviceUnregisteredView extends FrameLayout {
-
- private static final String TAG = "DeviceUnregisteredView";
-
- private static final int ERROR_CODE_ACTIVATE_FAILED = 10500;
- private static final int ERROR_CODE_DEVICE_UNBOUND = 10600;
- private static final int ERROR_CODE_MEMBER_EXPIRED = 10700;
- private static final int ERROR_CODE_DEVICE_OCCUPIED = 10800;
-
- private static final String DEFAULT_MESSAGE = "设备未注册,请联系第三方开通服务";
-
- private TextView mMessageView;
- private Button mExitButton;
-
- @Nullable
- private OnExitListener mOnExitListener;
-
- public interface OnExitListener {
- void onExit();
- }
-
- public DeviceUnregisteredView(Context context) {
- this(context, null);
- }
-
- public DeviceUnregisteredView(Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public DeviceUnregisteredView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- initView(context);
- }
-
- private void initView(Context context) {
- LayoutInflater.from(context).inflate(R.layout.view_device_unregistered, this, true);
- mMessageView = findViewById(R.id.id_tv_unregistered_message);
- mExitButton = findViewById(R.id.id_btn_exit);
-
- mExitButton.setFocusable(true);
- mExitButton.setFocusableInTouchMode(true);
- mExitButton.setClickable(true);
- mExitButton.setOnClickListener(v -> {
- if (mOnExitListener != null) {
- mOnExitListener.onExit();
- }
- });
-
- setVisibility(GONE);
- }
-
- /**
- * 根据 checkBenefits 返回的状态码设置阻断页提示文案。
- *
- * @param errorCode 状态码
- * @param message 自定义提示文案,仅在状态码未命中预设映射时使用
- */
- public void setStatusInfo(int errorCode, @Nullable String message) {
- String displayMessage;
- switch (errorCode) {
- case ERROR_CODE_ACTIVATE_FAILED:
- displayMessage = "激活失败,请联系第三方重新开通";
- break;
- case ERROR_CODE_DEVICE_UNBOUND:
- displayMessage = "设备未绑定,请联系第三方开通服务";
- break;
- case ERROR_CODE_MEMBER_EXPIRED:
- displayMessage = "会员已过期,请联系第三方续费";
- break;
- case ERROR_CODE_DEVICE_OCCUPIED:
- displayMessage = "设备已被占用,请联系客服";
- break;
- default:
- displayMessage = !TextUtils.isEmpty(message) ? message : DEFAULT_MESSAGE;
- break;
- }
- mMessageView.setText(displayMessage);
- }
-
- public void setOnExitListener(@Nullable OnExitListener listener) {
- mOnExitListener = listener;
- }
-
- @Override
- public void setVisibility(int visibility) {
- super.setVisibility(visibility);
- if (visibility == VISIBLE) {
- mExitButton.requestFocus();
- }
- }
-
- @Override
- public boolean dispatchKeyEvent(KeyEvent event) {
- if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
- if (event.getAction() == KeyEvent.ACTION_UP && mOnExitListener != null) {
- mOnExitListener.onExit();
- }
- return true;
- }
- return super.dispatchKeyEvent(event);
- }
-}
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/FocusLinearLayout.java b/app/src/main/java/com/pukka/jiecheng/widget/FocusLinearLayout.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/FocusRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/widget/FocusRecyclerView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/FocusTextView.java b/app/src/main/java/com/pukka/jiecheng/widget/FocusTextView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/HomeNavLinearLayoutManagerTv.java b/app/src/main/java/com/pukka/jiecheng/widget/HomeNavLinearLayoutManagerTv.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/HorizontalGridViewPk.java b/app/src/main/java/com/pukka/jiecheng/widget/HorizontalGridViewPk.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/LinearSpacingItemDecoration.java b/app/src/main/java/com/pukka/jiecheng/widget/LinearSpacingItemDecoration.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/LocationCodeInputView.kt b/app/src/main/java/com/pukka/jiecheng/widget/LocationCodeInputView.kt
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/MarqueeTextView.java b/app/src/main/java/com/pukka/jiecheng/widget/MarqueeTextView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/MgRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/widget/MgRecyclerView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/QrCodeView.java b/app/src/main/java/com/pukka/jiecheng/widget/QrCodeView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/SettingItemView.kt b/app/src/main/java/com/pukka/jiecheng/widget/SettingItemView.kt
new file mode 100644
index 0000000..4ca2a78
--- /dev/null
+++ b/app/src/main/java/com/pukka/jiecheng/widget/SettingItemView.kt
@@ -0,0 +1,78 @@
+package com.pukka.jiecheng.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.appcompat.widget.SwitchCompat
+import com.pukka.jiecheng.R
+import com.pukka.jiecheng.setting.SettingItem
+
+/**
+ * 设置页单个 item 的容器视图。
+ *
+ * 封装"标题 + 副标题 + SwitchCompat"的标准结构,外部通过 [bind] 绑定数据。
+ * 外部通过 [setOnCheckedChangeListener] 监听开关变化。
+ *
+ * 为什么不用 item layout 里的 id 直接 findViewById:
+ * RecyclerView 通过 ViewHolder 复用 itemView,多个 Holder 共享同一个 LayoutInflater
+ * 出来的 View 实例,外层 Activity / Adapter 直接持有这些 view 的引用会在复用时被覆盖;
+ * 封装成自定义 View 后,对外只暴露 [bind] / [setOnCheckedChangeListener],
+ * 每个 VH 拿到的是独立的 SettingItemView 实例,状态归属明确。
+ */
+class SettingItemView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+ private val tvTitle: TextView
+ private val tvSubtitle: TextView
+ private val switch: SwitchCompat
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.item_setting_switch_inner, this, true)
+ tvTitle = findViewById(R.id.id_tv_setting_title)
+ tvSubtitle = findViewById(R.id.id_tv_setting_subtitle)
+ switch = findViewById(R.id.id_switch_setting)
+ }
+
+ /**
+ * 暴露内部 SwitchCompat,便于外部在 RecyclerView 首次 bind 完成后主动请求焦点
+ * (遥控器支持:进入页面焦点直接落到开关,OK 键立即可切换)。
+ */
+ fun getSwitch(): SwitchCompat = switch
+
+ fun bind(item: SettingItem) {
+ tvTitle.text = item.title
+ if (item.subtitle.isNullOrEmpty()) {
+ tvSubtitle.visibility = GONE
+ } else {
+ tvSubtitle.visibility = VISIBLE
+ tvSubtitle.text = item.subtitle
+ }
+ // 关闭 listener 再 setChecked,避免 bind 阶段触发 onCheckedChange
+ switch.setOnCheckedChangeListener(null)
+ switch.isChecked = item.checked
+ switch.isEnabled = item.enabled
+ // 不在 bind() 里 requestFocus:RecyclerView 复用同一个 itemView 反复 bind,
+ // 在每个 item 上都 request 会让焦点频繁跳到最新可见的第一项,反而干扰遥控器上下键。
+ // 焦点交给外层 Activity 在 onCreate / onResume 时主动下发一次即可。
+ }
+
+ /**
+ * 把 SwitchCompat 的开关变化透传给外层 Adapter。
+ * 使用 lambda 形式避免直接引用 CompoundButton.OnCheckedChangeListener
+ * 这个跨包 Java 接口在 Kotlin 中 SAM 转换偶尔出问题。
+ */
+ fun setOnCheckedChangeListener(listener: (Boolean) -> Unit) {
+ switch.setOnCheckedChangeListener { _, isChecked -> listener.invoke(isChecked) }
+ }
+
+ /** 兼容从 XML 直接配置标题的便捷方法 */
+ fun setTitleText(@StringRes resId: Int) {
+ tvTitle.setText(resId)
+ }
+}
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/SpaceItemDecoration.java b/app/src/main/java/com/pukka/jiecheng/widget/SpaceItemDecoration.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/SquareTextView.java b/app/src/main/java/com/pukka/jiecheng/widget/SquareTextView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/TopView.java b/app/src/main/java/com/pukka/jiecheng/widget/TopView.java
old mode 100755
new mode 100644
index 7f388ac..0438ddf
--- a/app/src/main/java/com/pukka/jiecheng/widget/TopView.java
+++ b/app/src/main/java/com/pukka/jiecheng/widget/TopView.java
@@ -28,8 +28,8 @@ import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.migu.bean.GetTopButtonBean;
+import com.pukka.jiecheng.BuildConfig;
import com.pukka.jiecheng.R;
-import com.pukka.jiecheng.config.AppConfiguration;
import com.pukka.jiecheng.utils.MgRoute;
import com.pukka.jiecheng.utils.TopStyleUtils;
import com.pukka.libcore.boxShared.BoxSharedParamsUtils;
@@ -160,10 +160,7 @@ public class TopView extends ConstraintLayout implements View.OnClickListener, V
loadImgPre(focusHistoryImg,ContextUtils.getContext());
break;
case 5:
- if (AppConfiguration.getInstance().isThirdPartyVariant()) {
- break;
- }
- if(BoxSharedParamsUtils.isJcHotel()){
+ if(!BuildConfig.IS_HOME_EDITION){
break;
}
mIvOrder.setVisibility(View.VISIBLE);
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/TvXRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/widget/TvXRecyclerView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/UserAgreementView.java b/app/src/main/java/com/pukka/jiecheng/widget/UserAgreementView.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/VerticalRecyclerViewEx.java b/app/src/main/java/com/pukka/jiecheng/widget/VerticalRecyclerViewEx.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/VideoContainer.java b/app/src/main/java/com/pukka/jiecheng/widget/VideoContainer.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/Banner.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/Banner.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerConfig.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerConfig.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerScroller.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerScroller.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerViewPager.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerViewPager.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/OnBannerListener.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/OnBannerListener.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/WeakHandler.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/WeakHandler.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/BaseImageLoader.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/BaseImageLoader.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/FrescoImageLoader.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/FrescoImageLoader.java
old mode 100755
new mode 100644
diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/ImageLoaderInterface.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/ImageLoaderInterface.java
old mode 100755
new mode 100644
diff --git a/app/src/main/res/anim/activation_code_shake.xml b/app/src/main/res/anim/activation_code_shake.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/anim/rotate_anim.xml b/app/src/main/res/anim/rotate_anim.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/color/activation_action_text_selector.xml b/app/src/main/res/color/activation_action_text_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/color/activation_key_text_selector.xml b/app/src/main/res/color/activation_key_text_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/color/activation_view_text_selector.xml b/app/src/main/res/color/activation_view_text_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_about_submit.png b/app/src/main/res/drawable-hdpi/bg_about_submit.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_about_submit_focus.png b/app/src/main/res/drawable-hdpi/bg_about_submit_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_about_yhxy.png b/app/src/main/res/drawable-hdpi/bg_about_yhxy.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_about_yhxy_focus.png b/app/src/main/res/drawable-hdpi/bg_about_yhxy_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_about_ysxy.png b/app/src/main/res/drawable-hdpi/bg_about_ysxy.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_about_ysxy_focus.png b/app/src/main/res/drawable-hdpi/bg_about_ysxy_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_back_search.png b/app/src/main/res/drawable-hdpi/bg_back_search.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_back_search_focus.png b/app/src/main/res/drawable-hdpi/bg_back_search_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_back_top.png b/app/src/main/res/drawable-hdpi/bg_back_top.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_back_top_focus.png b/app/src/main/res/drawable-hdpi/bg_back_top_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_check_vr_btn_focus.png b/app/src/main/res/drawable-hdpi/bg_check_vr_btn_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_check_vr_btn_nor.png b/app/src/main/res/drawable-hdpi/bg_check_vr_btn_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_hotel_about.png b/app/src/main/res/drawable-hdpi/bg_hotel_about.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_hotel_deviceinfo.png b/app/src/main/res/drawable-hdpi/bg_hotel_deviceinfo.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_hotel_setting.png b/app/src/main/res/drawable-hdpi/bg_hotel_setting.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_hotel_update.png b/app/src/main/res/drawable-hdpi/bg_hotel_update.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_hotel_user.png b/app/src/main/res/drawable-hdpi/bg_hotel_user.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_short_mark.png b/app/src/main/res/drawable-hdpi/bg_short_mark.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_short_playing_check.png b/app/src/main/res/drawable-hdpi/bg_short_playing_check.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_short_vod.png b/app/src/main/res/drawable-hdpi/bg_short_vod.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_short_vod_focus.png b/app/src/main/res/drawable-hdpi/bg_short_vod_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_user_equit.png b/app/src/main/res/drawable-hdpi/bg_user_equit.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_useragreement.png b/app/src/main/res/drawable-hdpi/bg_useragreement.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_useragreement_submit.png b/app/src/main/res/drawable-hdpi/bg_useragreement_submit.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_useragreement_submit_focus.png b/app/src/main/res/drawable-hdpi/bg_useragreement_submit_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_useragreenmen_quit_focus.png b/app/src/main/res/drawable-hdpi/bg_useragreenmen_quit_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/bg_useragreenment_quit_unfocus.png b/app/src/main/res/drawable-hdpi/bg_useragreenment_quit_unfocus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/btn_1_foc.png b/app/src/main/res/drawable-hdpi/btn_1_foc.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/btn_1_nor.png b/app/src/main/res/drawable-hdpi/btn_1_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/btn_device_unactivit_focus.png b/app/src/main/res/drawable-hdpi/btn_device_unactivit_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/btn_focus.png b/app/src/main/res/drawable-hdpi/btn_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/btn_nor.png b/app/src/main/res/drawable-hdpi/btn_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/btn_progress.png b/app/src/main/res/drawable-hdpi/btn_progress.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/btn_short_playing_submit.png b/app/src/main/res/drawable-hdpi/btn_short_playing_submit.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/device_bg.png b/app/src/main/res/drawable-hdpi/device_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/device_check_update.png b/app/src/main/res/drawable-hdpi/device_check_update.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/device_check_update_focus.png b/app/src/main/res/drawable-hdpi/device_check_update_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/device_info_bg.png b/app/src/main/res/drawable-hdpi/device_info_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/gamepad.png b/app/src/main/res/drawable-hdpi/gamepad.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/help_bg.png b/app/src/main/res/drawable-hdpi/help_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/home_btn_down_foc.png b/app/src/main/res/drawable-hdpi/home_btn_down_foc.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/home_btn_down_nor.png b/app/src/main/res/drawable-hdpi/home_btn_down_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/ic_check_vr_no.png b/app/src/main/res/drawable-hdpi/ic_check_vr_no.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/ic_check_vr_yes.png b/app/src/main/res/drawable-hdpi/ic_check_vr_yes.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/ic_full_vr_tip.png b/app/src/main/res/drawable-hdpi/ic_full_vr_tip.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/ic_short_playing.gif b/app/src/main/res/drawable-hdpi/ic_short_playing.gif
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/ic_weixin.png b/app/src/main/res/drawable-hdpi/ic_weixin.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/ic_zhifubao.png b/app/src/main/res/drawable-hdpi/ic_zhifubao.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/icon_detail_back_normal.png b/app/src/main/res/drawable-hdpi/icon_detail_back_normal.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/icon_error_prompt.png b/app/src/main/res/drawable-hdpi/icon_error_prompt.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/index_bg.png b/app/src/main/res/drawable-hdpi/index_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/index_my_user_bg_focus.png b/app/src/main/res/drawable-hdpi/index_my_user_bg_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/jst_logo_right.webp b/app/src/main/res/drawable-hdpi/jst_logo_right.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/keyboard_clear.png b/app/src/main/res/drawable-hdpi/keyboard_clear.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/keyboard_delete.png b/app/src/main/res/drawable-hdpi/keyboard_delete.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/list_bg.png b/app/src/main/res/drawable-hdpi/list_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/list_bg_tag_focus.png b/app/src/main/res/drawable-hdpi/list_bg_tag_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/list_bg_tag_unfocus.png b/app/src/main/res/drawable-hdpi/list_bg_tag_unfocus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/list_search_ic.png b/app/src/main/res/drawable-hdpi/list_search_ic.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/my_order_bg.png b/app/src/main/res/drawable-hdpi/my_order_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/my_order_jilu_bg.webp b/app/src/main/res/drawable-hdpi/my_order_jilu_bg.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/my_order_page_select.png b/app/src/main/res/drawable-hdpi/my_order_page_select.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/my_order_page_unselect.png b/app/src/main/res/drawable-hdpi/my_order_page_unselect.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/order_bg1.webp b/app/src/main/res/drawable-hdpi/order_bg1.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/order_bg_qr_code.png b/app/src/main/res/drawable-hdpi/order_bg_qr_code.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/order_btn_focus_bg.png b/app/src/main/res/drawable-hdpi/order_btn_focus_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/order_btn_focused_bg.png b/app/src/main/res/drawable-hdpi/order_btn_focused_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/order_btn_unfocus_bg.png b/app/src/main/res/drawable-hdpi/order_btn_unfocus_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/order_center_keyboard_btn_focus.png b/app/src/main/res/drawable-hdpi/order_center_keyboard_btn_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/order_center_keyboard_btn_unfocus.png b/app/src/main/res/drawable-hdpi/order_center_keyboard_btn_unfocus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/order_center_vip_text_bg.png b/app/src/main/res/drawable-hdpi/order_center_vip_text_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/order_pre_tip.png b/app/src/main/res/drawable-hdpi/order_pre_tip.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/ordercenter_item_focus_bg.png b/app/src/main/res/drawable-hdpi/ordercenter_item_focus_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/ordercenter_item_unfoces_bg.png b/app/src/main/res/drawable-hdpi/ordercenter_item_unfoces_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/progress.png b/app/src/main/res/drawable-hdpi/progress.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/progress_loading.png b/app/src/main/res/drawable-hdpi/progress_loading.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/short_ok_game_tip.png b/app/src/main/res/drawable-hdpi/short_ok_game_tip.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/short_ok_tip.png b/app/src/main/res/drawable-hdpi/short_ok_tip.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/short_up_game_tip.png b/app/src/main/res/drawable-hdpi/short_up_game_tip.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/short_up_tip.png b/app/src/main/res/drawable-hdpi/short_up_tip.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/try_see_pay_tip.webp b/app/src/main/res/drawable-hdpi/try_see_pay_tip.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/update_dialog_bg.png b/app/src/main/res/drawable-hdpi/update_dialog_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/weilai_btn_error_focus.png b/app/src/main/res/drawable-hdpi/weilai_btn_error_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/weilai_btn_error_nofocus.png b/app/src/main/res/drawable-hdpi/weilai_btn_error_nofocus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-hdpi/weilai_error_bg.webp b/app/src/main/res/drawable-hdpi/weilai_error_bg.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xhdpi/progress_loading.png b/app/src/main/res/drawable-xhdpi/progress_loading.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading00.png b/app/src/main/res/drawable-xxhdpi/loading00.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading000.png b/app/src/main/res/drawable-xxhdpi/loading000.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading01.png b/app/src/main/res/drawable-xxhdpi/loading01.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading02.png b/app/src/main/res/drawable-xxhdpi/loading02.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading03.png b/app/src/main/res/drawable-xxhdpi/loading03.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading04.png b/app/src/main/res/drawable-xxhdpi/loading04.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading05.png b/app/src/main/res/drawable-xxhdpi/loading05.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading06.png b/app/src/main/res/drawable-xxhdpi/loading06.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading07.png b/app/src/main/res/drawable-xxhdpi/loading07.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading08.png b/app/src/main/res/drawable-xxhdpi/loading08.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading09.png b/app/src/main/res/drawable-xxhdpi/loading09.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading10.png b/app/src/main/res/drawable-xxhdpi/loading10.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading11.png b/app/src/main/res/drawable-xxhdpi/loading11.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading12.png b/app/src/main/res/drawable-xxhdpi/loading12.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading13.png b/app/src/main/res/drawable-xxhdpi/loading13.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading14.png b/app/src/main/res/drawable-xxhdpi/loading14.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading15.png b/app/src/main/res/drawable-xxhdpi/loading15.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading16.png b/app/src/main/res/drawable-xxhdpi/loading16.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading17.png b/app/src/main/res/drawable-xxhdpi/loading17.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading18.png b/app/src/main/res/drawable-xxhdpi/loading18.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading19.png b/app/src/main/res/drawable-xxhdpi/loading19.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading20.png b/app/src/main/res/drawable-xxhdpi/loading20.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading21.png b/app/src/main/res/drawable-xxhdpi/loading21.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/loading22.png b/app/src/main/res/drawable-xxhdpi/loading22.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/progress_loading.png b/app/src/main/res/drawable-xxhdpi/progress_loading.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxhdpi/switch_keyboard_bg.png b/app/src/main/res/drawable-xxhdpi/switch_keyboard_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxxhdpi/progress_loading.png b/app/src/main/res/drawable-xxxhdpi/progress_loading.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable-xxxhdpi/switch_keyboard_bg.png b/app/src/main/res/drawable-xxxhdpi/switch_keyboard_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/background_search.xml b/app/src/main/res/drawable/background_search.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/background_splash.webp b/app/src/main/res/drawable/background_splash.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_2.webp b/app/src/main/res/drawable/bg_2.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_about_submit_selector.xml b/app/src/main/res/drawable/bg_about_submit_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_about_yhxy_selector.xml b/app/src/main/res/drawable/bg_about_yhxy_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_about_ysxy_selector.xml b/app/src/main/res/drawable/bg_about_ysxy_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_activation_action_selector.xml b/app/src/main/res/drawable/bg_activation_action_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_activation_code_box.xml b/app/src/main/res/drawable/bg_activation_code_box.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_activation_device_panel.xml b/app/src/main/res/drawable/bg_activation_device_panel.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_activation_gradient.xml b/app/src/main/res/drawable/bg_activation_gradient.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_activation_input_preview.xml b/app/src/main/res/drawable/bg_activation_input_preview.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_activation_key_selector.xml b/app/src/main/res/drawable/bg_activation_key_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_activation_loading_panel.xml b/app/src/main/res/drawable/bg_activation_loading_panel.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_avatar_white_border.xml b/app/src/main/res/drawable/bg_avatar_white_border.xml
new file mode 100644
index 0000000..e7e3427
--- /dev/null
+++ b/app/src/main/res/drawable/bg_avatar_white_border.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_bottom_round_corner.xml b/app/src/main/res/drawable/bg_bottom_round_corner.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_bottom_white_round_corner.xml b/app/src/main/res/drawable/bg_bottom_white_round_corner.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_btn_confirm.xml b/app/src/main/res/drawable/bg_btn_confirm.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_btn_err_prompt.xml b/app/src/main/res/drawable/bg_btn_err_prompt.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_btn_retention_prompt.xml b/app/src/main/res/drawable/bg_btn_retention_prompt.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_btn_trysee_open_focus.xml b/app/src/main/res/drawable/bg_btn_trysee_open_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_btn_useragreenment_focus.xml b/app/src/main/res/drawable/bg_btn_useragreenment_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_btn_useragreenment_selector.xml b/app/src/main/res/drawable/bg_btn_useragreenment_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_btn_useragreenment_unfocus.xml b/app/src/main/res/drawable/bg_btn_useragreenment_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_check_8k_selector.xml b/app/src/main/res/drawable/bg_check_8k_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_check_vr_selector.xml b/app/src/main/res/drawable/bg_check_vr_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_content_c7_focus.xml b/app/src/main/res/drawable/bg_content_c7_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_content_c7_selector.xml b/app/src/main/res/drawable/bg_content_c7_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_content_focus.xml b/app/src/main/res/drawable/bg_content_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_content_selector.xml b/app/src/main/res/drawable/bg_content_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_content_t_focus.xml b/app/src/main/res/drawable/bg_content_t_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_content_t_selector.xml b/app/src/main/res/drawable/bg_content_t_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_content_unfocus.xml b/app/src/main/res/drawable/bg_content_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_deman_table_page_selector.xml b/app/src/main/res/drawable/bg_deman_table_page_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_demand_table_page_selector.xml b/app/src/main/res/drawable/bg_demand_table_page_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_demandbg_table_page_selector.xml b/app/src/main/res/drawable/bg_demandbg_table_page_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_back_selector.xml b/app/src/main/res/drawable/bg_detail_back_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_broadcast_label.xml b/app/src/main/res/drawable/bg_detail_broadcast_label.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_btn_order_selector.xml b/app/src/main/res/drawable/bg_detail_btn_order_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_btn_selector.xml b/app/src/main/res/drawable/bg_detail_btn_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_btn_video_selector.xml b/app/src/main/res/drawable/bg_detail_btn_video_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_btn_vip_selector.xml b/app/src/main/res/drawable/bg_detail_btn_vip_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_def_selector.xml b/app/src/main/res/drawable/bg_detail_def_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_episodes_mask.xml b/app/src/main/res/drawable/bg_detail_episodes_mask.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_episodes_pp_line.xml b/app/src/main/res/drawable/bg_detail_episodes_pp_line.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_more_selector.xml b/app/src/main/res/drawable/bg_detail_more_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_program_table_item_selector.xml b/app/src/main/res/drawable/bg_detail_program_table_item_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_table_page_selector.xml b/app/src/main/res/drawable/bg_detail_table_page_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_try_see_order.xml b/app/src/main/res/drawable/bg_detail_try_see_order.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_try_see_order2.xml b/app/src/main/res/drawable/bg_detail_try_see_order2.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_try_see_order_bottom.xml b/app/src/main/res/drawable/bg_detail_try_see_order_bottom.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_video_selector.xml b/app/src/main/res/drawable/bg_detail_video_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_video_selector2.xml b/app/src/main/res/drawable/bg_detail_video_selector2.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_detail_vip_label.xml b/app/src/main/res/drawable/bg_detail_vip_label.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_device_activate_btn.xml b/app/src/main/res/drawable/bg_device_activate_btn.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_device_activate_input.xml b/app/src/main/res/drawable/bg_device_activate_input.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_device_activate_panel.xml b/app/src/main/res/drawable/bg_device_activate_panel.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_device_check_selector.xml b/app/src/main/res/drawable/bg_device_check_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_dialog_activation.xml b/app/src/main/res/drawable/bg_dialog_activation.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_filter_table_page_selector.xml b/app/src/main/res/drawable/bg_filter_table_page_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_full_vr_player_tip.xml b/app/src/main/res/drawable/bg_full_vr_player_tip.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_full_window_bottom_mask.xml b/app/src/main/res/drawable/bg_full_window_bottom_mask.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_full_window_top_mask.xml b/app/src/main/res/drawable/bg_full_window_top_mask.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_fulldialogclose.png b/app/src/main/res/drawable/bg_fulldialogclose.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_gd.webp b/app/src/main/res/drawable/bg_gd.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_gx_no_circle.png b/app/src/main/res/drawable/bg_gx_no_circle.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_his_selector.xml b/app/src/main/res/drawable/bg_his_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_his_unfocus.xml b/app/src/main/res/drawable/bg_his_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_history_clear_selector.xml b/app/src/main/res/drawable/bg_history_clear_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_history_clear_unfocus.xml b/app/src/main/res/drawable/bg_history_clear_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_history_selector.xml b/app/src/main/res/drawable/bg_history_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_history_selector1.xml b/app/src/main/res/drawable/bg_history_selector1.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_history_selector2.xml b/app/src/main/res/drawable/bg_history_selector2.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_history_selector3.xml b/app/src/main/res/drawable/bg_history_selector3.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_index_sub_nav.xml b/app/src/main/res/drawable/bg_index_sub_nav.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_item_match_focus.xml b/app/src/main/res/drawable/bg_item_match_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_list_tag_selector.xml b/app/src/main/res/drawable/bg_list_tag_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_login.webp b/app/src/main/res/drawable/bg_login.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_login_bt.webp b/app/src/main/res/drawable/bg_login_bt.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_login_nber_no.xml b/app/src/main/res/drawable/bg_login_nber_no.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_login_nber_w.xml b/app/src/main/res/drawable/bg_login_nber_w.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_login_top.png b/app/src/main/res/drawable/bg_login_top.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_login_top_selector.xml b/app/src/main/res/drawable/bg_login_top_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_main_agree_selector.xml b/app/src/main/res/drawable/bg_main_agree_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_match_item_selector.xml b/app/src/main/res/drawable/bg_match_item_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_media_focus.xml b/app/src/main/res/drawable/bg_media_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_media_list_selector.xml b/app/src/main/res/drawable/bg_media_list_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_media_selector.xml b/app/src/main/res/drawable/bg_media_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_media_unfocus.xml b/app/src/main/res/drawable/bg_media_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_order_btn_selector.xml b/app/src/main/res/drawable/bg_order_btn_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_order_center_selector.xml b/app/src/main/res/drawable/bg_order_center_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_order_focus.xml b/app/src/main/res/drawable/bg_order_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_order_focus_down.xml b/app/src/main/res/drawable/bg_order_focus_down.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_order_unfocus.xml b/app/src/main/res/drawable/bg_order_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_order_xufei_selector.xml b/app/src/main/res/drawable/bg_order_xufei_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_ordergame_selector.xml b/app/src/main/res/drawable/bg_ordergame_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_orderlist.xml b/app/src/main/res/drawable/bg_orderlist.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_qr_code_card.xml b/app/src/main/res/drawable/bg_qr_code_card.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_qr_code_dialog_card.xml b/app/src/main/res/drawable/bg_qr_code_dialog_card.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_qr_code_image_rounded.xml b/app/src/main/res/drawable/bg_qr_code_image_rounded.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_search_back_btn_selector.xml b/app/src/main/res/drawable/bg_search_back_btn_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_search_history_selector.xml b/app/src/main/res/drawable/bg_search_history_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_setting_item_selector.xml b/app/src/main/res/drawable/bg_setting_item_selector.xml
new file mode 100644
index 0000000..1c3c375
--- /dev/null
+++ b/app/src/main/res/drawable/bg_setting_item_selector.xml
@@ -0,0 +1,14 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
diff --git a/app/src/main/res/drawable/bg_shap1_selector.xml b/app/src/main/res/drawable/bg_shap1_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_shap1_unfocus.xml b/app/src/main/res/drawable/bg_shap1_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_shap_focus.xml b/app/src/main/res/drawable/bg_shap_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_shap_selector.xml b/app/src/main/res/drawable/bg_shap_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_shap_unfocus.xml b/app/src/main/res/drawable/bg_shap_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_shap_useragreenment.xml b/app/src/main/res/drawable/bg_shap_useragreenment.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_short_video_selector.xml b/app/src/main/res/drawable/bg_short_video_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_splash.webp b/app/src/main/res/drawable/bg_splash.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_suggest_item_selector.xml b/app/src/main/res/drawable/bg_suggest_item_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_trysee_open.xml b/app/src/main/res/drawable/bg_trysee_open.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_update_tv_selector.xml b/app/src/main/res/drawable/bg_update_tv_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_useragreen_selector.xml b/app/src/main/res/drawable/bg_useragreen_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_video_btn.xml b/app/src/main/res/drawable/bg_video_btn.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_video_key_prompt_tips.xml b/app/src/main/res/drawable/bg_video_key_prompt_tips.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bg_vr_check_bottom_mask.xml b/app/src/main/res/drawable/bg_vr_check_bottom_mask.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bgc_content_focus.xml b/app/src/main/res/drawable/bgc_content_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/bgc_content_selector.xml b/app/src/main/res/drawable/bgc_content_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/btn_common_top_selector.xml b/app/src/main/res/drawable/btn_common_top_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/btn_home_logout_selector.xml b/app/src/main/res/drawable/btn_home_logout_selector.xml
new file mode 100644
index 0000000..3c88a72
--- /dev/null
+++ b/app/src/main/res/drawable/btn_home_logout_selector.xml
@@ -0,0 +1,18 @@
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/btn_my_back_top_selector.xml b/app/src/main/res/drawable/btn_my_back_top_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/btn_my_order_page_selector.xml b/app/src/main/res/drawable/btn_my_order_page_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/btn_my_search_selector.xml b/app/src/main/res/drawable/btn_my_search_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/btn_my_user_selector.xml b/app/src/main/res/drawable/btn_my_user_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/btn_orderlist.xml b/app/src/main/res/drawable/btn_orderlist.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/btn_to_top_selector.xml b/app/src/main/res/drawable/btn_to_top_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/btn_user_exit_selector.xml b/app/src/main/res/drawable/btn_user_exit_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/code_bg.webp b/app/src/main/res/drawable/code_bg.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/common_matchtext_bg_focus.xml b/app/src/main/res/drawable/common_matchtext_bg_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/common_matchtext_bg_unfocus.xml b/app/src/main/res/drawable/common_matchtext_bg_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/common_matchtextlive_bg_unfocus.xml b/app/src/main/res/drawable/common_matchtextlive_bg_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/common_notice_bg.xml b/app/src/main/res/drawable/common_notice_bg.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/common_set_btn_bg_focus.xml b/app/src/main/res/drawable/common_set_btn_bg_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/common_text_bg_cir_focus.xml b/app/src/main/res/drawable/common_text_bg_cir_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/common_text_bg_focus.xml b/app/src/main/res/drawable/common_text_bg_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/common_text_bg_round_focus.xml b/app/src/main/res/drawable/common_text_bg_round_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/common_text_color_selector.xml b/app/src/main/res/drawable/common_text_color_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/common_top_btn_bg_focus.xml b/app/src/main/res/drawable/common_top_btn_bg_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/cursor_color.xml b/app/src/main/res/drawable/cursor_color.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/detail_program_page_divider.xml b/app/src/main/res/drawable/detail_program_page_divider.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/dialog_order_bg.png b/app/src/main/res/drawable/dialog_order_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/downapkbg.png b/app/src/main/res/drawable/downapkbg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/filter_mask_bg.xml b/app/src/main/res/drawable/filter_mask_bg.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/gameorder.webp b/app/src/main/res/drawable/gameorder.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/hisroty_bg.webp b/app/src/main/res/drawable/hisroty_bg.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/history_del_focus.webp b/app/src/main/res/drawable/history_del_focus.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/history_del_nor.png b/app/src/main/res/drawable/history_del_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/history_delete.png b/app/src/main/res/drawable/history_delete.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/history_no_data.png b/app/src/main/res/drawable/history_no_data.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/history_text_bg_selector.xml b/app/src/main/res/drawable/history_text_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/history_top_bg_selector.xml b/app/src/main/res/drawable/history_top_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/hotcontent_item_selector.xml b/app/src/main/res/drawable/hotcontent_item_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/ic_detail_item_playing.gif b/app/src/main/res/drawable/ic_detail_item_playing.gif
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/icon_common_top_search_selector.xml b/app/src/main/res/drawable/icon_common_top_search_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/icon_team_logo_default.xml b/app/src/main/res/drawable/icon_team_logo_default.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/icon_to_top_selector.xml b/app/src/main/res/drawable/icon_to_top_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/icon_video_play.png b/app/src/main/res/drawable/icon_video_play.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/icon_vip.png b/app/src/main/res/drawable/icon_vip.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/index_my_history_mask_bg.xml b/app/src/main/res/drawable/index_my_history_mask_bg.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/index_my_seekbar_style.xml b/app/src/main/res/drawable/index_my_seekbar_style.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/index_my_user_bg.xml b/app/src/main/res/drawable/index_my_user_bg.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/index_nav_item_bg_selector.xml b/app/src/main/res/drawable/index_nav_item_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/index_set_item_bg_selector.xml b/app/src/main/res/drawable/index_set_item_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/indicator_demand_tab.xml b/app/src/main/res/drawable/indicator_demand_tab.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/indicator_detail_tab.xml b/app/src/main/res/drawable/indicator_detail_tab.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/keyboard_item_selector.xml b/app/src/main/res/drawable/keyboard_item_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/loading_player_animation.xml b/app/src/main/res/drawable/loading_player_animation.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/loadingicon.png b/app/src/main/res/drawable/loadingicon.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_bg.webp b/app/src/main/res/drawable/login_bg.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_cancle_bg.png b/app/src/main/res/drawable/login_cancle_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_code_bg.png b/app/src/main/res/drawable/login_code_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_code_bg_focus.png b/app/src/main/res/drawable/login_code_bg_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_code_bg_selector.xml b/app/src/main/res/drawable/login_code_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_codebtn_bg_selector.xml b/app/src/main/res/drawable/login_codebtn_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_codebtn_focus.png b/app/src/main/res/drawable/login_codebtn_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_codebtn_nor.png b/app/src/main/res/drawable/login_codebtn_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_getcode_btn_bg.webp b/app/src/main/res/drawable/login_getcode_btn_bg.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_icon_bg.webp b/app/src/main/res/drawable/login_icon_bg.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_item_bg_focus.png b/app/src/main/res/drawable/login_item_bg_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_item_bg_nor.png b/app/src/main/res/drawable/login_item_bg_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_line_1.webp b/app/src/main/res/drawable/login_line_1.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_phone_bg.png b/app/src/main/res/drawable/login_phone_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_phone_bg_focus.png b/app/src/main/res/drawable/login_phone_bg_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_phone_bg_selector.xml b/app/src/main/res/drawable/login_phone_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_show_bg.xml b/app/src/main/res/drawable/login_show_bg.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_tolin_btn_bg.png b/app/src/main/res/drawable/login_tolin_btn_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_tologin_bg_focus.png b/app/src/main/res/drawable/login_tologin_bg_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/login_tologin_bg_selector.xml b/app/src/main/res/drawable/login_tologin_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/logout_text_color_selector.xml b/app/src/main/res/drawable/logout_text_color_selector.xml
new file mode 100644
index 0000000..26806f9
--- /dev/null
+++ b/app/src/main/res/drawable/logout_text_color_selector.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/match_all_tv_selector.xml b/app/src/main/res/drawable/match_all_tv_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/match_list_all_selector.xml b/app/src/main/res/drawable/match_list_all_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/match_list_all_unfocus.xml b/app/src/main/res/drawable/match_list_all_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/match_list_bg.xml b/app/src/main/res/drawable/match_list_bg.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/match_list_focus.xml b/app/src/main/res/drawable/match_list_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/match_list_focus_sport.xml b/app/src/main/res/drawable/match_list_focus_sport.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/match_list_selector.xml b/app/src/main/res/drawable/match_list_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/match_list_selector_sport.xml b/app/src/main/res/drawable/match_list_selector_sport.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/matchlist_no_data_focus.xml b/app/src/main/res/drawable/matchlist_no_data_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/matchlist_no_data_selector.xml b/app/src/main/res/drawable/matchlist_no_data_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/mb.png b/app/src/main/res/drawable/mb.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/no_data_img.png b/app/src/main/res/drawable/no_data_img.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/order_bg.webp b/app/src/main/res/drawable/order_bg.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/order_center_keyboader_bg_focus.xml b/app/src/main/res/drawable/order_center_keyboader_bg_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/order_center_keyboard_bg_selector.xml b/app/src/main/res/drawable/order_center_keyboard_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/order_center_xufei_bg_focus.xml b/app/src/main/res/drawable/order_center_xufei_bg_focus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/order_center_xufei_bg_selector.xml b/app/src/main/res/drawable/order_center_xufei_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/order_center_xufei_bg_unfocus.xml b/app/src/main/res/drawable/order_center_xufei_bg_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/order_keyboard_item_selector.xml b/app/src/main/res/drawable/order_keyboard_item_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/ordercenter_item_bigfoces_bg.png b/app/src/main/res/drawable/ordercenter_item_bigfoces_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/progress_bar_custom.xml b/app/src/main/res/drawable/progress_bar_custom.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/progress_circle.xml b/app/src/main/res/drawable/progress_circle.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/progressbar_end.xml b/app/src/main/res/drawable/progressbar_end.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/progressbar_start.xml b/app/src/main/res/drawable/progressbar_start.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/progressbar_update_h.xml b/app/src/main/res/drawable/progressbar_update_h.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/progressbar_update_other.xml b/app/src/main/res/drawable/progressbar_update_other.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/rect_no.png b/app/src/main/res/drawable/rect_no.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/rect_ok.png b/app/src/main/res/drawable/rect_ok.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/saomakuang.png b/app/src/main/res/drawable/saomakuang.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/saosao.png b/app/src/main/res/drawable/saosao.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/search_keyboard_bg_selector.xml b/app/src/main/res/drawable/search_keyboard_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/selector_bg_activate_page_btn.xml b/app/src/main/res/drawable/selector_bg_activate_page_btn.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/selector_btn_update_now.xml b/app/src/main/res/drawable/selector_btn_update_now.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/selector_txt_update_now.xml b/app/src/main/res/drawable/selector_txt_update_now.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/switch_keyboard_bg_selector.xml b/app/src/main/res/drawable/switch_keyboard_bg_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/switch_keyboard_bg_unfocus.xml b/app/src/main/res/drawable/switch_keyboard_bg_unfocus.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/sxtitle.png b/app/src/main/res/drawable/sxtitle.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/tryseeoverbg.xml b/app/src/main/res/drawable/tryseeoverbg.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/update_tv_selector.xml b/app/src/main/res/drawable/update_tv_selector.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/video_bg_img.xml b/app/src/main/res/drawable/video_bg_img.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/weilai_btn_error.xml b/app/src/main/res/drawable/weilai_btn_error.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/drawable/weilai_btn_error_textcolor.xml b/app/src/main/res/drawable/weilai_btn_error_textcolor.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout-land/device_not_activated_view.xml b/app/src/main/res/layout-land/device_not_activated_view.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/activity_about_my.xml b/app/src/main/res/layout/activity_about_my.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/activity_device.xml b/app/src/main/res/layout/activity_device.xml
old mode 100755
new mode 100644
index dbda194..9811359
--- a/app/src/main/res/layout/activity_device.xml
+++ b/app/src/main/res/layout/activity_device.xml
@@ -203,15 +203,63 @@
android:textColor="@color/white6"
android:textSize="@dimen/mg_sp_24" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -80,6 +80,7 @@
android:layout_marginTop="@dimen/qb_px_125"
app:layout_constraintLeft_toRightOf="@id/id_cl_orderCenter"
android:focusable="true"
+ android:visibility="gone"
android:background="@drawable/order_btn_unfocus_bg"
app:layout_constraintTop_toTopOf="parent">
@@ -156,7 +157,9 @@
android:layout_width="match_parent"
android:layout_height="0dp"
android:focusable="false"
+ android:id="@+id/cl_center_zone"
app:layout_constraintBottom_toBottomOf="parent"
+ android:layout_marginBottom="@dimen/qb_px_95"
app:layout_constraintTop_toBottomOf="@id/id_cl_myOrder">
@@ -220,14 +222,23 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
- android:layout_marginBottom="@dimen/qb_px_76"
- android:layout_marginLeft="@dimen/qb_px_827"
+ android:layout_marginLeft="@dimen/qb_px_798"
app:layout_constraintLeft_toLeftOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- android:text="版权渠道:橘视厅 备案号:视听备【2025】A2580"
+ app:layout_constraintTop_toBottomOf="@+id/cl_center_zone"
+ android:text="版权渠道:橘视厅 备案号:"
android:textSize="@dimen/mg_sp_16"
android:textColor="@color/white30"
/>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_test.xml b/app/src/main/res/layout/activity_test.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/activity_thirdparty_entry.xml b/app/src/main/res/layout/activity_thirdparty_entry.xml
deleted file mode 100644
index ba96d3c..0000000
--- a/app/src/main/res/layout/activity_thirdparty_entry.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/activity_video_detail.xml b/app/src/main/res/layout/activity_video_detail.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/activity_wx_mp_login.xml b/app/src/main/res/layout/activity_wx_mp_login.xml
new file mode 100644
index 0000000..6fe4f4a
--- /dev/null
+++ b/app/src/main/res/layout/activity_wx_mp_login.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/banner.xml b/app/src/main/res/layout/banner.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/device_not_activated_view.xml b/app/src/main/res/layout/device_not_activated_view.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/dialog_activation_success.xml b/app/src/main/res/layout/dialog_activation_success.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/dialog_benefit_revoked.xml b/app/src/main/res/layout/dialog_benefit_revoked.xml
new file mode 100644
index 0000000..04e8290
--- /dev/null
+++ b/app/src/main/res/layout/dialog_benefit_revoked.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_home_logout_confirm.xml b/app/src/main/res/layout/dialog_home_logout_confirm.xml
new file mode 100644
index 0000000..e17dd6c
--- /dev/null
+++ b/app/src/main/res/layout/dialog_home_logout_confirm.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_qr_code.xml b/app/src/main/res/layout/dialog_qr_code.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/filter_header_layout.xml b/app/src/main/res/layout/filter_header_layout.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/fragment_content.xml b/app/src/main/res/layout/fragment_content.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/fragment_home_my.xml b/app/src/main/res/layout/fragment_home_my.xml
new file mode 100644
index 0000000..37216ec
--- /dev/null
+++ b/app/src/main/res/layout/fragment_home_my.xml
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_hotel_my.xml b/app/src/main/res/layout/fragment_hotel_my.xml
old mode 100755
new mode 100644
index da1efa9..6d29116
--- a/app/src/main/res/layout/fragment_hotel_my.xml
+++ b/app/src/main/res/layout/fragment_hotel_my.xml
@@ -76,81 +76,8 @@
app:layout_constraintTop_toTopOf="parent" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/app/src/main/res/layout/fragment_my.xml b/app/src/main/res/layout/fragment_my.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/fragment_order_center.xml b/app/src/main/res/layout/fragment_order_center.xml
old mode 100755
new mode 100644
index 7ddf8ef..0da5299
--- a/app/src/main/res/layout/fragment_order_center.xml
+++ b/app/src/main/res/layout/fragment_order_center.xml
@@ -8,16 +8,17 @@
tools:context=".order.fragment.OrderCenterFragment">
@@ -34,26 +35,29 @@
-
-
+ tools:visibility="visible">
+
+
@@ -110,27 +115,31 @@
android:text="支付宝"
android:textColor="@color/white30"
android:textSize="@dimen/mg_sp_16"
+ android:visibility="gone"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
-
diff --git a/app/src/main/res/layout/fragment_orderlist.xml b/app/src/main/res/layout/fragment_orderlist.xml
old mode 100755
new mode 100644
index 8334927..aad1244
--- a/app/src/main/res/layout/fragment_orderlist.xml
+++ b/app/src/main/res/layout/fragment_orderlist.xml
@@ -46,7 +46,7 @@
android:layout_height="wrap_content"
android:layout_weight="1.3"
android:gravity="center"
- android:text="支付时间"
+ android:text="订购时间"
android:textColor="#80ffffff"
android:textSize="@dimen/mg_sp_17" />
@@ -56,7 +56,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
- android:text="订购时长"
+ android:text="权益结束时间"
android:textColor="#80ffffff"
android:textSize="@dimen/mg_sp_17" />
diff --git a/app/src/main/res/layout/fragment_orderxufei.xml b/app/src/main/res/layout/fragment_orderxufei.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/fragment_recommend.xml b/app/src/main/res/layout/fragment_recommend.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/fragment_short_video.xml b/app/src/main/res/layout/fragment_short_video.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/include_hotel_my_buttons.xml b/app/src/main/res/layout/include_hotel_my_buttons.xml
new file mode 100644
index 0000000..92a4e9b
--- /dev/null
+++ b/app/src/main/res/layout/include_hotel_my_buttons.xml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/index_floor_player_error.xml b/app/src/main/res/layout/index_floor_player_error.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/index_floor_player_order.xml b/app/src/main/res/layout/index_floor_player_order.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/index_floor_player_progress.xml b/app/src/main/res/layout/index_floor_player_progress.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/index_floor_title_layout.xml b/app/src/main/res/layout/index_floor_title_layout.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/index_floor_totop.xml b/app/src/main/res/layout/index_floor_totop.xml
old mode 100755
new mode 100644
index 091a3b3..4a5eac8
--- a/app/src/main/res/layout/index_floor_totop.xml
+++ b/app/src/main/res/layout/index_floor_totop.xml
@@ -1,19 +1,45 @@
+
+
+
+
+
+
+
+
@@ -91,6 +93,7 @@
android:focusable="false"
android:textColor="@color/order_btn_text_color"
android:textSize="@dimen/mg_sp_21"
+ tools:text="3453453"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/id_tv_type_name"
app:layout_constraintTop_toTopOf="parent" />
@@ -102,6 +105,7 @@
android:layout_width="0dp"
android:layout_height="match_parent"
android:visibility="gone"
+ tools:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/id_divider"
@@ -118,6 +122,7 @@
android:textColor="@color/order_list_focus_text_color"
android:textSize="@dimen/mg_sp_21"
android:textStyle="bold"
+ android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@@ -125,7 +130,7 @@
android:id="@+id/id_tv_product_name2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginLeft="@dimen/qb_px_21"
+ android:layout_marginLeft="@dimen/qb_px_19"
android:layout_marginTop="@dimen/qb_px_18"
android:focusable="false"
android:maxLines="1"
@@ -147,7 +152,7 @@
android:textSize="@dimen/mg_sp_19"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/id_tv_type_name2" />
+ app:layout_constraintTop_toBottomOf="@id/id_tv_product_name2" />
diff --git a/app/src/main/res/layout/item_order_center_vip.xml b/app/src/main/res/layout/item_order_center_vip.xml
old mode 100755
new mode 100644
index dd140fc..df50904
--- a/app/src/main/res/layout/item_order_center_vip.xml
+++ b/app/src/main/res/layout/item_order_center_vip.xml
@@ -3,14 +3,14 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/id_ll_order_center_vip_item"
- android:layout_width="@dimen/qb_px_720"
+ android:layout_width="match_parent"
android:layout_height="@dimen/qb_px_108"
android:background="@drawable/ordercenter_item_unfoces_bg"
android:focusable="true">
+
+
diff --git a/app/src/main/res/layout/item_setting_switch_inner.xml b/app/src/main/res/layout/item_setting_switch_inner.xml
new file mode 100644
index 0000000..21f38d6
--- /dev/null
+++ b/app/src/main/res/layout/item_setting_switch_inner.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_short_vod.xml b/app/src/main/res/layout/item_short_vod.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/item_suggest.xml b/app/src/main/res/layout/item_suggest.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/item_t9keyboard.xml b/app/src/main/res/layout/item_t9keyboard.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_activation_loading_small.xml b/app/src/main/res/layout/layout_activation_loading_small.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_badge_view.xml b/app/src/main/res/layout/layout_badge_view.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_common_loading.xml b/app/src/main/res/layout/layout_common_loading.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_detail_info_popupwindow.xml b/app/src/main/res/layout/layout_detail_info_popupwindow.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_detail_ondemandepisodes_popupwindow.xml b/app/src/main/res/layout/layout_detail_ondemandepisodes_popupwindow.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_detail_video_full_black.xml b/app/src/main/res/layout/layout_detail_video_full_black.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_detail_video_full_loading.xml b/app/src/main/res/layout/layout_detail_video_full_loading.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_detail_video_status_err.xml b/app/src/main/res/layout/layout_detail_video_status_err.xml
old mode 100755
new mode 100644
index 2faf592..be27f5e
--- a/app/src/main/res/layout/layout_detail_video_status_err.xml
+++ b/app/src/main/res/layout/layout_detail_video_status_err.xml
@@ -46,26 +46,26 @@
android:id="@+id/id_tv_err_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginHorizontal="@dimen/qb_px_10"
android:layout_marginTop="@dimen/qb_px_11"
android:gravity="left"
android:text=""
android:textColor="@color/white7"
android:textSize="@dimen/mg_sp_16"
- app:layout_constraintRight_toRightOf="@+id/id_gl"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toLeftOf="@+id/id_tv_err_msg"
app:layout_constraintTop_toBottomOf="@+id/id_tv_text" />
diff --git a/app/src/main/res/layout/layout_detail_video_status_loading.xml b/app/src/main/res/layout/layout_detail_video_status_loading.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_detail_video_status_noorder_host.xml b/app/src/main/res/layout/layout_detail_video_status_noorder_host.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_detail_video_status_try_see.xml b/app/src/main/res/layout/layout_detail_video_status_try_see.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_detail_video_status_try_see_end.xml b/app/src/main/res/layout/layout_detail_video_status_try_see_end.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_dialog_backimg.xml b/app/src/main/res/layout/layout_dialog_backimg.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_dialog_downapk.xml b/app/src/main/res/layout/layout_dialog_downapk.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_dialog_fullimg.xml b/app/src/main/res/layout/layout_dialog_fullimg.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_dialog_order.xml b/app/src/main/res/layout/layout_dialog_order.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_dialog_order_cancel.xml b/app/src/main/res/layout/layout_dialog_order_cancel.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_dialog_tryseeover.xml b/app/src/main/res/layout/layout_dialog_tryseeover.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_dialog_user_exit.xml b/app/src/main/res/layout/layout_dialog_user_exit.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_error_prompt.xml b/app/src/main/res/layout/layout_error_prompt.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_login_new_qr.xml b/app/src/main/res/layout/layout_login_new_qr.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_retention_back.xml b/app/src/main/res/layout/layout_retention_back.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_retention_prompt.xml b/app/src/main/res/layout/layout_retention_prompt.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/layout_video_play_key_prompt_tips.xml b/app/src/main/res/layout/layout_video_play_key_prompt_tips.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/newtv_view.xml b/app/src/main/res/layout/newtv_view.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/pop_t9keyboard.xml b/app/src/main/res/layout/pop_t9keyboard.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/top_view.xml b/app/src/main/res/layout/top_view.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/update_dialog.xml b/app/src/main/res/layout/update_dialog.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/update_other_dialog.xml b/app/src/main/res/layout/update_other_dialog.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/useragreenment_view.xml b/app/src/main/res/layout/useragreenment_view.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/view_activation_code_input.xml b/app/src/main/res/layout/view_activation_code_input.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/view_customer_code.xml b/app/src/main/res/layout/view_customer_code.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/view_device_unregistered.xml b/app/src/main/res/layout/view_device_unregistered.xml
deleted file mode 100644
index 595f4d2..0000000
--- a/app/src/main/res/layout/view_device_unregistered.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/view_location_code_input.xml b/app/src/main/res/layout/view_location_code_input.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/view_no_data.xml b/app/src/main/res/layout/view_no_data.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/view_qr_code.xml b/app/src/main/res/layout/view_qr_code.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/view_user_agreement.xml b/app/src/main/res/layout/view_user_agreement.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/layout/web_activity_simple.xml b/app/src/main/res/layout/web_activity_simple.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/all_game_foc.webp b/app/src/main/res/mipmap-hdpi/all_game_foc.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/all_game_nor.png b/app/src/main/res/mipmap-hdpi/all_game_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_detail_video_try_see.png b/app/src/main/res/mipmap-hdpi/bg_detail_video_try_see.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_history_foc1.webp b/app/src/main/res/mipmap-hdpi/bg_history_foc1.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_history_foc2.webp b/app/src/main/res/mipmap-hdpi/bg_history_foc2.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_history_foc3.webp b/app/src/main/res/mipmap-hdpi/bg_history_foc3.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_history_foc4.png b/app/src/main/res/mipmap-hdpi/bg_history_foc4.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_history_nor1.png b/app/src/main/res/mipmap-hdpi/bg_history_nor1.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_history_nor2.png b/app/src/main/res/mipmap-hdpi/bg_history_nor2.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_history_nor3.png b/app/src/main/res/mipmap-hdpi/bg_history_nor3.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_history_nor4.png b/app/src/main/res/mipmap-hdpi/bg_history_nor4.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_index_my_collect.png b/app/src/main/res/mipmap-hdpi/bg_index_my_collect.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_index_my_history.png b/app/src/main/res/mipmap-hdpi/bg_index_my_history.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_item_match_list.png b/app/src/main/res/mipmap-hdpi/bg_item_match_list.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_match_list.png b/app/src/main/res/mipmap-hdpi/bg_match_list.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/bg_suggest.png b/app/src/main/res/mipmap-hdpi/bg_suggest.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/btn_episode_sec.png b/app/src/main/res/mipmap-hdpi/btn_episode_sec.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/btn_episode_sec_variety.png b/app/src/main/res/mipmap-hdpi/btn_episode_sec_variety.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/calendar1.png b/app/src/main/res/mipmap-hdpi/calendar1.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/calendar_nor.png b/app/src/main/res/mipmap-hdpi/calendar_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/common_notice.png b/app/src/main/res/mipmap-hdpi/common_notice.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/head_clear.png b/app/src/main/res/mipmap-hdpi/head_clear.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/head_clear_focus.png b/app/src/main/res/mipmap-hdpi/head_clear_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/head_delete.png b/app/src/main/res/mipmap-hdpi/head_delete.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/head_delete_focus.png b/app/src/main/res/mipmap-hdpi/head_delete_focus.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/home_icon_up_down_foc.png b/app/src/main/res/mipmap-hdpi/home_icon_up_down_foc.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/home_icon_up_down_nor.png b/app/src/main/res/mipmap-hdpi/home_icon_up_down_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_app.png b/app/src/main/res/mipmap-hdpi/ic_app.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_detail_benefits.png b/app/src/main/res/mipmap-hdpi/ic_detail_benefits.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_detail_btn_collect.png b/app/src/main/res/mipmap-hdpi/ic_detail_btn_collect.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_detail_btn_collect_f.png b/app/src/main/res/mipmap-hdpi/ic_detail_btn_collect_f.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_detail_btn_collect_s.png b/app/src/main/res/mipmap-hdpi/ic_detail_btn_collect_s.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_detail_btn_order.png b/app/src/main/res/mipmap-hdpi/ic_detail_btn_order.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_detail_btn_order_s.png b/app/src/main/res/mipmap-hdpi/ic_detail_btn_order_s.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_detail_video_from_mg.png b/app/src/main/res/mipmap-hdpi/ic_detail_video_from_mg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_key_prompt_1.png b/app/src/main/res/mipmap-hdpi/ic_key_prompt_1.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_key_prompt_2.png b/app/src/main/res/mipmap-hdpi/ic_key_prompt_2.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_key_prompt_3.png b/app/src/main/res/mipmap-hdpi/ic_key_prompt_3.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_key_prompt_4.png b/app/src/main/res/mipmap-hdpi/ic_key_prompt_4.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_migu_mini.png b/app/src/main/res/mipmap-hdpi/ic_migu_mini.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/ic_more_foc.png b/app/src/main/res/mipmap-hdpi/ic_more_foc.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/icon_backward.png b/app/src/main/res/mipmap-hdpi/icon_backward.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/icon_clear.png b/app/src/main/res/mipmap-hdpi/icon_clear.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/icon_forward.png b/app/src/main/res/mipmap-hdpi/icon_forward.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/icon_nonetwork.png b/app/src/main/res/mipmap-hdpi/icon_nonetwork.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/icon_noresult.png b/app/src/main/res/mipmap-hdpi/icon_noresult.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/icon_player_error.png b/app/src/main/res/mipmap-hdpi/icon_player_error.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/index_my_collect_more.png b/app/src/main/res/mipmap-hdpi/index_my_collect_more.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/index_my_history_more.png b/app/src/main/res/mipmap-hdpi/index_my_history_more.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/index_my_setting_bg.png b/app/src/main/res/mipmap-hdpi/index_my_setting_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/keyboard_bg.png b/app/src/main/res/mipmap-hdpi/keyboard_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/label_vip.png b/app/src/main/res/mipmap-hdpi/label_vip.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/my_account_bg.png b/app/src/main/res/mipmap-hdpi/my_account_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/my_avatar_default.png b/app/src/main/res/mipmap-hdpi/my_avatar_default.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/no1.png b/app/src/main/res/mipmap-hdpi/no1.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/no10.png b/app/src/main/res/mipmap-hdpi/no10.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/no2.png b/app/src/main/res/mipmap-hdpi/no2.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/no3.png b/app/src/main/res/mipmap-hdpi/no3.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/no4.png b/app/src/main/res/mipmap-hdpi/no4.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/no5.png b/app/src/main/res/mipmap-hdpi/no5.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/no6.png b/app/src/main/res/mipmap-hdpi/no6.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/no7.png b/app/src/main/res/mipmap-hdpi/no7.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/no8.png b/app/src/main/res/mipmap-hdpi/no8.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/no9.png b/app/src/main/res/mipmap-hdpi/no9.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/nodata.png b/app/src/main/res/mipmap-hdpi/nodata.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/player_bg_vip.webp b/app/src/main/res/mipmap-hdpi/player_bg_vip.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/player_load_bg.png b/app/src/main/res/mipmap-hdpi/player_load_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/player_load_logo.png b/app/src/main/res/mipmap-hdpi/player_load_logo.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/search_tv_line.png b/app/src/main/res/mipmap-hdpi/search_tv_line.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/status_icon_search_foc.png b/app/src/main/res/mipmap-hdpi/status_icon_search_foc.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/status_icon_search_nor.png b/app/src/main/res/mipmap-hdpi/status_icon_search_nor.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/t9_five_bg.png b/app/src/main/res/mipmap-hdpi/t9_five_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/t9_four_bg.png b/app/src/main/res/mipmap-hdpi/t9_four_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/t9_two_bg.png b/app/src/main/res/mipmap-hdpi/t9_two_bg.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/videocolled.png b/app/src/main/res/mipmap-hdpi/videocolled.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/videofs.png b/app/src/main/res/mipmap-hdpi/videofs.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/videohotel.png b/app/src/main/res/mipmap-hdpi/videohotel.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/videonocoll.png b/app/src/main/res/mipmap-hdpi/videonocoll.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/videoorder.png b/app/src/main/res/mipmap-hdpi/videoorder.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/videoordered.png b/app/src/main/res/mipmap-hdpi/videoordered.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/vip.png b/app/src/main/res/mipmap-hdpi/vip.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-hdpi/vs.png b/app/src/main/res/mipmap-hdpi/vs.png
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-xhdpi/jst_order_bg.webp b/app/src/main/res/mipmap-xhdpi/jst_order_bg.webp
new file mode 100644
index 0000000000000000000000000000000000000000..a80c7dd59aab233c261728a940dafa9a80fb250a
GIT binary patch
literal 7550
zcmeH~Wl$T?md8Vi6hiPqA;levyEGIl1q#6-Xwc%tt+-pU;xrA^plG4ANFY$$p=fa@
z6nB@mv$M1FW@q2~ygS4Bbmu?!%RT3R&-oo4)o0Iq=m7vjWd$t*ElDQF|2TgG0FgL6
zp#+k+(A4KB4fap0EKDMP55G|mI=Ehm`>$?JW8*9`j4Ce#u6$h@Z_nZQ0aS(77yL(O
zxYDT3x-j_h&29ms{e{AAPd;otSrtsRTm@VtDSm6yCS9!YmTcpz_zT72Pm@A-(p@I*
zJaP9G?wZ0~FT9%yck}TM3+@2vjtlPy`TzW4kT!(%7Fr;Be(^oH-nB)+=BU~zH1zSJ
zktxcXpG+4r+g0cR7Yw3OZ&7BbCKcoi){$umit2gU!xmm7Bl%c?4_qtnhaxv|qp$
zBcuu&dO;B-nZ=x5yM@u#0_7sKg?0`Dw5(8g6esY%`Q_o&nILT=;POv5xaR8Akeci$
zU9(zrEYSrnF_uIA!3G$D_+}L1@t`U-s^~|gDc9?0DzY&h=j<4L)ha7`eU9!}Z@oMY
z=KqxR+|@OTmj95D17HZLq#Ql7?IEsUK5WZO_j}QUd1QP$T9H0V?Qe+>Xa$Ow=DP
zyn2xJq9rZ;qHbCUQ>cWU;k!0-x6ctJ7D0)V0z1hwL3v6z7{S7+9%39Gp*&py_JL_k9sFJ&>Ha(hf5?YtnCR<{Mw8xC&$Sj0Jn#Nm2rNG#z~SSSY^TV|
zUCc*legNtc{3MypD2cVLAugC#`u)OScgBg=WSH8ZIVF7})laj46-OFxmE`+I)Q;Jr
zb{t5PU0g6@GanpCZC?S8Bhq2aG(_nzWss@||ab{k@
zIWA?&RiCkpAFLLH1YRAUG6M55+hNkn{?EV2atE|tw~)}n+ybOmI|%-l?ERO-QflX=
zpY6nI38ah?GHzA3FUP3+I19PlJS1@*;Ul9ZH=8*PF$Q_m6xqA3_xYk6_VURHf+PCM&Yt(LY02S~b1ff<
zd_leqE`}Vx`Zs_y^AL~}aAdnAwfXz|W7GYvkj3SOPVyf@4VqH8V*g$GfBl|+Hpu_D
zrF{mtxm|4n{J#x+gD3f_QUxERN!M`eI#{Ul@KkVvf(r*qF3VwS2ar5o(nETmjc6@w
zqMTMv{MW6@lWl?=bW^I{tt5xTiuDf_Ej4gC2VV3!wUcbCtYj0rSyBfNN!z=lyfFWx
z=84AZ?6L+CX)`C7`-O=v##h7>VWj6#fpb6r`{_G+I)r8lfyT&8FdCuyP9vqVZoQLZ
zUn9XnZG0E57f^6n1t)y{+dGvCa%
z8y;(36zvGdB-NB)jwM@S8dbY;CKrQ{V)uf1*2%Etvm9oF=#$p7>pDusUu;D93p6mb
zA_3tsvtaAFJrP-_@mTgME7VW_n$Y*~b6?LZarI&q%Gp_)AR+6RQA;(qsnDVPFH=pw
zyK*mVNz*&u+>3m*rV|5h%0lb$CwN5OD~NNNZ>tLo+P2~tsb>B8Vy;3Yp`X6Ff_1^r
zlKjE&mP5!deNoU${8^1h!He`otd%Fvq%ux}+cO-aUW;nBW1j%(>-=4P%!;)~?QyS&a?2#P977JQv@OYlOcb1?jHRz9;7_
zrdD70vtaf2a4tvF_{ZL|?E`*C^a4zaF5%ni@NQd;I%t3V>DTEDQ6jfsv-uVLcD_RN
z*Ff%ovBBlRQO`b#Fr~rPHZN6r>bJ+*uDtcegF8bBjR<)5OAZ(2_rH?V_U3>6j6(Xz
zekhHlkyidOk(!(|;Qpd(jgNLxE|Li4as<#eaXI(AjBk%^Zo~6TpkI12zh*qXS*sw9
zlFQScgY|W=OKqXk3Ava`?3j=3Fpx(SZ-<}79K1=^z%0uw(pX8EnG*Y==ykcBrDGpH
z5oJup!G_@E;zbdE1TMF@Pohe|F4WqC2j+OpxSS}Wikv1J(BM#Hvx_OKk1i#D=Dpnu
z(~Uz@cv*T6Rf`3F3t{Ee?Lc$?o~sF^K$8iX@J`6~dS1BP6&H~KCXpLt5c+%JmrV2d
zPV&@yvNkr2?1wF!5z-D~1N@Bcu#@K|hQs@o7T(mn4Yuw`{0;|XqqjymH}?x}*TMDe
zEgbT1`tA{z{yRpp#a*~iE3XIJc8`0fK-;bjG
zI;)=^t5EAUef{38!i<5wctt@kB_+T9R?R|=Tj_81e>?WL@bCvMbeMIJkte2WqNJWm)4NQBd7
zd-U(hK8sf`M*E$Q5>tc+p_6OVFEuY7PWrzgUeuq9n(8Vyw=%}`L-cG6gGzhzT&-6$
z%@^VLwZ=IG6vN)~0@yi9!h=Yy0?KVEATX|=che2^uuzivkQu5Q{t1BN9^kHLG!ZVj
zVd;z`f&O#$?Z#meq>+I9>*o7ZJ*$
zdB4$DJS#dO(GU=}{GvAatv_t5wz+
zum0K?&wVy~hqi1$^@LVMEdiux0RZs}wRt(IW=%@S(L)}kwyHytDERnFB3ec+KWw06!cF)je9-
zvNZ5ZIMn(M4i+%G-+B9v2v}kb3$~dc1$}&-eMk?J7Eu4tx+W1l=VtU`p&fOuk4*Z_
z?1&CSzkuA-Vx`c#3x|a4nk!J2em;%n0+if#gO3iZ_7g}L7G9~GGw>bE)=9i`M6RaU
z*fYX0TjL?^MuX>cKsV9hfb+U|M7ry|(oSySoh-OPi7B>ouLZXxS~8g*;$21_j;?jC
z@}?rPI>4ueh2!=HyqI^2aJya}nl3LURX9Q!YYiA4>~1N~*dL|(kIaS>bg7`EP%R1e6@{59gY!v
zumTV&4KaP*-GD3#7a;7SclndVENgbAbBTD)^QRBxWgf
z(NQYgFvJbyx@g)RVHfi@Z|fdekI!HJxstRyO{RWBSmU4R;oKFGcBGL6pf!Ou2;WC0
zchHFDa(Y+!44!8(>yAhN*7`lC9GsC+(u^S|WsexTu2ESlv`Mm9sIY0!w;)V3v0@kE
zSF$c#n=tUmyikdAOlR0If~3A~R4g=t6qM(qE9_4)Iv(ja(P$%yo-qY=XMP>C{v>~3@m7@P
z{&>=gn1XH4=vh5)ZF(pr2`Kbq$Q0W=>QsG0bw7@*%vR6F$DKA;j+fiz0mS~vb0Ugh
zn;db>L3tB3u@cd{)&6qU`9LP%PEaH`
ze+V*PRJ(iP#V}nik}6RyN?giV)xx-_?xRwe=
zaSdQ03W&~_)mv$85y7)Mjv=RO;OwhdUuHQh5fgrbSSf>G`dq!L3ve9F;D53iZSxa<
z9DA(d>cTU$q6n0lGU2ErAET5#ebMPp*B0Ckd(pV#1}4P^{kn%Mq;
z#ltM8Y-)R%SE$XKKvNLECh&N=dHOCsJ!~Nr;;C1th>i_;7F;?N?*RdRG-_ho>A`*}
z6~>BK!2)M)Gi7kiLx!6xNz$I8^)X#-HHV-38~t3Fe;ti`F4RbY;>|GTno;L*>Cr;K
zj^Wxxi+UCL^Df~CGB$#G*>9~0s(j{$Bzxt~Ezu<6{y$8$>}yLe?{j$x7U}6_UysM*
z
z=@FrgrXdJx#*!T~9UVrR(Qm+#UMA_OlVecbva`d-{#P(?ZL_Q1ROcpLn>FI)Bax*@
zO2_%OjnHC&+pl>(kFFn)HU~6O6=jLJ7cqLHyLHxH^??LKU>(M|?Fr;=9@c!`ShDSMPmH=YOy
zCQ+i-5w_5Jx;8~*z3J$ZzMt#F5!Y`4!6bF^ul*e%#~LBuvZQF-?ufJkv}P*vr-muERmfIU`6+DHJ=+S$z$wHjR6^Ro&~NI(mGP&Xt>~4Nj7^Xc3n#LT*!$Kfgw3PJKkPvn*Q=f
J>z_BNe*gt5*NgxF
literal 0
HcmV?d00001
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values-sw1080dp-land/dimens.xml b/app/src/main/res/values-sw1080dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values-sw360dp-land/dimens.xml b/app/src/main/res/values-sw360dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values-sw480dp-land/dimens.xml b/app/src/main/res/values-sw480dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values-sw540dp-land/dimens.xml b/app/src/main/res/values-sw540dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values-sw617dp-land/dimens.xml b/app/src/main/res/values-sw617dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values-sw720dp-land/dimens.xml b/app/src/main/res/values-sw720dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values-sw768dp-land/dimens.xml b/app/src/main/res/values-sw768dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values-sw864dp-land/dimens.xml b/app/src/main/res/values-sw864dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
old mode 100755
new mode 100644
index f8eb85d..c2c540c
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -112,4 +112,9 @@
#0056B3
#E5E7EB
#35FC7E
+
+
+ #1AFFFFFF
+ #33FFFFFF
+ #80FFFFFF
diff --git a/app/src/main/res/values/dimens_sw.xml b/app/src/main/res/values/dimens_sw.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
old mode 100755
new mode 100644
index cd4689e..73f8308
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -31,6 +31,7 @@
卡密激活失败,订单编号:%1$s,请联系客服退款
已购买VIP,请欣赏影片
有效期至:%1$s
+ 会员权益已到期
按
唤起播放设置菜单
@@ -119,4 +120,43 @@
退出应用
最大可输入5位数
会员权益激活
+
+ 会员权益已退订,将退出程序!
+ 确定
+
+
+ 语音服务运行中
+ 语音保活
+ 视听备【2025】B6539
+
+
+ 设置
+ 开启悬浮窗
+ 需要悬浮窗权限,用于语音冷启动服务相关(该功能有平台限制!)
+ 已开启悬浮窗
+ 未授权悬浮窗权限,无法开启
+ 正在打开系统权限设置…
+
+
+ 微信扫码登录
+
+
+ 退出登录
+
+ 是否退出登录?
+ 确认
+ 取消
+ 会员到期日:%1$s
+ 会员到期日:暂无数据
+
+
+ 微信登录
+ 扫码功能开发中
+
+ 取消
+
+
+ 正在生成二维码…
+ 请打开手机微信扫一扫
+ 登录失败:%1$s
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/xml/device_not_activated_scene.xml b/app/src/main/res/xml/device_not_activated_scene.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
old mode 100755
new mode 100644
diff --git a/app/src/main/res/xml/network_permission_config.xml b/app/src/main/res/xml/network_permission_config.xml
old mode 100755
new mode 100644
diff --git a/app/src/ruigeer/res/values/strings.xml b/app/src/ruigeer/res/values/strings.xml
old mode 100755
new mode 100644
diff --git a/app/src/test/java/com/migu/jiecheng/ExampleUnitTest.java b/app/src/test/java/com/migu/jiecheng/ExampleUnitTest.java
old mode 100755
new mode 100644
diff --git a/app/src/test/java/com/pukka/jiecheng/downapk/StoreVersionUpgraderTest.kt b/app/src/test/java/com/pukka/jiecheng/downapk/StoreVersionUpgraderTest.kt
new file mode 100644
index 0000000..2e35e2d
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/downapk/StoreVersionUpgraderTest.kt
@@ -0,0 +1,98 @@
+package com.pukka.jiecheng.downapk
+
+import com.pukka.jiecheng.network.dto.StoreVersionInfoDto
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+/**
+ * StoreVersionUpgrader 纯函数单测。
+ *
+ * 覆盖两个 internal 函数:
+ * - pickUrl(touch, v):URL 选择策略
+ * - isCompulsory(s):是否强制升级判定
+ *
+ * 这两个函数从主流程里抽出来便于测试,因为它们不带 Android 依赖、纯输入 → 输出,
+ * 是这条新链路最容易出 bug 的两个分支点。
+ */
+class StoreVersionUpgraderTest {
+
+ // --------------------------- pickUrl ---------------------------
+
+ @Test
+ fun `touch 模式优先 touchUrl`() {
+ val v = StoreVersionInfoDto(touchUrl = "http://touch.apk", keyUrl = "http://key.apk")
+ assertEquals("http://touch.apk", StoreVersionUpgrader.pickUrl(touch = true, v = v))
+ }
+
+ @Test
+ fun `touch 模式下 touchUrl 为空字符串时回退到 keyUrl`() {
+ val v = StoreVersionInfoDto(touchUrl = "", keyUrl = "http://key.apk")
+ assertEquals("http://key.apk", StoreVersionUpgrader.pickUrl(touch = true, v = v))
+ }
+
+ @Test
+ fun `touch 模式下 touchUrl 为 null 时回退到 keyUrl`() {
+ val v = StoreVersionInfoDto(touchUrl = null, keyUrl = "http://key.apk")
+ assertEquals("http://key.apk", StoreVersionUpgrader.pickUrl(touch = true, v = v))
+ }
+
+ @Test
+ fun `remote 模式始终取 keyUrl 忽略 touchUrl`() {
+ val v = StoreVersionInfoDto(touchUrl = "http://touch.apk", keyUrl = "http://key.apk")
+ assertEquals("http://key.apk", StoreVersionUpgrader.pickUrl(touch = false, v = v))
+ }
+
+ @Test
+ fun `remote 模式下 keyUrl 为空返回空串`() {
+ val v = StoreVersionInfoDto(touchUrl = "http://touch.apk", keyUrl = "")
+ assertEquals("", StoreVersionUpgrader.pickUrl(touch = false, v = v))
+ }
+
+ @Test
+ fun `remote 模式下 keyUrl 为 null 返回空串`() {
+ val v = StoreVersionInfoDto(touchUrl = "http://touch.apk", keyUrl = null)
+ assertEquals("", StoreVersionUpgrader.pickUrl(touch = false, v = v))
+ }
+
+ @Test
+ fun `touch 模式下两者都为空返回空串`() {
+ val v = StoreVersionInfoDto(touchUrl = "", keyUrl = "")
+ assertEquals("", StoreVersionUpgrader.pickUrl(touch = true, v = v))
+ }
+
+ @Test
+ fun `versionInfo 为 null 时返回空串`() {
+ assertEquals("", StoreVersionUpgrader.pickUrl(touch = true, v = null))
+ assertEquals("", StoreVersionUpgrader.pickUrl(touch = false, v = null))
+ }
+
+ // --------------------------- isCompulsory ---------------------------
+
+ @Test
+ fun `isCompulsory - true 大写小写都识别`() {
+ assertTrue(StoreVersionUpgrader.isCompulsory("true"))
+ assertTrue(StoreVersionUpgrader.isCompulsory("TRUE"))
+ assertTrue(StoreVersionUpgrader.isCompulsory("True"))
+ }
+
+ @Test
+ fun `isCompulsory - 1 识别为强制`() {
+ assertTrue(StoreVersionUpgrader.isCompulsory("1"))
+ }
+
+ @Test
+ fun `isCompulsory - 0 false no yes 不识别`() {
+ assertFalse(StoreVersionUpgrader.isCompulsory("0"))
+ assertFalse(StoreVersionUpgrader.isCompulsory("false"))
+ assertFalse(StoreVersionUpgrader.isCompulsory("no"))
+ assertFalse(StoreVersionUpgrader.isCompulsory("yes"))
+ }
+
+ @Test
+ fun `isCompulsory - null 和空字符串都不是强制`() {
+ assertFalse(StoreVersionUpgrader.isCompulsory(null))
+ assertFalse(StoreVersionUpgrader.isCompulsory(""))
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/pukka/jiecheng/index/IndexActivityNavSelectionStateTest.java b/app/src/test/java/com/pukka/jiecheng/index/IndexActivityNavSelectionStateTest.java
old mode 100755
new mode 100644
diff --git a/app/src/test/java/com/pukka/jiecheng/index/adapter/viewholder/NavViewHolderTest.java b/app/src/test/java/com/pukka/jiecheng/index/adapter/viewholder/NavViewHolderTest.java
old mode 100755
new mode 100644
diff --git a/app/src/test/java/com/pukka/jiecheng/order/adapter/OrderCenterAdapterStructureTest.java b/app/src/test/java/com/pukka/jiecheng/order/adapter/OrderCenterAdapterStructureTest.java
new file mode 100644
index 0000000..311cf36
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/order/adapter/OrderCenterAdapterStructureTest.java
@@ -0,0 +1,24 @@
+/**
+ * @file OrderCenterAdapterStructureTest.java
+ * @description 约束订购中心适配器不再暴露废弃的家庭版 Header 开关
+ * @author ainetv
+ * @date 2026-09-08
+ */
+package com.pukka.jiecheng.order.adapter;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.assertFalse;
+
+public class OrderCenterAdapterStructureTest {
+
+ @Test
+ public void 不应再暴露家庭版Header开关() {
+ boolean hasHomeEditionSetter = Arrays.stream(OrderCenterAdapter.class.getDeclaredMethods())
+ .anyMatch(method -> "setHomeEdition".equals(method.getName()));
+
+ assertFalse(hasHomeEditionSetter);
+ }
+}
diff --git a/app/src/test/java/com/pukka/jiecheng/request/AsApiErrorMapperTest.kt b/app/src/test/java/com/pukka/jiecheng/request/AsApiErrorMapperTest.kt
new file mode 100644
index 0000000..98b12d1
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/request/AsApiErrorMapperTest.kt
@@ -0,0 +1,85 @@
+package com.pukka.jiecheng.request
+
+import com.pukka.jiecheng.network.infra.ApiException
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import retrofit2.HttpException
+import retrofit2.Response
+import okhttp3.ResponseBody
+import java.io.IOException
+import java.net.ConnectException
+import java.net.SocketTimeoutException
+import java.net.UnknownHostException
+
+/**
+ * AsApiErrorMapper 单测:验证 1007=网络 / 1004=解析 的统一错误码语义与友好文案。
+ */
+class AsApiErrorMapperTest {
+
+ @Test
+ fun `枚举语义对换后 1007 是网络 1004 是解析`() {
+ assertEquals(1007, AsApiErrorCode.ERROR_NETWORK.getCode())
+ assertEquals("网络异常,请稍后重试", AsApiErrorCode.ERROR_NETWORK.getMessage())
+ assertEquals(1004, AsApiErrorCode.ERROR_RESULT_PARSE.getCode())
+ assertEquals(1005, AsApiErrorCode.ERROR_SERVER.getCode())
+ }
+
+ @Test
+ fun `UnknownHostException 映射 1007 无网络文案`() {
+ val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(UnknownHostException("no dns"))
+ assertEquals(1007, code)
+ assertEquals(AsApiErrorMapper.MSG_NO_NETWORK, msg)
+ }
+
+ @Test
+ fun `ConnectException 映射 1007 无法访问服务器`() {
+ val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(ConnectException("refused"))
+ assertEquals(1007, code)
+ assertEquals(AsApiErrorMapper.MSG_SERVER_UNREACHABLE, msg)
+ }
+
+ @Test
+ fun `SocketTimeoutException 映射 1007 连接超时`() {
+ val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(SocketTimeoutException("timeout"))
+ assertEquals(1007, code)
+ assertEquals(AsApiErrorMapper.MSG_SERVER_TIMEOUT, msg)
+ }
+
+ @Test
+ fun `普通 IOException 映射 1007 通用网络文案`() {
+ val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(IOException("reset"))
+ assertEquals(1007, code)
+ assertEquals(AsApiErrorMapper.MSG_NETWORK_GENERIC, msg)
+ }
+
+ @Test
+ fun `Moshi 解析异常映射 1004 解析文案`() {
+ val parseError = com.squareup.moshi.JsonDataException("expected a name")
+ val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(parseError)
+ assertEquals(1004, code)
+ assertEquals(AsApiErrorMapper.MSG_PARSE, msg)
+ }
+
+ @Test
+ fun `HttpException 映射 1005 服务器异常`() {
+ val http = HttpException(Response.error(500, ResponseBody.create(null, "boom")))
+ val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(http)
+ assertEquals(1005, code)
+ assertEquals(AsApiErrorMapper.MSG_HTTP_SERVER, msg)
+ }
+
+ @Test
+ fun `业务异常原样透传 code 与 msg`() {
+ val biz = ApiException.BusinessException(code = 10500, msg = "激活失败")
+ val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(biz)
+ assertEquals(10500, code)
+ assertEquals("激活失败", msg)
+ }
+
+ @Test
+ fun `未知异常映射 1099 且保留原始 message`() {
+ val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(IllegalStateException("stale"))
+ assertEquals(1099, code)
+ assertEquals("stale", msg)
+ }
+}
diff --git a/app/src/test/java/com/pukka/jiecheng/request/AsApiRequesterMacCacheTest.kt b/app/src/test/java/com/pukka/jiecheng/request/AsApiRequesterMacCacheTest.kt
new file mode 100644
index 0000000..24b2c35
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/request/AsApiRequesterMacCacheTest.kt
@@ -0,0 +1,27 @@
+package com.pukka.jiecheng.request
+
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class AsApiRequesterMacCacheTest {
+
+ @Test
+ fun `同一个缓存实例只加载一次`() {
+ val cache = SingleValueCache>()
+ var loadCount = 0
+
+ val first = cache.getOrLoad {
+ loadCount++
+ arrayOf("ETH_MAC", "WIFI_MAC")
+ }
+ val second = cache.getOrLoad {
+ loadCount++
+ arrayOf("ETH_MAC_2", "WIFI_MAC_2")
+ }
+
+ assertEquals(1, loadCount)
+ assertArrayEquals(arrayOf("ETH_MAC", "WIFI_MAC"), first)
+ assertArrayEquals(arrayOf("ETH_MAC", "WIFI_MAC"), second)
+ }
+}
diff --git a/app/src/test/java/com/pukka/jiecheng/utils/JetsenxSdkUtilFallbackAdapterTest.java b/app/src/test/java/com/pukka/jiecheng/utils/JetsenxSdkUtilFallbackAdapterTest.java
new file mode 100644
index 0000000..e280537
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/utils/JetsenxSdkUtilFallbackAdapterTest.java
@@ -0,0 +1,76 @@
+package com.pukka.jiecheng.utils;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * @file JetsenxSdkUtilFallbackAdapterTest.java
+ * @description 验证 globalInit 兜底分支的三态回调(FallbackInitCallback)到二态回调(InitMemberCodeCallback)的映射
+ * @author ainetv
+ * @date 2026-09-17
+ */
+public class JetsenxSdkUtilFallbackAdapterTest {
+
+ private static class RecordingCallback implements JetsenxSdkUtil.InitMemberCodeCallback {
+ String successMemberCode;
+ Integer failCode;
+ String failMessage;
+
+ @Override
+ public void onSuccess(String memberCode) {
+ successMemberCode = memberCode;
+ }
+
+ @Override
+ public void onFail(int errorCode, String message) {
+ failCode = errorCode;
+ failMessage = message;
+ }
+ }
+
+ @Test
+ public void onReady透传到onSuccess() {
+ RecordingCallback recorder = new RecordingCallback();
+ JetsenxSdkUtil.FallbackInitCallback adapted = JetsenxSdkUtil.toFallbackCallback(recorder);
+
+ adapted.onReady("member-001");
+
+ assertEquals("member-001", recorder.successMemberCode);
+ assertNull(recorder.failCode);
+ }
+
+ @Test
+ public void onNoEntitlement映射到onFail() {
+ RecordingCallback recorder = new RecordingCallback();
+ JetsenxSdkUtil.FallbackInitCallback adapted = JetsenxSdkUtil.toFallbackCallback(recorder);
+
+ adapted.onNoEntitlement(10700, "权益已过期");
+
+ assertNull(recorder.successMemberCode);
+ assertEquals(Integer.valueOf(10700), recorder.failCode);
+ assertEquals("权益已过期", recorder.failMessage);
+ }
+
+ @Test
+ public void onNetworkError映射到onFail() {
+ RecordingCallback recorder = new RecordingCallback();
+ JetsenxSdkUtil.FallbackInitCallback adapted = JetsenxSdkUtil.toFallbackCallback(recorder);
+
+ adapted.onNetworkError(1004, "网络异常");
+
+ assertNull(recorder.successMemberCode);
+ assertEquals(Integer.valueOf(1004), recorder.failCode);
+ assertEquals("网络异常", recorder.failMessage);
+ }
+
+ @Test
+ public void 空callback不抛异常() {
+ JetsenxSdkUtil.FallbackInitCallback adapted = JetsenxSdkUtil.toFallbackCallback(null);
+
+ adapted.onReady("member-001");
+ adapted.onNoEntitlement(0, "无权益");
+ adapted.onNetworkError(1004, "网络异常");
+ }
+}
diff --git a/app/src/test/java/com/pukka/jiecheng/utils/JetsenxSdkUtilFallbackThrottleTest.java b/app/src/test/java/com/pukka/jiecheng/utils/JetsenxSdkUtilFallbackThrottleTest.java
new file mode 100644
index 0000000..de649a3
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/utils/JetsenxSdkUtilFallbackThrottleTest.java
@@ -0,0 +1,51 @@
+package com.pukka.jiecheng.utils;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @file JetsenxSdkUtilFallbackThrottleTest.java
+ * @description 验证 recoverUidAndInit 的 3 秒节流:窗口期内只放行一次 checkBenefits 请求,窗口期外允许重试
+ * @author ainetv
+ * @date 2026-09-17
+ */
+public class JetsenxSdkUtilFallbackThrottleTest {
+
+ @Before
+ public void resetThrottle() throws Exception {
+ Field field = JetsenxSdkUtil.class.getDeclaredField("sLastFallbackAt");
+ field.setAccessible(true);
+ ((AtomicLong) field.get(null)).set(0L);
+ }
+
+ @Test
+ public void 首次调用放行() {
+ assertTrue(JetsenxSdkUtil.tryAcquireFallback(10_000L));
+ }
+
+ @Test
+ public void 三秒内第二次调用被挡下() {
+ assertTrue(JetsenxSdkUtil.tryAcquireFallback(10_000L));
+ assertFalse(JetsenxSdkUtil.tryAcquireFallback(12_999L));
+ }
+
+ @Test
+ public void 满三秒后允许重试() {
+ assertTrue(JetsenxSdkUtil.tryAcquireFallback(10_000L));
+ assertTrue(JetsenxSdkUtil.tryAcquireFallback(13_000L));
+ }
+
+ @Test
+ public void 被挡下的调用不推进时间戳() {
+ assertTrue(JetsenxSdkUtil.tryAcquireFallback(10_000L));
+ assertFalse(JetsenxSdkUtil.tryAcquireFallback(11_000L));
+ // 若被挡下的调用也推进时间戳,13_000 时距 11_000 仅 2 秒会被误挡
+ assertTrue(JetsenxSdkUtil.tryAcquireFallback(13_000L));
+ }
+}
diff --git a/app/src/test/java/com/pukka/jiecheng/voice/parser/SkyworthVodParserTest.java b/app/src/test/java/com/pukka/jiecheng/voice/parser/SkyworthVodParserTest.java
new file mode 100644
index 0000000..eac9383
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/voice/parser/SkyworthVodParserTest.java
@@ -0,0 +1,282 @@
+package com.pukka.jiecheng.voice.parser;
+
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.protocol.VoiceCommandType;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * SkyworthVodParser 单测。
+ *
+ * 覆盖文档 §6.3 中创维 VOD 场景:
+ * 第三方投递广播 → {@link com.pukka.jiecheng.voice.adapter.SkyworthVoiceAdapter}
+ * 归一化 JSON → 本 Parser 解析 → VoiceCommand(OPEN_FILM_DETAIL, filmName)。
+ */
+public class SkyworthVodParserTest {
+
+ private final SkyworthVodParser parser = new SkyworthVodParser();
+
+ // ===== 正常路径 =====
+
+ @Test
+ public void 应当正确解析JUMP_FILM并产出OPEN_FILM_DETAIL() {
+ VoiceCommand cmd = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":\"流浪地球\"}");
+
+ assertNotNull(cmd);
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, cmd.getCommandType());
+ assertEquals("流浪地球", cmd.getPayloadValue("filmName"));
+ // 未显式带 autoPlay 时,Parser 不额外注入(默认值由 Adapter 归一化层负责);
+ // 这里只透传 filmName,payload size = 1
+ assertEquals(1, cmd.getPayload().size());
+ }
+
+ @Test
+ public void 应当透传autoPlay字段到payload() {
+ // Adapter 归一化时默认写入 autoPlay=true;Parser 需原样透传给 Navigator。
+ VoiceCommand cmd = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":\"流浪地球\",\"autoPlay\":\"true\"}");
+
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, cmd.getCommandType());
+ assertEquals("流浪地球", cmd.getPayloadValue("filmName"));
+ assertEquals("true", cmd.getPayloadValue("autoPlay"));
+ // 白名单字段仅 filmName + autoPlay → size = 2
+ assertEquals(2, cmd.getPayload().size());
+ }
+
+ @Test
+ public void 应当透传autoPlay_false并忽略其他多余字段() {
+ // 调试场景下 extra 里显式传 autoPlay=false 能覆盖默认值;
+ // 同时任何非白名单字段(extra / customFlag)都必须被丢弃,避免 Navigator 误消费。
+ VoiceCommand cmd = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":\"战狼2\",\"autoPlay\":\"false\","
+ + "\"extra\":\"ignore\",\"customFlag\":\"x\"}");
+
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, cmd.getCommandType());
+ assertEquals("战狼2", cmd.getPayloadValue("filmName"));
+ assertEquals("false", cmd.getPayloadValue("autoPlay"));
+ assertNull(cmd.getPayloadValue("extra"));
+ assertNull(cmd.getPayloadValue("customFlag"));
+ // 白名单字段仅 filmName + autoPlay → size = 2
+ assertEquals(2, cmd.getPayload().size());
+ }
+
+ @Test
+ public void payload中多余字段应当被忽略() {
+ // Adapter 归一化时仅写 jumpType/filmName/autoPlay;防御性校验:
+ // 上游意外多塞的其他私有字段不得进入 payload。
+ // 注意:autoPlay 属于白名单保留字段,此处不放入输入,单独用上面用例覆盖。
+ VoiceCommand cmd = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":\"战狼2\",\"extra\":\"ignore\",\"customFlag\":\"drop-me\"}");
+
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, cmd.getCommandType());
+ assertEquals("战狼2", cmd.getPayloadValue("filmName"));
+ // 解析结果只保留 filmName(无 autoPlay 输入时不注入),避免后续 navigator 误消费上游私有字段
+ assertEquals(1, cmd.getPayload().size());
+ assertNull(cmd.getPayloadValue("extra"));
+ assertNull(cmd.getPayloadValue("customFlag"));
+ }
+
+ @Test
+ public void filmName包含特殊字符应能正确处理() {
+ // JSON 转义后的英文双引号
+ VoiceCommand cmd = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":\"\\\"Hello\\\" World\"}");
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, cmd.getCommandType());
+ assertEquals("\"Hello\" World", cmd.getPayloadValue("filmName"));
+
+ // 中文冒号 / 空格
+ VoiceCommand cmd2 = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":\" 黑客帝国 : 矩阵重启 \"}");
+ assertEquals(" 黑客帝国 : 矩阵重启 ", cmd2.getPayloadValue("filmName"));
+ }
+
+ @Test
+ public void payload字段顺序与换行不影响解析() {
+ VoiceCommand cmd = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\n \"filmName\" : \"星际穿越\" ,\n \"jumpType\":\"FILM\"\n}");
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, cmd.getCommandType());
+ assertEquals("星际穿越", cmd.getPayloadValue("filmName"));
+ }
+
+ // ===== commandType 校验 =====
+
+ @Test
+ public void commandType为null应当抛异常() {
+ try {
+ parser.parse(null, "{\"jumpType\":\"FILM\",\"filmName\":\"x\"}");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("commandType"));
+ }
+ }
+
+ @Test
+ public void commandType不是JUMP应当抛异常() {
+ try {
+ parser.parse("SEARCH", "{\"filmName\":\"x\"}");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("SEARCH"));
+ }
+
+ try {
+ parser.parse("CONTROL", "{\"action\":\"PAUSE\"}");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("CONTROL"));
+ }
+ }
+
+ // ===== jumpType 校验 =====
+
+ @Test
+ public void jumpType缺失应当抛异常() {
+ try {
+ parser.parse(SkyworthVodParser.COMMAND_JUMP, "{\"filmName\":\"x\"}");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("jumpType"));
+ }
+ }
+
+ @Test
+ public void jumpType为空字符串应当抛异常() {
+ try {
+ parser.parse(SkyworthVodParser.COMMAND_JUMP, "{\"jumpType\":\"\",\"filmName\":\"x\"}");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("jumpType"));
+ }
+ }
+
+ @Test
+ public void jumpType不是FILM应当抛异常() {
+ // 创维 VOD Parser 仅认 FILM;CATEGORY/ACTOR/DIRECTOR 走 VendorAParser,
+ // 不在 SkyworthVodParser 支持范围内
+ try {
+ parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"CATEGORY\",\"categoryName\":\"动作片\"}");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("CATEGORY"));
+ }
+ }
+
+ // ===== filmName 校验 =====
+
+ @Test
+ public void filmName缺失应当抛异常() {
+ try {
+ parser.parse(SkyworthVodParser.COMMAND_JUMP, "{\"jumpType\":\"FILM\"}");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("filmName"));
+ }
+ }
+
+ @Test
+ public void filmName为空字符串应当抛异常() {
+ try {
+ parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":\"\"}");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("filmName"));
+ }
+ }
+
+ @Test
+ public void filmName为空白字符串应当被视作非法() {
+ // 注:当前实现按 isEmpty() 判断,空白字符串不算空但行为上等同非法。
+ // 本用例固定现状:空白原样透传,由下游 navigator / 搜索兜底。
+ VoiceCommand cmd = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":\" \"}");
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, cmd.getCommandType());
+ assertEquals(" ", cmd.getPayloadValue("filmName"));
+ }
+
+ // ===== payload 形态校验 =====
+
+ @Test
+ public void payload为null应当在jumpType校验时抛异常() {
+ try {
+ parser.parse(SkyworthVodParser.COMMAND_JUMP, null);
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ // jumpType 缺失触发
+ assertTrue(ex.getMessage().contains("jumpType"));
+ }
+ }
+
+ @Test
+ public void payload为非法JSON应当抛异常() {
+ // 用 JSON 数组:一定不是 JSONObject,JSONException 分支稳定触发
+ // (宽松的 malformed 对象在 org.json 不同实现下行为不一致,不作断言依据)
+ try {
+ parser.parse(SkyworthVodParser.COMMAND_JUMP, "[\"FILM\",\"流浪地球\"]");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("JSON"));
+ }
+ }
+
+ @Test
+ public void payload为JSON数组而非对象应当抛异常() {
+ try {
+ parser.parse(SkyworthVodParser.COMMAND_JUMP, "[\"jumpType\":\"FILM\"]");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("JSON"));
+ }
+ }
+
+ @Test
+ public void payload为空对象应当因缺少jumpType而抛异常() {
+ try {
+ parser.parse(SkyworthVodParser.COMMAND_JUMP, "{}");
+ fail("应当抛出 IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("jumpType"));
+ }
+ }
+
+ // ===== 协议常量稳定性 =====
+
+ @Test
+ public void 公开常量应当与Adapter归一化协议保持一致() {
+ // Adapter 在归一化时会写死 JUMP + jumpType=FILM,本 Parser 常量被两端共同依赖。
+ // 单测固定下来,避免后续 rename 出现 Adapter / Parser 不一致。
+ assertEquals("JUMP", SkyworthVodParser.COMMAND_JUMP);
+ assertEquals("FILM", SkyworthVodParser.JUMP_FILM);
+ }
+
+ @Test
+ public void 多次解析应当互不影响() {
+ VoiceCommand a = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":\"AAA\"}");
+ VoiceCommand b = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":\"BBB\"}");
+
+ assertEquals("AAA", a.getPayloadValue("filmName"));
+ assertEquals("BBB", b.getPayloadValue("filmName"));
+ }
+
+ @Test
+ public void payload中filmName值含null字面量应当按字符串处理() {
+ // JSON 中 "filmName":null 会进入 result.put(k, "null") 而非真 null,
+ // 此时 isEmpty() 仍为 false,应被当作合法 filmName 透传(后续由搜索兜底)。
+ VoiceCommand cmd = parser.parse(SkyworthVodParser.COMMAND_JUMP,
+ "{\"jumpType\":\"FILM\",\"filmName\":null}");
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, cmd.getCommandType());
+ assertEquals("null", cmd.getPayloadValue("filmName"));
+ }
+}
diff --git a/app/src/test/java/com/pukka/jiecheng/voice/parser/VendorAParserTest.java b/app/src/test/java/com/pukka/jiecheng/voice/parser/VendorAParserTest.java
new file mode 100644
index 0000000..9bdb56d
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/voice/parser/VendorAParserTest.java
@@ -0,0 +1,161 @@
+package com.pukka.jiecheng.voice.parser;
+
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.protocol.VoiceCommandType;
+
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * VendorAParser 单测(文档 §6.1)。
+ */
+public class VendorAParserTest {
+
+ @Test
+ public void 应当正确解析JUMP_FILM并填充payload() {
+ Map payload = new HashMap<>();
+ payload.put("jumpType", "FILM");
+ payload.put("filmId", "film-001");
+ payload.put("filmName", "冷启动测试片");
+ payload.put("autoPlay", "true");
+
+ VendorAParser parser = new VendorAParser();
+ VoiceCommand cmd = parser.parseJumpPayload(payload);
+
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, cmd.getCommandType());
+ assertEquals("film-001", cmd.getPayloadValue("filmId"));
+ assertEquals("true", cmd.getPayloadValue("autoPlay"));
+ }
+
+ @Test
+ public void 应当正确解析JUMP_CATEGORY() {
+ Map payload = new HashMap<>();
+ payload.put("jumpType", "CATEGORY");
+ payload.put("categoryId", "cat-action");
+ payload.put("categoryName", "动作片");
+
+ VoiceCommand cmd = new VendorAParser().parseJumpPayload(payload);
+ assertEquals(VoiceCommandType.OPEN_CATEGORY, cmd.getCommandType());
+ assertEquals("cat-action", cmd.getPayloadValue("categoryId"));
+ }
+
+ @Test
+ public void 未支持的jumpType应当抛异常() {
+ Map payload = new HashMap<>();
+ payload.put("jumpType", "UNKNOWN_TYPE");
+ try {
+ new VendorAParser().parseJumpPayload(payload);
+ fail("应当抛出非法参数异常");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("UNKNOWN_TYPE"));
+ }
+ }
+
+ @Test
+ public void 应当正确解析SEARCH_payload() {
+ Map payload = new HashMap<>();
+ payload.put("filmName", "流浪地球");
+ payload.put("actorName", "吴京");
+ payload.put("category", "科幻");
+
+ VoiceCommand cmd = new VendorAParser().parseSearchPayload(payload);
+ assertEquals(VoiceCommandType.SEARCH_FILM, cmd.getCommandType());
+ assertEquals("流浪地球", cmd.getPayloadValue("filmName"));
+ assertEquals("吴京", cmd.getPayloadValue("actorName"));
+ }
+
+ @Test
+ public void 应当正确解析CONTROL所有action() {
+ String[][] cases = new String[][]{
+ {"PLAY", "PLAYBACK_RESUME"},
+ {"PAUSE", "PLAYBACK_PAUSE"},
+ {"FULLSCREEN_ENTER", "PLAYBACK_ENTER_FULLSCREEN"},
+ {"FULLSCREEN_EXIT", "PLAYBACK_EXIT_FULLSCREEN"},
+ {"SEEK_FORWARD", "PLAYBACK_SEEK_FORWARD"},
+ {"SEEK_BACKWARD", "PLAYBACK_SEEK_BACKWARD"},
+ {"SEEK_TO", "PLAYBACK_SEEK_TO"},
+ {"PREV", "PLAYBACK_PREVIOUS"},
+ {"NEXT", "PLAYBACK_NEXT"},
+ {"SELECT_INDEX", "PLAYBACK_SELECT_INDEX"},
+ {"PAGE_NEXT", "PLAYBACK_PAGE_NEXT"},
+ {"PAGE_PREV", "PLAYBACK_PAGE_PREV"},
+ {"GOTO_PAGE", "PLAYBACK_GOTO_PAGE"},
+ };
+
+ for (String[] c : cases) {
+ Map payload = new HashMap<>();
+ payload.put("action", c[0]);
+ payload.put("seconds", "10");
+ payload.put("index", "3");
+ payload.put("page", "2");
+ VoiceCommand cmd = new VendorAParser().parseControlPayload(payload);
+ assertNotNull(cmd);
+ assertEquals(c[1], cmd.getCommandType().name());
+ assertEquals("10", cmd.getPayloadValue("seconds"));
+ assertEquals("3", cmd.getPayloadValue("index"));
+ assertEquals("2", cmd.getPayloadValue("page"));
+ }
+ }
+
+ @Test
+ public void 未支持的control_action应当抛异常() {
+ Map payload = new HashMap<>();
+ payload.put("action", "WHATEVER");
+ try {
+ new VendorAParser().parseControlPayload(payload);
+ fail("应当抛出非法参数异常");
+ } catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("WHATEVER"));
+ }
+ }
+
+ @Test
+ public void parse应当根据commandType分发到正确子解析() {
+ Map jumpMap = new HashMap<>();
+ jumpMap.put("jumpType", "FILM");
+ jumpMap.put("filmId", "f1");
+ VoiceCommand jump = new VendorAParser().parse("JUMP", jumpMap);
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, jump.getCommandType());
+
+ Map search = new HashMap<>();
+ search.put("filmName", "流浪地球");
+ VoiceCommand searchCmd = new VendorAParser().parse("SEARCH", search);
+ assertEquals(VoiceCommandType.SEARCH_FILM, searchCmd.getCommandType());
+
+ Map control = new HashMap<>();
+ control.put("action", "PAUSE");
+ VoiceCommand controlCmd = new VendorAParser().parse("CONTROL", control);
+ assertEquals(VoiceCommandType.PLAYBACK_PAUSE, controlCmd.getCommandType());
+
+ try {
+ new VendorAParser().parse("WHATEVER", new HashMap<>());
+ fail("应当抛出非法参数异常");
+ } catch (IllegalArgumentException ex) {
+ assertFalse(ex.getMessage().isEmpty());
+ }
+ }
+
+ @Test
+ public void 简单JSON解析器应当支持扁平对象() {
+ Map parsed = VendorAParser.SimpleJsonParser.parseObject(
+ "{\"jumpType\":\"FILM\",\"filmId\":\"x\",\"autoPlay\":\"true\"}");
+ assertEquals("FILM", parsed.get("jumpType"));
+ assertEquals("x", parsed.get("filmId"));
+ assertEquals("true", parsed.get("autoPlay"));
+ }
+
+ @Test
+ public void 简单JSON解析器应能处理空对象与null() {
+ assertTrue(VendorAParser.SimpleJsonParser.parseObject(null).isEmpty());
+ assertTrue(VendorAParser.SimpleJsonParser.parseObject("{}").isEmpty());
+ assertTrue(VendorAParser.SimpleJsonParser.parseObject(" ").isEmpty());
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceCallbackResultTest.java b/app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceCallbackResultTest.java
new file mode 100644
index 0000000..60a295d
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceCallbackResultTest.java
@@ -0,0 +1,58 @@
+package com.pukka.jiecheng.voice.protocol;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * VoiceCallbackResult 模型单测。
+ */
+public class VoiceCallbackResultTest {
+
+ @Test
+ public void 成功结果应当正确携带success标志与code() {
+ VoiceCallbackResult result = VoiceCallbackResult.success("vt-001", "OK", "data-payload");
+
+ assertEquals("vt-001", result.getRequestId());
+ assertTrue(result.isSuccess());
+ assertEquals("OK", result.getResultCode());
+ assertEquals("data-payload", result.getResultMessage());
+ assertEquals("data-payload", result.getData());
+ }
+
+ @Test
+ public void 失败结果应当正确构造() {
+ VoiceCallbackResult result = VoiceCallbackResult.failure("vt-002", "DEVICE_NOT_ACTIVATED", "设备未激活");
+
+ assertEquals("vt-002", result.getRequestId());
+ assertFalse(result.isSuccess());
+ assertEquals("DEVICE_NOT_ACTIVATED", result.getResultCode());
+ assertEquals("设备未激活", result.getResultMessage());
+ assertNull(result.getData());
+ }
+
+ @Test
+ public void 标准结果码常量应当与文档一致() {
+ assertNotNull(VoiceCallbackResult.CODE_OK);
+ assertNotNull(VoiceCallbackResult.CODE_DEVICE_NOT_ACTIVATED);
+ assertNotNull(VoiceCallbackResult.CODE_INVALID_PAYLOAD);
+ assertNotNull(VoiceCallbackResult.CODE_THROTTLED);
+ assertNotNull(VoiceCallbackResult.CODE_NOT_IN_PLAYER);
+ assertNotNull(VoiceCallbackResult.CODE_DIALOG_BUSY);
+ assertNotNull(VoiceCallbackResult.CODE_FORBIDDEN);
+ }
+
+ @Test
+ public void 序列化结果应当包含所有字段() {
+ VoiceCallbackResult result = VoiceCallbackResult.success("vt-003", "OK", "data");
+
+ String json = result.toJsonString();
+ assertTrue(json.contains("vt-003"));
+ assertTrue(json.contains("\"success\":true"));
+ assertTrue(json.contains("\"resultCode\":\"OK\""));
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceCommandTest.java b/app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceCommandTest.java
new file mode 100644
index 0000000..c8d5f78
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceCommandTest.java
@@ -0,0 +1,91 @@
+package com.pukka.jiecheng.voice.protocol;
+
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * VoiceCommand 模型单测。
+ */
+public class VoiceCommandTest {
+
+ @Test
+ public void 应当正确构造跳转单片命令() {
+ Map payload = new HashMap<>();
+ payload.put("filmId", "film-001");
+ payload.put("filmName", "测试片");
+ payload.put("autoPlay", "true");
+
+ VoiceCommand cmd = VoiceCommand.builder()
+ .commandType(VoiceCommandType.OPEN_FILM_DETAIL)
+ .requestId("vt-123")
+ .sourceApp("com.test.voicehelper")
+ .sourcePlatform("vendorA")
+ .callbackAction("com.test.voicehelper.action.VOICE_CALLBACK")
+ .payload(payload)
+ .build();
+
+ assertEquals(VoiceCommandType.OPEN_FILM_DETAIL, cmd.getCommandType());
+ assertEquals("vt-123", cmd.getRequestId());
+ assertEquals("film-001", cmd.getPayloadValue("filmId"));
+ assertEquals("true", cmd.getPayloadValue("autoPlay"));
+ }
+
+ @Test
+ public void 应当支持构造搜索命令并取各字段() {
+ Map payload = new HashMap<>();
+ payload.put("filmName", "流浪地球");
+ payload.put("actorName", "吴京");
+
+ VoiceCommand cmd = VoiceCommand.builder()
+ .commandType(VoiceCommandType.SEARCH_FILM)
+ .payload(payload)
+ .build();
+
+ assertEquals("流浪地球", cmd.getPayloadValue("filmName"));
+ assertEquals("吴京", cmd.getPayloadValue("actorName"));
+ }
+
+ @Test
+ public void 控制命令应当携带action与seconds与index() {
+ Map payload = new HashMap<>();
+ payload.put("action", "SEEK_FORWARD");
+ payload.put("seconds", "10");
+
+ VoiceCommand cmd = VoiceCommand.builder()
+ .commandType(VoiceCommandType.PLAYBACK_SEEK_FORWARD)
+ .payload(payload)
+ .build();
+
+ assertEquals("SEEK_FORWARD", cmd.getPayloadValue("action"));
+ assertEquals("10", cmd.getPayloadValue("seconds"));
+ }
+
+ @Test
+ public void payload应当支持put与get() {
+ VoiceCommand cmd = VoiceCommand.builder()
+ .commandType(VoiceCommandType.GO_HOME)
+ .build();
+ assertNotNull(cmd.getPayload());
+
+ cmd.putPayload("k1", "v1");
+ assertEquals("v1", cmd.getPayloadValue("k1"));
+ assertTrue(cmd.getPayload().containsKey("k1"));
+ }
+
+ @Test
+ public void 同类命令应当支持按commandType比较() {
+ VoiceCommand a = VoiceCommand.builder().commandType(VoiceCommandType.PLAYBACK_PAUSE).build();
+ VoiceCommand b = VoiceCommand.builder().commandType(VoiceCommandType.PLAYBACK_PAUSE).build();
+ VoiceCommand c = VoiceCommand.builder().commandType(VoiceCommandType.PLAYBACK_RESUME).build();
+
+ assertTrue(a.isSameThrottleKey(b));
+ assertFalse(a.isSameThrottleKey(c));
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceProtocolConstantsTest.java b/app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceProtocolConstantsTest.java
new file mode 100644
index 0000000..8e9f333
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceProtocolConstantsTest.java
@@ -0,0 +1,66 @@
+package com.pukka.jiecheng.voice.protocol;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * 协议常量测试。
+ *
+ * 覆盖可行性评估文档 §6.1 协议定义:action / permission / component / extras 名称。
+ *
+ */
+public class VoiceProtocolConstantsTest {
+
+ @Test
+ public void 协议广播action应当与文档一致() {
+ assertEquals("com.pukka.jiecheng.action.VOICE_COMMAND",
+ VoiceProtocolConstants.ACTION_VOICE_COMMAND);
+ }
+
+ @Test
+ public void 协议permission应当与文档一致() {
+ assertEquals("com.pukka.jiecheng.permission.VOICE_COMMAND",
+ VoiceProtocolConstants.PERMISSION_VOICE_COMMAND);
+ }
+
+ @Test
+ public void 接收器组件路径应当与测试脚本一致() {
+ assertEquals("com.pukka.jiecheng/.voice.receiver.VoiceCommandReceiver",
+ VoiceProtocolConstants.RECEIVER_COMPONENT);
+ }
+
+ @Test
+ public void 包名应当与common脚本默认值一致() {
+ assertEquals("com.pukka.jiecheng", VoiceProtocolConstants.APP_PACKAGE);
+ }
+
+ @Test
+ public void 第三方回传日志tag应当固定() {
+ assertEquals("VoiceCommand", VoiceProtocolConstants.LOG_TAG);
+ }
+
+ @Test
+ public void extras字段名应使用文档约定() {
+ assertEquals("requestId", VoiceProtocolConstants.EXTRA_REQUEST_ID);
+ assertEquals("sourceApp", VoiceProtocolConstants.EXTRA_SOURCE_APP);
+ assertEquals("sourcePlatform", VoiceProtocolConstants.EXTRA_SOURCE_PLATFORM);
+ assertEquals("commandType", VoiceProtocolConstants.EXTRA_COMMAND_TYPE);
+ assertEquals("payload", VoiceProtocolConstants.EXTRA_PAYLOAD);
+ assertEquals("callbackAction", VoiceProtocolConstants.EXTRA_CALLBACK_ACTION);
+ assertEquals("timestamp", VoiceProtocolConstants.EXTRA_TIMESTAMP);
+ }
+
+ @Test
+ public void 同名字符串引用应指向同一常量() {
+ assertEquals(VoiceProtocolConstants.ACTION_VOICE_COMMAND,
+ VoiceProtocolConstants.ACTION_VOICE_COMMAND);
+ assertNotEquals(VoiceProtocolConstants.ACTION_VOICE_COMMAND,
+ VoiceProtocolConstants.PERMISSION_VOICE_COMMAND);
+ assertFalse(VoiceProtocolConstants.EXTRA_PAYLOAD.isEmpty());
+ assertTrue(VoiceProtocolConstants.EXTRA_COMMAND_TYPE.length() > 0);
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/pukka/jiecheng/voice/queue/CommandQueueTest.java b/app/src/test/java/com/pukka/jiecheng/voice/queue/CommandQueueTest.java
new file mode 100644
index 0000000..07e8e73
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/voice/queue/CommandQueueTest.java
@@ -0,0 +1,92 @@
+package com.pukka.jiecheng.voice.queue;
+
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.protocol.VoiceCommandType;
+
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * CommandQueue 单测(文档 §7.4)。
+ *
+ * FIFO 容量上限测试使用 5 个 VT-0..VT-4 入队,容量 3 意味着保留最新的
+ * VT-2 / VT-3 / VT-4(FIFO 抛弃最早)。
+ *
+ */
+public class CommandQueueTest {
+
+ @Test
+ public void 入队顺序应被保留() {
+ CommandQueue queue = new CommandQueue();
+
+ VoiceCommand a = cmd(VoiceCommandType.OPEN_FILM_DETAIL, "vt-001");
+ VoiceCommand b = cmd(VoiceCommandType.OPEN_SEARCH, "vt-002");
+ VoiceCommand c = cmd(VoiceCommandType.PLAYBACK_PAUSE, "vt-003");
+
+ queue.enqueue(a);
+ queue.enqueue(b);
+ queue.enqueue(c);
+
+ List snapshot = queue.snapshot();
+ assertEquals(3, snapshot.size());
+ assertEquals("vt-001", snapshot.get(0).getRequestId());
+ assertEquals("vt-002", snapshot.get(1).getRequestId());
+ assertEquals("vt-003", snapshot.get(2).getRequestId());
+ }
+
+ @Test
+ public void drainAll应返回并清空全部命令() {
+ CommandQueue queue = new CommandQueue();
+ queue.enqueue(cmd(VoiceCommandType.GO_HOME, "vt-001"));
+ queue.enqueue(cmd(VoiceCommandType.GO_HOME, "vt-002"));
+
+ List drained = queue.drainAll();
+ assertEquals(2, drained.size());
+ assertEquals(0, queue.size());
+ assertTrue(queue.isEmpty());
+ }
+
+ @Test
+ public void 空队列drainAll应返回空列表() {
+ CommandQueue queue = new CommandQueue();
+ List drained = queue.drainAll();
+ assertNotNull(drained);
+ assertEquals(0, drained.size());
+ }
+
+ @Test
+ public void 容量上限应被尊重且保留最新条目() {
+ CommandQueue queue = new CommandQueue(3);
+ for (int i = 0; i < 5; i++) {
+ queue.enqueue(cmd(VoiceCommandType.GO_HOME, "vt-" + i));
+ }
+ assertEquals(3, queue.size());
+ List snapshot = queue.snapshot();
+ // 容量超限后 FIFO 抛弃最旧的两条,保留 vt-2 / vt-3 / vt-4
+ assertEquals("vt-2", snapshot.get(0).getRequestId());
+ assertEquals("vt-3", snapshot.get(1).getRequestId());
+ assertEquals("vt-4", snapshot.get(2).getRequestId());
+ }
+
+ @Test
+ public void peek应不移除队首() {
+ CommandQueue queue = new CommandQueue();
+ queue.enqueue(cmd(VoiceCommandType.GO_HOME, "vt-001"));
+
+ VoiceCommand peeked = queue.peek();
+ assertEquals("vt-001", peeked.getRequestId());
+ assertEquals(1, queue.size());
+ }
+
+ private VoiceCommand cmd(VoiceCommandType type, String requestId) {
+ return VoiceCommand.builder()
+ .commandType(type)
+ .requestId(requestId)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/pukka/jiecheng/voice/router/CommandRouterTest.java b/app/src/test/java/com/pukka/jiecheng/voice/router/CommandRouterTest.java
new file mode 100644
index 0000000..ada9152
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/voice/router/CommandRouterTest.java
@@ -0,0 +1,272 @@
+package com.pukka.jiecheng.voice.router;
+
+import com.pukka.jiecheng.voice.guard.AppStateGuard;
+import com.pukka.jiecheng.voice.protocol.VoiceCallbackResult;
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.protocol.VoiceCommandType;
+import com.pukka.jiecheng.voice.router.CommandRouter.RouteResult;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * CommandRouter 单测。
+ */
+public class CommandRouterTest {
+
+ private FakePageNavigator navigator;
+ private FakePlaybackBridge bridge;
+ private FakeAppStateGuard guard;
+ private CommandRouter router;
+
+ @Before
+ public void setUp() {
+ navigator = new FakePageNavigator();
+ bridge = new FakePlaybackBridge();
+ guard = new FakeAppStateGuard();
+ router = new CommandRouter(navigator, bridge, guard);
+ }
+
+ @Test
+ public void 守卫拒绝时不应执行任何导航或桥接() {
+ guard.setActivated(false);
+ VoiceCommand cmd = cmd(VoiceCommandType.OPEN_FILM_DETAIL, "vt-001");
+
+ RouteResult result = router.route(cmd);
+
+ assertFalse(result.isSuccess());
+ assertEquals(VoiceCallbackResult.CODE_DEVICE_NOT_ACTIVATED, result.getResult().getResultCode());
+ assertEquals(0, navigator.getJumpCount());
+ assertEquals(0, bridge.getInvokeCount());
+ }
+
+ @Test
+ public void 单片跳转在守卫通过时应调用navigator_openFilm() {
+ guard.setActivated(true);
+ Map payload = new HashMap<>();
+ payload.put("filmId", "film-001");
+ payload.put("filmName", "测试片");
+ payload.put("autoPlay", "true");
+
+ VoiceCommand cmd = VoiceCommand.builder()
+ .commandType(VoiceCommandType.OPEN_FILM_DETAIL)
+ .requestId("vt-001")
+ .payload(payload)
+ .build();
+
+ RouteResult result = router.route(cmd);
+ assertTrue(result.isSuccess());
+ assertEquals(1, navigator.getJumpCount());
+ assertEquals("film-001", navigator.getLastFilmId());
+ assertEquals(0, bridge.getInvokeCount());
+ }
+
+ @Test
+ public void 分类跳转应调用navigator_openCategory() {
+ guard.setActivated(true);
+ Map payload = new HashMap<>();
+ payload.put("categoryId", "cat-action");
+ payload.put("categoryName", "动作片");
+
+ VoiceCommand cmd = VoiceCommand.builder()
+ .commandType(VoiceCommandType.OPEN_CATEGORY)
+ .requestId("vt-002")
+ .payload(payload)
+ .build();
+
+ RouteResult result = router.route(cmd);
+ assertTrue(result.isSuccess());
+ assertEquals("cat-action", navigator.getLastCategoryId());
+ assertEquals("动作片", navigator.getLastCategoryName());
+ }
+
+ @Test
+ public void 返回首页应调用navigator_goHome() {
+ guard.setActivated(true);
+ RouteResult result = router.route(cmd(VoiceCommandType.GO_HOME, "vt-003"));
+ assertTrue(result.isSuccess());
+ assertEquals(1, navigator.getHomeCount());
+ }
+
+ @Test
+ public void 搜索命令应调用navigator_openSearch() {
+ guard.setActivated(true);
+ Map payload = new HashMap<>();
+ payload.put("filmName", "流浪地球");
+
+ VoiceCommand cmd = VoiceCommand.builder()
+ .commandType(VoiceCommandType.SEARCH_FILM)
+ .requestId("vt-004")
+ .payload(payload)
+ .build();
+
+ RouteResult result = router.route(cmd);
+ assertTrue(result.isSuccess());
+ assertEquals("流浪地球", navigator.getLastSearchKeyword());
+ }
+
+ @Test
+ public void 播放控制命令应调用bridge() {
+ guard.setActivated(true);
+ bridge.setAvailable(true);
+
+ RouteResult result = router.route(cmd(VoiceCommandType.PLAYBACK_PAUSE, "vt-005"));
+ assertTrue(result.isSuccess());
+ assertEquals(1, bridge.getInvokeCount());
+ assertEquals(VoiceCommandType.PLAYBACK_PAUSE, bridge.getLastCommand());
+ }
+
+ @Test
+ public void 播放控制命令在bridge不可用时应返回失败() {
+ guard.setActivated(true);
+ bridge.setAvailable(false);
+
+ RouteResult result = router.route(cmd(VoiceCommandType.PLAYBACK_PAUSE, "vt-006"));
+ assertFalse(result.isSuccess());
+ assertEquals(VoiceCallbackResult.CODE_NOT_IN_PLAYER, result.getResult().getResultCode());
+ }
+
+ @Test
+ public void 跳转时正在播放应先退出播放态() {
+ guard.setActivated(true);
+ bridge.setAvailable(true);
+ navigator.setPlaying(true);
+
+ Map payload = new HashMap<>();
+ payload.put("filmId", "f2");
+ VoiceCommand cmd = VoiceCommand.builder()
+ .commandType(VoiceCommandType.OPEN_FILM_DETAIL)
+ .requestId("vt-007")
+ .payload(payload)
+ .build();
+
+ RouteResult result = router.route(cmd);
+ assertTrue(result.isSuccess());
+ assertTrue(bridge.wasExitPlaybackCalled());
+ assertEquals(1, navigator.getJumpCount());
+ }
+
+ @Test
+ public void 未支持的commandType应返回invalidPayload() {
+ guard.setActivated(true);
+ VoiceCommand cmd = cmd(VoiceCommandType.OPEN_ACTOR, "vt-010");
+ RouteResult result = router.route(cmd);
+ assertFalse(result.isSuccess());
+ assertEquals(VoiceCallbackResult.CODE_INVALID_PAYLOAD, result.getResult().getResultCode());
+ }
+
+ @Test
+ public void 路由结果应携带原requestId() {
+ guard.setActivated(true);
+ RouteResult result = router.route(cmd(VoiceCommandType.GO_HOME, "vt-trace"));
+ assertEquals("vt-trace", result.getResult().getRequestId());
+ }
+
+ @Test
+ public void sanity() {
+ assertNotNull(router);
+ }
+
+ private VoiceCommand cmd(VoiceCommandType type, String requestId) {
+ return VoiceCommand.builder()
+ .commandType(type)
+ .requestId(requestId)
+ .build();
+ }
+
+ // ---- Fake dependencies ----
+
+ static class FakePageNavigator implements PageNavigator {
+ int jumpCount = 0;
+ int homeCount = 0;
+ String lastFilmId;
+ String lastCategoryId;
+ String lastCategoryName;
+ String lastSearchKeyword;
+ boolean playing;
+ AtomicReference lastResult = new AtomicReference<>();
+
+ void setPlaying(boolean playing) { this.playing = playing; }
+
+ int getJumpCount() { return jumpCount; }
+ int getHomeCount() { return homeCount; }
+ String getLastFilmId() { return lastFilmId; }
+ String getLastCategoryId() { return lastCategoryId; }
+ String getLastCategoryName() { return lastCategoryName; }
+ String getLastSearchKeyword() { return lastSearchKeyword; }
+
+ @Override public void openFilmDetail(VoiceCommand cmd) {
+ jumpCount++;
+ lastFilmId = cmd.getPayloadValue("filmId");
+ lastResult.set(RouteResult.ok(cmd.getRequestId()));
+ }
+ @Override public void openCategory(VoiceCommand cmd) {
+ jumpCount++;
+ lastCategoryId = cmd.getPayloadValue("categoryId");
+ lastCategoryName = cmd.getPayloadValue("categoryName");
+ lastResult.set(RouteResult.ok(cmd.getRequestId()));
+ }
+ @Override public void openSearch(VoiceCommand cmd) {
+ jumpCount++;
+ lastSearchKeyword = cmd.getPayloadValue("filmName");
+ lastResult.set(RouteResult.ok(cmd.getRequestId()));
+ }
+ @Override public void goHome(VoiceCommand cmd) {
+ homeCount++;
+ lastResult.set(RouteResult.ok(cmd.getRequestId()));
+ }
+ @Override public boolean isPlaying() {
+ return playing;
+ }
+ @Override public void exitPlaybackIfNeeded() {
+ // 模拟退出播放
+ }
+ }
+
+ static class FakePlaybackBridge implements PlaybackControlBridge {
+ boolean available = false;
+ AtomicInteger invokeCount = new AtomicInteger();
+ AtomicReference lastCommand = new AtomicReference<>();
+ boolean exitPlaybackCalled = false;
+
+ void setAvailable(boolean available) { this.available = available; }
+ int getInvokeCount() { return invokeCount.get(); }
+ VoiceCommandType getLastCommand() { return lastCommand.get(); }
+ boolean wasExitPlaybackCalled() { return exitPlaybackCalled; }
+
+ @Override public boolean isAvailable() {
+ return available;
+ }
+ @Override public void dispatch(VoiceCommand cmd) {
+ invokeCount.incrementAndGet();
+ lastCommand.set(cmd.getCommandType());
+ }
+ @Override public void exitPlayback() {
+ exitPlaybackCalled = true;
+ }
+ }
+
+ static class FakeAppStateGuard implements AppStateGuard {
+ boolean activated = true;
+
+ void setActivated(boolean v) { this.activated = v; }
+
+ @Override public boolean isActivated() {
+ return activated;
+ }
+
+ @Override public boolean hasOpenDialog() {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/pukka/jiecheng/voice/throttle/CommandThrottleTest.java b/app/src/test/java/com/pukka/jiecheng/voice/throttle/CommandThrottleTest.java
new file mode 100644
index 0000000..301f5a1
--- /dev/null
+++ b/app/src/test/java/com/pukka/jiecheng/voice/throttle/CommandThrottleTest.java
@@ -0,0 +1,128 @@
+package com.pukka.jiecheng.voice.throttle;
+
+import com.pukka.jiecheng.voice.protocol.VoiceCommand;
+import com.pukka.jiecheng.voice.protocol.VoiceCommandType;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * CommandThrottle 单测(文档 §7.5)。
+ */
+public class CommandThrottleTest {
+
+ private FakeClock clock;
+ private CommandThrottle throttle;
+
+ @Before
+ public void setUp() {
+ clock = new FakeClock(0L);
+ throttle = new CommandThrottle(2000L, clock);
+ }
+
+ @Test
+ public void 同类命令2秒内应被新命令替换旧命令() {
+ VoiceCommand old = cmd(VoiceCommandType.PLAYBACK_PAUSE, "vt-001");
+ throttle.accept(old);
+
+ clock.advance(500L);
+ VoiceCommand recent = cmd(VoiceCommandType.PLAYBACK_PAUSE, "vt-002");
+
+ VoiceCommand merged = throttle.accept(recent);
+ assertSame(recent, merged);
+
+ VoiceCommand drained = throttle.drain();
+ assertSame(recent, drained);
+ assertNull(throttle.drain());
+ }
+
+ @Test
+ public void 超过2秒窗口后旧节流记录应当清空() {
+ throttle.accept(cmd(VoiceCommandType.PLAYBACK_PAUSE, "vt-001"));
+
+ clock.advance(2500L);
+ VoiceCommand fresh = cmd(VoiceCommandType.PLAYBACK_PAUSE, "vt-002");
+ VoiceCommand merged = throttle.accept(fresh);
+ assertSame(fresh, merged);
+
+ VoiceCommand drained = throttle.drain();
+ assertSame(fresh, drained);
+ assertNull(throttle.drain());
+ }
+
+ @Test
+ public void 不同commandType互不影响节流() {
+ VoiceCommand pause = cmd(VoiceCommandType.PLAYBACK_PAUSE, "vt-001");
+ VoiceCommand resume = cmd(VoiceCommandType.PLAYBACK_RESUME, "vt-002");
+
+ throttle.accept(pause);
+ clock.advance(300L);
+ VoiceCommand merged = throttle.accept(resume);
+
+ assertSame(resume, merged);
+
+ VoiceCommand first = throttle.drain();
+ VoiceCommand second = throttle.drain();
+ assertNotNull(first);
+ assertNotNull(second);
+ assertFalse(first == second);
+ }
+
+ @Test
+ public void 节流判应当只判同类命令() {
+ assertTrue(CommandThrottle.isSameThrottleKey(
+ VoiceCommandType.PLAYBACK_PAUSE, VoiceCommandType.PLAYBACK_PAUSE));
+ assertFalse(CommandThrottle.isSameThrottleKey(
+ VoiceCommandType.PLAYBACK_PAUSE, VoiceCommandType.PLAYBACK_RESUME));
+ assertFalse(CommandThrottle.isSameThrottleKey(
+ VoiceCommandType.OPEN_FILM_DETAIL, VoiceCommandType.PLAYBACK_PAUSE));
+ }
+
+ @Test
+ public void drain应按accept时间倒序返回命令() {
+ throttle.accept(cmd(VoiceCommandType.PLAYBACK_PAUSE, "vt-001"));
+ clock.advance(500L);
+ throttle.accept(cmd(VoiceCommandType.PLAYBACK_RESUME, "vt-002"));
+ clock.advance(3000L);
+
+ VoiceCommand first = throttle.drain();
+ assertEquals("vt-002", first.getRequestId());
+
+ VoiceCommand second = throttle.drain();
+ assertEquals("vt-001", second.getRequestId());
+
+ assertNull(throttle.drain());
+ }
+
+ private VoiceCommand cmd(VoiceCommandType type, String requestId) {
+ return VoiceCommand.builder()
+ .commandType(type)
+ .requestId(requestId)
+ .build();
+ }
+
+ /** 假时钟:测试期可控推进。 */
+ static class FakeClock implements CommandThrottleClock {
+ private long now;
+
+ FakeClock(long start) {
+ this.now = start;
+ }
+
+ void advance(long ms) {
+ now += ms;
+ }
+
+ @Override
+ public long now() {
+ return now;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/thirdparty/res/values/strings.xml b/app/src/thirdparty/res/values/strings.xml
deleted file mode 100644
index d3b99d2..0000000
--- a/app/src/thirdparty/res/values/strings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- 橘视厅
-
diff --git a/build.gradle b/build.gradle
old mode 100755
new mode 100644
diff --git a/build_channels.bat b/build_channels.bat
old mode 100755
new mode 100644
diff --git a/config.gradle b/config.gradle
old mode 100755
new mode 100644
index db06180..a5ffaa8
--- a/config.gradle
+++ b/config.gradle
@@ -1,7 +1,7 @@
ext {
app_android = [
compileSdkVersion: 34,
- minSdkVersion : 17, // Android 4.2 兼容(探测编译,待验证)
+ minSdkVersion : 19,
targetSdkVersion : 29,
multiDexEnabled : true
]
diff --git a/day.code-workspace b/day.code-workspace
deleted file mode 100644
index f1d6e8a..0000000
--- a/day.code-workspace
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "folders": [
- {
- "path": "../../Desktop/tv.huashi.jetsenx.jetsenfilx.jsttvday/day"
- },
- {
- "path": "."
- }
- ],
- "settings": {}
-}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
old mode 100755
new mode 100644
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
old mode 100755
new mode 100644
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
old mode 100755
new mode 100644
diff --git a/gradlew b/gradlew
old mode 100755
new mode 100644
diff --git a/gradlew.bat b/gradlew.bat
old mode 100755
new mode 100644
diff --git a/key/jiecheng.jks b/key/jiecheng.jks
old mode 100755
new mode 100644
diff --git a/key/keystore.properties b/key/keystore.properties
old mode 100755
new mode 100644
diff --git a/libcore/.gitignore b/libcore/.gitignore
old mode 100755
new mode 100644
diff --git a/libcore/build.gradle b/libcore/build.gradle
old mode 100755
new mode 100644
diff --git a/libcore/consumer-rules.pro b/libcore/consumer-rules.pro
old mode 100755
new mode 100644
diff --git a/libcore/proguard-rules.pro b/libcore/proguard-rules.pro
old mode 100755
new mode 100644
diff --git a/libcore/src/androidTest/java/com/pukka/libcore/ExampleInstrumentedTest.java b/libcore/src/androidTest/java/com/pukka/libcore/ExampleInstrumentedTest.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/AndroidManifest.xml b/libcore/src/main/AndroidManifest.xml
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/base/BaseConst.java b/libcore/src/main/java/com/pukka/libcore/base/BaseConst.java
old mode 100755
new mode 100644
index d666d42..3b7621e
--- a/libcore/src/main/java/com/pukka/libcore/base/BaseConst.java
+++ b/libcore/src/main/java/com/pukka/libcore/base/BaseConst.java
@@ -14,14 +14,14 @@ public class BaseConst {
public static final String header = "huashitv";
- //渠道code
- public static String agentVendorCode = "1100101010208919998410060";
+ //渠道code(橘视厅5.1环绕声业务渠道,与 ChannelManger.initChannel 保持一致)
+ public static String agentVendorCode = "1100101010200492358120169";
//业务组合code
- public static String serviceComboCode = "1100121010208920760580061";
+ public static String serviceComboCode = "1100121044377892530980001";
//业务分组code
- public static String serviceGroupCode = "1100122010208920968780062";
+ public static String serviceGroupCode = "1100122010201752209710182";
//业务线参数
- public static String businessLine = "4100450011967708592750001";
+ public static String businessLine = "4100450044057893707260002";
public static String getMemberCode(String token){
diff --git a/libcore/src/main/java/com/pukka/libcore/base/BaseUrl.java b/libcore/src/main/java/com/pukka/libcore/base/BaseUrl.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/base/BuildConfig.java b/libcore/src/main/java/com/pukka/libcore/base/BuildConfig.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/base/DifferenceApi.java b/libcore/src/main/java/com/pukka/libcore/base/DifferenceApi.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/base/DifferenceUrl.java b/libcore/src/main/java/com/pukka/libcore/base/DifferenceUrl.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/boxShared/BoxSharedParameters.java b/libcore/src/main/java/com/pukka/libcore/boxShared/BoxSharedParameters.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/boxShared/BoxSharedParamsUtils.java b/libcore/src/main/java/com/pukka/libcore/boxShared/BoxSharedParamsUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/boxShared/IpUtils.java b/libcore/src/main/java/com/pukka/libcore/boxShared/IpUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/boxShared/MacUtils.java b/libcore/src/main/java/com/pukka/libcore/boxShared/MacUtils.java
old mode 100755
new mode 100644
index 4c9b768..3e35b97
--- a/libcore/src/main/java/com/pukka/libcore/boxShared/MacUtils.java
+++ b/libcore/src/main/java/com/pukka/libcore/boxShared/MacUtils.java
@@ -1,27 +1,13 @@
package com.pukka.libcore.boxShared;
import android.content.Context;
-import android.content.SharedPreferences;
-import android.net.wifi.WifiInfo;
-import android.net.wifi.WifiManager;
-import android.os.Build;
import android.text.TextUtils;
-import android.widget.Toast;
import com.pukka.libcore.utils.LogUtils;
import com.pukka.libcore.utils.SharedPreferenceUtils;
import com.pukka.libcore.utils.StringUtil;
import com.pukka.libcore.utils.StringUtils;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.LineNumberReader;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.util.Enumeration;
import java.util.Locale;
/**
@@ -31,226 +17,70 @@ import java.util.Locale;
*/
public class MacUtils {
/**
- * 获取mac地址(适配所有Android版本)
+ * 获取 mac 地址(适配所有 Android 版本)
+ * 优先取 ETH_MAC 缓存,再读设备;都拿不到就如实返回空字符串,
+ * 不再伪造(伪造 MAC 在服务端设备库里匹配不到,行为更糟)。
*
* @param context 上下文
- * @return mac地址
+ * @return mac地址;拿不到时返回 ""
*/
public static String getMac(Context context) {
-// String mac = getMacAddressIP();
-// if (TextUtils.isEmpty(mac)) {
-// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-// mac = getMacDefault(context);
-// } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
-// mac = getMacAddress();
-// } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-// mac = getMacFromHardware();
-// }
-// }
- // if (TextUtils.isEmpty(mac)){
- String mac = SharedPreferenceUtils.getString("ETH_MAC","");
- LogUtils.d("从APK缓存中获取有线mac="+mac);
- if(StringUtils.isEmpty(mac)) {
- //安柠获取mac的方法
+ // 1) 优先从缓存取(有线 MAC 第一次成功读取后会写到这里)
+ String mac = SharedPreferenceUtils.getString("ETH_MAC", "");
+ LogUtils.d("从APK缓存中获取有线mac=" + mac);
+ if (StringUtils.isEmpty(mac)) {
+ // 2) 缓存未命中,按多路策略读设备
mac = StringUtil.getLocalMac();
- LogUtils.d("从设备中获取mac="+mac);
- }
- if(StringUtils.isEmpty(mac)) {
- //自己生成mac的方法
- mac = MacUtils2.getMAC(context);
- LogUtils.d("从设备中自生成mac="+mac);
+ LogUtils.d("从设备中获取mac=" + mac);
}
return mac;
- //}
- //return mac;
}
+ /**
+ * 读 /sys/class/net/wlan0/address,走 shell cat 以绕过 SELinux 对 Java 直读 sysfs 的拒绝。
+ * 循环读满直到 EOF,避免单次 read(buffer) 不保证读满的坑。
+ *
+ * @return wlan0 MAC;读不到返回 ""
+ */
public static String getWlan0Mac() {
- String mac = "";
- try {
- Process p = Runtime.getRuntime().exec("cat /sys/class/net/wlan0/address");
- InputStream is = p.getInputStream();
- InputStreamReader isr = new InputStreamReader(is);
- BufferedReader bf = new BufferedReader(isr);
- String line = "";
- if ((line = bf.readLine()) != null) {
- mac = line;
- }
- bf.close();
- isr.close();
- is.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return mac.toUpperCase();
+ // 复用 StringUtil.readSysFsMac,仅末尾大小写归一化与本方法旧行为一致
+ return StringUtil.readSysFsMac("/sys/class/net/wlan0/address").toUpperCase(Locale.ENGLISH);
}
/**
- * Android 6.0 之前(不包括6.0)获取mac地址
- * 必须的权限
+ * 统一封装:拿不到有线 MAC 时以 WifiMAC 为主(两者相同),避免下游出现"伪 mac"。
+ * 返回 String[2] = {mac, wifiMac}:
+ *
+ * - 有线拿到了:mac = 有线MAC,wifiMac = wlan0MAC(两者可能不同——双网卡设备)
+ * - 有线没拿到、wifi 拿到了:mac = wlan0MAC,wifiMac = wlan0MAC(两者完全一致)
+ * - 都拿不到:两者均为 ""
+ *
+ * 当两者真值相同(wifi-only 设备)但大小写不同时,统一成大写——便于后端按字符串比对 MAC。
+ * 双网卡设备上两者来自不同接口(有线 ≠ 无线),保留各自原值不强制统一。
*
- * @param context * @return
+ * @param context 上下文
+ * @return 长度固定为 2 的数组:[mac, wifiMac]
*/
- private static String getMacDefault(Context context) {
- String mac = "";
- if (context == null) {
- return mac;
+ public static String[] getDeviceMacPair(Context context) {
+ // 独立取两条路径,避免继承 getMac() 内部任何兜底行为
+ String ethMac = getMac(context);
+ String wifiMac = getWlan0Mac();
+ // 拿不到有线时以 wifi 为主(两者相同)
+ if (TextUtils.isEmpty(ethMac) && !TextUtils.isEmpty(wifiMac)) {
+ ethMac = wifiMac;
}
- WifiManager wifi = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
- WifiInfo info = null;
- try {
- info = wifi.getConnectionInfo();
- } catch (Exception e) {
- e.printStackTrace();
+ // 拿不到 wifi 时以有线为主(双向兜底,保持两者一致)
+ if (TextUtils.isEmpty(wifiMac) && !TextUtils.isEmpty(ethMac)) {
+ wifiMac = ethMac;
}
-
- if (info == null) {
- return null;
+ // 两者都非空且真值相同(wifi-only 设备的常见场景),统一大小写
+ // 双网卡设备上两者来源不同,equalsIgnoreCase 不命中,保留各自原值
+ if (!TextUtils.isEmpty(ethMac) && !TextUtils.isEmpty(wifiMac)
+ && ethMac.equalsIgnoreCase(wifiMac)) {
+ String canonical = ethMac.toUpperCase(Locale.ENGLISH);
+ ethMac = canonical;
+ wifiMac = canonical;
}
- mac = info.getMacAddress();
- if (!TextUtils.isEmpty(mac)) {
- mac = mac.toUpperCase(Locale.ENGLISH);
- }
- return mac;
- }
-
- /**
- * Android 6.0-Android 7.0 获取mac地址
- */
- private static String getMacAddress() {
- String macSerial = null;
- String str = "";
-
- try {
- Process pp = Runtime.getRuntime().exec("cat /sys/class/net/wlan0/address");
- InputStreamReader ir = new InputStreamReader(pp.getInputStream());
- LineNumberReader input = new LineNumberReader(ir);
-
- while (null != str) {
- str = input.readLine();
- if (str != null) {
- macSerial = str.trim();//去空格
- break;
- }
- }
- } catch (IOException ex) {
- // 赋予默认值
- ex.printStackTrace();
- }
-
- return macSerial;
- }
-
- /**
- * Android 7.0之后获取Mac地址
- * 遍历循环所有的网络接口,找到接口是 wlan0
- * 必须的权限
- *
- * @return
- */
- private static String getMacFromHardware() {
- Enumeration interfaces = null;
- try {
- interfaces = NetworkInterface.getNetworkInterfaces();
- } catch (SocketException e) {
- e.printStackTrace();
- }
- String hardWareAddress = null;
- NetworkInterface iF = null;
- if (interfaces == null) {
- return null;
- }
- while (interfaces.hasMoreElements()) {
- iF = interfaces.nextElement();
- try {
- hardWareAddress = bytesToString(iF.getHardwareAddress());
- if (hardWareAddress != null) {
- break;
- }
- } catch (SocketException e) {
- e.printStackTrace();
- }
- }
- return hardWareAddress;
- }
-
- /**
- * Android 6.0 之前(不包括6.0)
- * 获取有线网络mac
- *
- * @return 有线网络mac
- */
- private static String getMacAddressIP() {
- String strMacAddr = null;
- try {
- InetAddress ip = getLocalInetAddress();
- byte[] b = NetworkInterface.getByInetAddress(ip)
- .getHardwareAddress();
- StringBuffer buffer = new StringBuffer();
- if(b != null) {
- for (int i = 0; i < b.length; i++) {
- if (i != 0) {
- buffer.append(':');
- }
- String str = Integer.toHexString(b[i] & 0xFF);
- buffer.append(str.length() == 1 ? 0 + str : str);
- }
- }
- strMacAddr = buffer.toString().toUpperCase();
- } catch (Exception e) {
- e.printStackTrace();
- }
- return strMacAddr;
- }
-
- /***
- * byte转为String
- *
- * @param bytes
- * @return
- */
- private static String bytesToString(byte[] bytes) {
- if (bytes == null || bytes.length == 0) {
- return null;
- }
- StringBuilder buf = new StringBuilder();
- for (byte b : bytes) {
- buf.append(String.format("%02X:", b));
- }
- if (buf.length() > 0) {
- buf.deleteCharAt(buf.length() - 1);
- }
- return buf.toString();
- }
-
- /**
- * 获取本地IP
- *
- * @return
- */
- private static InetAddress getLocalInetAddress() {
- InetAddress ip = null;
- try {
- //列举
- Enumeration en_netInterface = NetworkInterface.getNetworkInterfaces();
- while (en_netInterface.hasMoreElements()) {//是否还有元素
- NetworkInterface ni = (NetworkInterface) en_netInterface.nextElement();//得到下一个元素
- Enumeration en_ip = ni.getInetAddresses();//得到一个ip地址的列举
- while (en_ip.hasMoreElements()) {
- ip = (InetAddress) en_ip.nextElement();
- if (!ip.isLoopbackAddress() && ip.getHostAddress().indexOf(":") == -1) {
- break;
- } else {
- ip = null;
- }
- }
- if (ip != null) {
- break;
- }
- }
- } catch (SocketException e) {
- e.printStackTrace();
- }
- return ip;
+ return new String[]{ethMac, wifiMac};
}
}
diff --git a/libcore/src/main/java/com/pukka/libcore/boxShared/MacUtils2.java b/libcore/src/main/java/com/pukka/libcore/boxShared/MacUtils2.java
deleted file mode 100755
index 04fa426..0000000
--- a/libcore/src/main/java/com/pukka/libcore/boxShared/MacUtils2.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.pukka.libcore.boxShared;
-
-import android.content.Context;
-import android.os.Build;
-import android.provider.Settings;
-import android.text.TextUtils;
-
-import com.pukka.libcore.utils.LogUtils;
-
-import java.security.MessageDigest;
-
-public class MacUtils2 {
-
- public static String getMAC(Context context) {
- // 获取基础信息
- String baseInfo = getBaseDeviceInfo(context);
- // 生成MAC格式字符串
- String macAddress = generateMacFromHash(baseInfo);
- LogUtils.i("MacUtils", "return Mac:" + macAddress);
- return macAddress;
- }
-
- public static String getBaseDeviceInfo(Context context) {
- // 获取设备基本信息
- String androidId = "";
- androidId = Settings.Secure.getString(
- context.getContentResolver(),
- Settings.Secure.ANDROID_ID
- );
- LogUtils.i("MacUtils", "androidId:" + androidId);
- String deviceInfo = Build.BRAND
- + Build.DEVICE
- + Build.MANUFACTURER
- + Build.MODEL;
- LogUtils.i("MacUtils", "BRAND:" + Build.BRAND);
- LogUtils.i("MacUtils", "DEVICE:" + Build.DEVICE);
- LogUtils.i("MacUtils", "MANUFACTURER:" + Build.MANUFACTURER);
- LogUtils.i("MacUtils", "MODEL:" + Build.MODEL);
- return androidId + deviceInfo;
- }
-
- public static String getModel() {
- LogUtils.i("getModel", Build.BRAND + " " + Build.MODEL);
- return Build.BRAND + " " + Build.MODEL;
- }
-
- private static String generateMacFromHash(String input) {
- LogUtils.i("MacUtils", "generateMacFromHash:" + input);
- try {
- // 生成MD5
- MessageDigest md = MessageDigest.getInstance("MD5");
- byte[] hash = md.digest(input.getBytes());
-
- // 取前6个字节构造MAC
- StringBuilder mac = new StringBuilder();
- for (int i = 0; i < 6; i++) {
- if (i > 0) mac.append(":");
- mac.append(String.format("%02X", hash[i] & 0xFF));
- }
-
- // 确保第一个字节是偶数(本地管理的MAC地址)
- char firstChar = mac.charAt(0);
- if ((Character.digit(firstChar, 16) & 0x1) != 0) {
- mac.setCharAt(0, (char) (firstChar - 1));
- }
-
- return mac.toString();
- } catch (Exception e) {
- e.printStackTrace();
- return "02:00:00:00:00:00"; // 默认值
- }
- }
-}
diff --git a/libcore/src/main/java/com/pukka/libcore/cipher/AesUtils.java b/libcore/src/main/java/com/pukka/libcore/cipher/AesUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/cipher/HMacShaUtils.java b/libcore/src/main/java/com/pukka/libcore/cipher/HMacShaUtils.java
old mode 100755
new mode 100644
index f91ff9f..0c895a0
--- a/libcore/src/main/java/com/pukka/libcore/cipher/HMacShaUtils.java
+++ b/libcore/src/main/java/com/pukka/libcore/cipher/HMacShaUtils.java
@@ -1,6 +1,6 @@
package com.pukka.libcore.cipher;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@@ -29,7 +29,7 @@ public class HMacShaUtils {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA256_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
mac.init(signingKey);
- return mac.doFinal(data.getBytes(Charset.forName("UTF-8")));
+ return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
@@ -51,7 +51,7 @@ public class HMacShaUtils {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
- return mac.doFinal(data.getBytes(Charset.forName("UTF-8")));
+ return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
} catch (Exception ex) {
ex.printStackTrace();
}
diff --git a/libcore/src/main/java/com/pukka/libcore/cipher/Md5Utils.java b/libcore/src/main/java/com/pukka/libcore/cipher/Md5Utils.java
old mode 100755
new mode 100644
index 9370e89..6e612b6
--- a/libcore/src/main/java/com/pukka/libcore/cipher/Md5Utils.java
+++ b/libcore/src/main/java/com/pukka/libcore/cipher/Md5Utils.java
@@ -2,7 +2,7 @@ package com.pukka.libcore.cipher;
import com.pukka.libcore.utils.ByteConversionUtils;
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
/**
@@ -61,7 +61,7 @@ public class Md5Utils {
public static String encrypt(String message) {
String md5str = "";
try {
- md5str = encrypt(message.getBytes(Charset.forName("UTF-8")));
+ md5str = encrypt(message.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
e.printStackTrace();
}
diff --git a/libcore/src/main/java/com/pukka/libcore/fresco/BaseSimpleDraweeView.java b/libcore/src/main/java/com/pukka/libcore/fresco/BaseSimpleDraweeView.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/fresco/FrescoInit.java b/libcore/src/main/java/com/pukka/libcore/fresco/FrescoInit.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/fresco/IPkSimpleDraweeView.java b/libcore/src/main/java/com/pukka/libcore/fresco/IPkSimpleDraweeView.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/fresco/PkControllerListener.java b/libcore/src/main/java/com/pukka/libcore/fresco/PkControllerListener.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/fresco/PkSimpleDraweeListener.java b/libcore/src/main/java/com/pukka/libcore/fresco/PkSimpleDraweeListener.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/fresco/PkSimpleDraweeView.java b/libcore/src/main/java/com/pukka/libcore/fresco/PkSimpleDraweeView.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/fresco/Tls12SocketFactory.java b/libcore/src/main/java/com/pukka/libcore/fresco/Tls12SocketFactory.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/fresco/TrustAllManager.java b/libcore/src/main/java/com/pukka/libcore/fresco/TrustAllManager.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/retrofit/DefaultObserver.java b/libcore/src/main/java/com/pukka/libcore/retrofit/DefaultObserver.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/retrofit/ExceptionReason.java b/libcore/src/main/java/com/pukka/libcore/retrofit/ExceptionReason.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/retrofit/OkHttpClientUtil.java b/libcore/src/main/java/com/pukka/libcore/retrofit/OkHttpClientUtil.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/retrofit/PkApiService.java b/libcore/src/main/java/com/pukka/libcore/retrofit/PkApiService.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/retrofit/RetrofitApi.java b/libcore/src/main/java/com/pukka/libcore/retrofit/RetrofitApi.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/retrofit/lowplatform/AndroidPlatform9.java b/libcore/src/main/java/com/pukka/libcore/retrofit/lowplatform/AndroidPlatform9.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/retrofit/lowplatform/LowPlatformUtils.java b/libcore/src/main/java/com/pukka/libcore/retrofit/lowplatform/LowPlatformUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/retrofit/lowplatform/OptionalMethod.java b/libcore/src/main/java/com/pukka/libcore/retrofit/lowplatform/OptionalMethod.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/threading/ConcurrentDispatchQueue.java b/libcore/src/main/java/com/pukka/libcore/threading/ConcurrentDispatchQueue.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/threading/DispatchQueue.java b/libcore/src/main/java/com/pukka/libcore/threading/DispatchQueue.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/threading/DispatchQueueType.java b/libcore/src/main/java/com/pukka/libcore/threading/DispatchQueueType.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/threading/GuardedRunnable.java b/libcore/src/main/java/com/pukka/libcore/threading/GuardedRunnable.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/threading/SerialDispatchQueue.java b/libcore/src/main/java/com/pukka/libcore/threading/SerialDispatchQueue.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/threading/Timer.java b/libcore/src/main/java/com/pukka/libcore/threading/Timer.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/AppContext.java b/libcore/src/main/java/com/pukka/libcore/utils/AppContext.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/AppLifecycleObserver.java b/libcore/src/main/java/com/pukka/libcore/utils/AppLifecycleObserver.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/AppManager.java b/libcore/src/main/java/com/pukka/libcore/utils/AppManager.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/AppUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/AppUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/Base64Decoder.java b/libcore/src/main/java/com/pukka/libcore/utils/Base64Decoder.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/Base64Util.java b/libcore/src/main/java/com/pukka/libcore/utils/Base64Util.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/ByteConversionUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/ByteConversionUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/ContextUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/ContextUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/DateUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/DateUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/GreyingManager.java b/libcore/src/main/java/com/pukka/libcore/utils/GreyingManager.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/JsonUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/JsonUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/ListUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/ListUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/LogUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/LogUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/MD5Utils.java b/libcore/src/main/java/com/pukka/libcore/utils/MD5Utils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/MacAddressUtil.java b/libcore/src/main/java/com/pukka/libcore/utils/MacAddressUtil.java
old mode 100755
new mode 100644
index 7c15d9d..75d5b78
--- a/libcore/src/main/java/com/pukka/libcore/utils/MacAddressUtil.java
+++ b/libcore/src/main/java/com/pukka/libcore/utils/MacAddressUtil.java
@@ -113,17 +113,62 @@ public class MacAddressUtil {
if (interfaces == null) {
return null;
}
+ // 优先取 eth0,再退回 wlan0。原实现是"遍历到第一个有 MAC 就 break",
+ // 但 Linux 设备上 dummy0/ifb0/tunl0/gre* 等虚拟接口都有内核分配的本地随机 MAC,
+ // 会被错误命中。改为过滤虚拟接口并按 eth0 > wlan0 优先级返回。
+ String ethMac = null;
+ String wlanMac = null;
while (interfaces.hasMoreElements()) {
iF = interfaces.nextElement();
+ String name = iF.getName();
+ String lower = name == null ? "" : name.toLowerCase();
try {
- hardWareAddress = bytesToString(iF.getHardwareAddress());
- if (hardWareAddress != null)
- break;
+ // 跳过回环
+ if (iF.isLoopback()) continue;
+ // 跳过 DOWN 状态
+ if (!iF.isUp()) continue;
} catch (SocketException e) {
e.printStackTrace();
+ continue;
+ }
+ // 跳过明显的虚拟接口(dummy/ifb/隧道/p2p)
+ if (isVirtualNetworkInterface(lower)) continue;
+ try {
+ hardWareAddress = bytesToString(iF.getHardwareAddress());
+ } catch (SocketException e) {
+ e.printStackTrace();
+ continue;
+ }
+ if (hardWareAddress == null) continue;
+ // 跳过 Android 7.0+ 默认占位 MAC
+ if ("02:00:00:00:00:00".equals(hardWareAddress)) continue;
+ if ("eth0".equals(lower)) {
+ ethMac = hardWareAddress;
+ } else if ("wlan0".equals(lower)) {
+ wlanMac = hardWareAddress;
}
}
- return hardWareAddress;
+ return ethMac != null ? ethMac : wlanMac;
+ }
+
+ /**
+ * 判断是否为虚拟网络接口(不应作为设备主 MAC 返回)。
+ * 覆盖常见 Linux 虚拟接口:dummy/ifb/tunl/gre/gretap/erspan/ip_vti/ip6_vti/sit/ip6tnl/ip6gre/p2p。
+ */
+ private static boolean isVirtualNetworkInterface(String lowerName) {
+ return lowerName.startsWith("dummy")
+ || lowerName.startsWith("ifb")
+ || lowerName.startsWith("tunl")
+ || lowerName.startsWith("gre") // 包括 gre0 / gretap0 / ip6gre0
+ || lowerName.startsWith("gretap")
+ || lowerName.startsWith("erspan")
+ || lowerName.startsWith("ip_vti")
+ || lowerName.startsWith("ip6_vti")
+ || lowerName.startsWith("sit")
+ || lowerName.startsWith("ip6tnl")
+ || lowerName.startsWith("ip6gre")
+ || lowerName.startsWith("p2p")
+ || lowerName.equals("lo");
}
private static String bytesToString(byte[] bytes) {
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/PackageUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/PackageUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/ProcessUtil.java b/libcore/src/main/java/com/pukka/libcore/utils/ProcessUtil.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/ScreenUtil.java b/libcore/src/main/java/com/pukka/libcore/utils/ScreenUtil.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/SharedPreferenceUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/SharedPreferenceUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/SpSaveModel.java b/libcore/src/main/java/com/pukka/libcore/utils/SpSaveModel.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/SpTimeUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/SpTimeUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/StringUtil.java b/libcore/src/main/java/com/pukka/libcore/utils/StringUtil.java
old mode 100755
new mode 100644
index d75333e..6cbc898
--- a/libcore/src/main/java/com/pukka/libcore/utils/StringUtil.java
+++ b/libcore/src/main/java/com/pukka/libcore/utils/StringUtil.java
@@ -2,6 +2,9 @@ package com.pukka.libcore.utils;
import android.text.TextUtils;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
import java.util.Locale;
public class StringUtil {
@@ -10,8 +13,11 @@ public class StringUtil {
public static String getLocalMac() {
String mac = "";
- mac= MacAddressUtil.getMacAddressFromFile("/sys/class/net/eth0/address");
- LogUtils.d("有线(eth0)mac:"+mac);
+ // 第 1 层:sysfs 直读 eth0。走 shell `cat` 而非 FileInputStream,绕过 SELinux 对
+ // Java 进程直读 /sys/class/net/*/address 的拒绝(这是导致 wifi-only / 严格 SELinux
+ // 设备上"7 层全空"的根因)。
+ mac = readSysFsMac("/sys/class/net/eth0/address");
+ LogUtils.d("有线(eth0)mac:" + mac);
if (!TextUtils.isEmpty(mac)) {
//本地存有线mac
if (!StringUtils.isEmpty(mac)) {
@@ -20,43 +26,88 @@ public class StringUtil {
return mac.toLowerCase(Locale.getDefault());
}
- mac= MacAddressUtil.getLocalMacAddress();
- LogUtils.d("有线(LocalMac)mac:"+mac);
- if (!TextUtils.isEmpty(mac)) {
- return mac.toLowerCase(Locale.getDefault());
- }
-
- mac= MacAddressUtil.getMacAddressFromFile("/sys/class/net/wlan0/address");
- LogUtils.d("无线mac:"+mac);
+ // 第 2 层:NetworkInterface 枚举 eth0 -> wlan0 硬件地址(API 路径)
+ mac = MacAddressUtil.getLocalMacAddress();
+ LogUtils.d("有线(LocalMac)mac:" + mac);
+ if (!TextUtils.isEmpty(mac) && !TextUtils.equals("02:00:00:00:00:00", mac)) {
+ return mac.toLowerCase(Locale.getDefault());
+ }
+
+ // 第 3 层:sysfs 直读 wlan0。同样走 shell `cat`。
+ mac = readSysFsMac("/sys/class/net/wlan0/address");
+ LogUtils.d("无线mac:" + mac);
if (!TextUtils.isEmpty(mac)) {
return mac.toLowerCase(Locale.getDefault());
}
+ // 第 4 层:InetAddress 反查(API 路径)
mac = MacAddressUtil.getMacAddress();
- LogUtils.d("通过InetAddress mac:"+mac);
+ LogUtils.d("通过InetAddress mac:" + mac);
if (!TextUtils.isEmpty(mac)) {
return mac.toLowerCase(Locale.getDefault());
}
+ // 第 5 层:ifconfig eth0(shell 路径)
mac = MacAddressUtil.getLocalMac("ifconfig eth0");
- LogUtils.d("通过ifconfig eth0 mac:"+mac);
+ LogUtils.d("通过ifconfig eth0 mac:" + mac);
if (!TextUtils.isEmpty(mac)) {
return mac.toLowerCase(Locale.getDefault());
}
+ // 第 6 层:ifconfig wlan0(shell 路径)
mac = MacAddressUtil.getLocalMac("ifconfig wlan0");
- LogUtils.d("通过ifconfig wlan0 mac:"+mac);
+ LogUtils.d("通过ifconfig wlan0 mac:" + mac);
if (!TextUtils.isEmpty(mac)) {
return mac.toLowerCase(Locale.getDefault());
}
+ // 第 7 层:NetworkInterface 遍历硬件地址(API 路径)
mac = MacAddressUtil.getMachineHardwareAddress();
- LogUtils.d("通过MachineHardwareAddress mac:"+mac);
+ LogUtils.d("通过MachineHardwareAddress mac:" + mac);
if (!TextUtils.isEmpty(mac)) {
return mac.toLowerCase(Locale.getDefault());
}
- // 如果所有方法都失败,返回空字符串
+ // 如果所有方法都失败,返回空字符串(不再伪造;MacUtils2 已删除)
return "";
}
+ /**
+ * 走 shell `cat ` 读 sysfs 网络节点。
+ * 走 shell 而非 FileInputStream 的原因:
+ * SELinux 策略下 untrusted_app / system_app 直读 sysfs class net 下各 address 常被 MAC 拒绝,
+ * 而 Runtime.exec("cat ...") 启动的进程 label 是 init_shell,能绕过该限制。
+ * 同时循环 read 直到 EOF,避免单次 read(buffer) 不保证读满的坑。
+ *
+ * @return 读到的内容;读不到返回 ""
+ */
+ public static String readSysFsMac(String path) {
+ BufferedReader reader = null;
+ try {
+ Process process = Runtime.getRuntime().exec("cat " + path);
+ reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
+ String line;
+ StringBuilder sb = new StringBuilder();
+ while ((line = reader.readLine()) != null) {
+ if (sb.length() > 0) sb.append('\n');
+ sb.append(line);
+ }
+ try {
+ process.waitFor();
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ }
+ return sb.toString().trim();
+ } catch (IOException e) {
+ LogUtils.d("readSysFsMac 读取 " + path + " 失败: " + e.getMessage());
+ return "";
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
}
diff --git a/libcore/src/main/java/com/pukka/libcore/utils/StringUtils.java b/libcore/src/main/java/com/pukka/libcore/utils/StringUtils.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/viewmodel/BaseVM.java b/libcore/src/main/java/com/pukka/libcore/viewmodel/BaseVM.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/widget/AlwaysMarqueeTextView.java b/libcore/src/main/java/com/pukka/libcore/widget/AlwaysMarqueeTextView.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/widget/BrowserWebView.java b/libcore/src/main/java/com/pukka/libcore/widget/BrowserWebView.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/widget/ContentRecyclerViewTv.java b/libcore/src/main/java/com/pukka/libcore/widget/ContentRecyclerViewTv.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/widget/HorizontalLinearLayoutManagerTv.java b/libcore/src/main/java/com/pukka/libcore/widget/HorizontalLinearLayoutManagerTv.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/widget/HorizontalRecyclerViewTv.java b/libcore/src/main/java/com/pukka/libcore/widget/HorizontalRecyclerViewTv.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/widget/TVLinearLayoutManager.java b/libcore/src/main/java/com/pukka/libcore/widget/TVLinearLayoutManager.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/widget/VerticalGridLayoutManagerTv.java b/libcore/src/main/java/com/pukka/libcore/widget/VerticalGridLayoutManagerTv.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/widget/VerticalLinearLayoutManagerTv.java b/libcore/src/main/java/com/pukka/libcore/widget/VerticalLinearLayoutManagerTv.java
old mode 100755
new mode 100644
diff --git a/libcore/src/main/java/com/pukka/libcore/widget/VerticalRecyclerViewTv.java b/libcore/src/main/java/com/pukka/libcore/widget/VerticalRecyclerViewTv.java
old mode 100755
new mode 100644
diff --git a/libcore/src/test/java/com/pukka/libcore/ExampleUnitTest.java b/libcore/src/test/java/com/pukka/libcore/ExampleUnitTest.java
old mode 100755
new mode 100644
diff --git a/libdata/.gitignore b/libdata/.gitignore
old mode 100755
new mode 100644
diff --git a/libdata/build.gradle b/libdata/build.gradle
old mode 100755
new mode 100644
diff --git a/libdata/consumer-rules.pro b/libdata/consumer-rules.pro
old mode 100755
new mode 100644
diff --git a/libdata/proguard-rules.pro b/libdata/proguard-rules.pro
old mode 100755
new mode 100644
diff --git a/libdata/src/androidTest/java/com/migu/libdata/ExampleInstrumentedTest.java b/libdata/src/androidTest/java/com/migu/libdata/ExampleInstrumentedTest.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/AndroidManifest.xml b/libdata/src/main/AndroidManifest.xml
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/DataActivity.java b/libdata/src/main/java/com/migu/DataActivity.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/amber/AmberHeaderBean.java b/libdata/src/main/java/com/migu/amber/AmberHeaderBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/amber/AmberUtil.java b/libdata/src/main/java/com/migu/amber/AmberUtil.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/AdBean.java b/libdata/src/main/java/com/migu/bean/AdBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/ApkVersionResponse.java b/libdata/src/main/java/com/migu/bean/ApkVersionResponse.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/AspectRatioBean.java b/libdata/src/main/java/com/migu/bean/AspectRatioBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/AuthBean.java b/libdata/src/main/java/com/migu/bean/AuthBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/AuthOrizeBean_shanghai.java b/libdata/src/main/java/com/migu/bean/AuthOrizeBean_shanghai.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/AuthorizeNewBean.java b/libdata/src/main/java/com/migu/bean/AuthorizeNewBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/BesAuthBean.java b/libdata/src/main/java/com/migu/bean/BesAuthBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/BesOrderBean.java b/libdata/src/main/java/com/migu/bean/BesOrderBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/BesPlayUrlBean.java b/libdata/src/main/java/com/migu/bean/BesPlayUrlBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/CollectBean.java b/libdata/src/main/java/com/migu/bean/CollectBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/CollectReportBean_shanghai.java b/libdata/src/main/java/com/migu/bean/CollectReportBean_shanghai.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/ContentListBean.java b/libdata/src/main/java/com/migu/bean/ContentListBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/DisplayTypeListBean.java b/libdata/src/main/java/com/migu/bean/DisplayTypeListBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/EventBusMessage.java b/libdata/src/main/java/com/migu/bean/EventBusMessage.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetCalenderMatchDate.java b/libdata/src/main/java/com/migu/bean/GetCalenderMatchDate.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetCollectBean.java b/libdata/src/main/java/com/migu/bean/GetCollectBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetEpgNavBean.java b/libdata/src/main/java/com/migu/bean/GetEpgNavBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetFindPlayrecordBean.java b/libdata/src/main/java/com/migu/bean/GetFindPlayrecordBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetGameDetailBean.java b/libdata/src/main/java/com/migu/bean/GetGameDetailBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetGamePlayurlAndPlayStatusBean.java b/libdata/src/main/java/com/migu/bean/GetGamePlayurlAndPlayStatusBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetGameSeasonInfoListBean.java b/libdata/src/main/java/com/migu/bean/GetGameSeasonInfoListBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetGameSeasonInfoRecommendBean.java b/libdata/src/main/java/com/migu/bean/GetGameSeasonInfoRecommendBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetLayoutBean.java b/libdata/src/main/java/com/migu/bean/GetLayoutBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetLayoutNavBean.java b/libdata/src/main/java/com/migu/bean/GetLayoutNavBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetMatchListBean.java b/libdata/src/main/java/com/migu/bean/GetMatchListBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetServiceListBean.java b/libdata/src/main/java/com/migu/bean/GetServiceListBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetShortVideoBean.java b/libdata/src/main/java/com/migu/bean/GetShortVideoBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetTopButtonBean.java b/libdata/src/main/java/com/migu/bean/GetTopButtonBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetcontentlistBean.java b/libdata/src/main/java/com/migu/bean/GetcontentlistBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetlistcollBean.java b/libdata/src/main/java/com/migu/bean/GetlistcollBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GetsystemBean.java b/libdata/src/main/java/com/migu/bean/GetsystemBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GettagslistBean.java b/libdata/src/main/java/com/migu/bean/GettagslistBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GoOrderBean_shanghai.java b/libdata/src/main/java/com/migu/bean/GoOrderBean_shanghai.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/GoldMainBean.java b/libdata/src/main/java/com/migu/bean/GoldMainBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/HeartBeatEntity.java b/libdata/src/main/java/com/migu/bean/HeartBeatEntity.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/HistoryReportBean_shanghai.java b/libdata/src/main/java/com/migu/bean/HistoryReportBean_shanghai.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/HljAddCollectionBean.java b/libdata/src/main/java/com/migu/bean/HljAddCollectionBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/HljAddHistoryBean.java b/libdata/src/main/java/com/migu/bean/HljAddHistoryBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/IntentBean_Shanghai.java b/libdata/src/main/java/com/migu/bean/IntentBean_Shanghai.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/KYSDKPlayInfoUser.java b/libdata/src/main/java/com/migu/bean/KYSDKPlayInfoUser.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/KeyboardBean.java b/libdata/src/main/java/com/migu/bean/KeyboardBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/MediaPlayerHeaderBean.java b/libdata/src/main/java/com/migu/bean/MediaPlayerHeaderBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/NewtvBean.java b/libdata/src/main/java/com/migu/bean/NewtvBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/OTTSSOEntity.java b/libdata/src/main/java/com/migu/bean/OTTSSOEntity.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/PassDataBean.java b/libdata/src/main/java/com/migu/bean/PassDataBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/PhoneDataBean.java b/libdata/src/main/java/com/migu/bean/PhoneDataBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/PlayHistoryBean.java b/libdata/src/main/java/com/migu/bean/PlayHistoryBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/Province.java b/libdata/src/main/java/com/migu/bean/Province.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/RateBean.java b/libdata/src/main/java/com/migu/bean/RateBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/ReleaseVideoBean.java b/libdata/src/main/java/com/migu/bean/ReleaseVideoBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/ResumeVideoBean.java b/libdata/src/main/java/com/migu/bean/ResumeVideoBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/SearchRecommendBean.java b/libdata/src/main/java/com/migu/bean/SearchRecommendBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/SearchResultBean.java b/libdata/src/main/java/com/migu/bean/SearchResultBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/ShanghaiBean.java b/libdata/src/main/java/com/migu/bean/ShanghaiBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/ShortVideoBean.java b/libdata/src/main/java/com/migu/bean/ShortVideoBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/ShortVideoInfoListBean.java b/libdata/src/main/java/com/migu/bean/ShortVideoInfoListBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/ShortVideoLazyBean.java b/libdata/src/main/java/com/migu/bean/ShortVideoLazyBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/ShowOrGoneNavBean.java b/libdata/src/main/java/com/migu/bean/ShowOrGoneNavBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/SilenceFullPlay.java b/libdata/src/main/java/com/migu/bean/SilenceFullPlay.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/StarInfoIndexBean.java b/libdata/src/main/java/com/migu/bean/StarInfoIndexBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/StarPlayerInfoBean.java b/libdata/src/main/java/com/migu/bean/StarPlayerInfoBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/SubmitPointsLikesBean.java b/libdata/src/main/java/com/migu/bean/SubmitPointsLikesBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/SuggestBean.java b/libdata/src/main/java/com/migu/bean/SuggestBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/SupportVoteBean.java b/libdata/src/main/java/com/migu/bean/SupportVoteBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/T9KeyboardBean.java b/libdata/src/main/java/com/migu/bean/T9KeyboardBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/TopicVoteInfoBean.java b/libdata/src/main/java/com/migu/bean/TopicVoteInfoBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/UpApkBean.java b/libdata/src/main/java/com/migu/bean/UpApkBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/UpApkRequestBean.java b/libdata/src/main/java/com/migu/bean/UpApkRequestBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/bean/VideoResultBean.java b/libdata/src/main/java/com/migu/bean/VideoResultBean.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/callback/AijiaCallback.java b/libdata/src/main/java/com/migu/callback/AijiaCallback.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/callback/CommonCallback.java b/libdata/src/main/java/com/migu/callback/CommonCallback.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/callback/GoOrderCallbackShanghai.java b/libdata/src/main/java/com/migu/callback/GoOrderCallbackShanghai.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/httputils/AndroidPlatform9.java b/libdata/src/main/java/com/migu/httputils/AndroidPlatform9.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/httputils/BaseHttpUtils.java b/libdata/src/main/java/com/migu/httputils/BaseHttpUtils.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/httputils/ContentHttpUtils.java b/libdata/src/main/java/com/migu/httputils/ContentHttpUtils.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/httputils/DataValueUtils.java b/libdata/src/main/java/com/migu/httputils/DataValueUtils.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/httputils/HomeHttpUtils.java b/libdata/src/main/java/com/migu/httputils/HomeHttpUtils.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/httputils/LowPlatformUtils.java b/libdata/src/main/java/com/migu/httputils/LowPlatformUtils.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/httputils/MatchHttpUtils.java b/libdata/src/main/java/com/migu/httputils/MatchHttpUtils.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/httputils/OptionalMethod.java b/libdata/src/main/java/com/migu/httputils/OptionalMethod.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/AnHuiApi.java b/libdata/src/main/java/com/migu/libnetwork/AnHuiApi.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/ApiService.java b/libdata/src/main/java/com/migu/libnetwork/ApiService.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/GuangDongApi.java b/libdata/src/main/java/com/migu/libnetwork/GuangDongApi.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HaiNanApi.java b/libdata/src/main/java/com/migu/libnetwork/HaiNanApi.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HeNanApi.java b/libdata/src/main/java/com/migu/libnetwork/HeNanApi.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HeiLongJiangApi.java b/libdata/src/main/java/com/migu/libnetwork/HeiLongJiangApi.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HttpRequestCallback.java b/libdata/src/main/java/com/migu/libnetwork/HttpRequestCallback.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HttpRequestConstants.java b/libdata/src/main/java/com/migu/libnetwork/HttpRequestConstants.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HttpRequestFileCallback.java b/libdata/src/main/java/com/migu/libnetwork/HttpRequestFileCallback.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HttpRequestInterceptor.java b/libdata/src/main/java/com/migu/libnetwork/HttpRequestInterceptor.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HttpRetrofit.java b/libdata/src/main/java/com/migu/libnetwork/HttpRetrofit.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HttpRetrofitRequest.java b/libdata/src/main/java/com/migu/libnetwork/HttpRetrofitRequest.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HttpTool.java b/libdata/src/main/java/com/migu/libnetwork/HttpTool.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HttpUtils.java b/libdata/src/main/java/com/migu/libnetwork/HttpUtils.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/HttpsTrustManager.kt b/libdata/src/main/java/com/migu/libnetwork/HttpsTrustManager.kt
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/IHttpRequest.java b/libdata/src/main/java/com/migu/libnetwork/IHttpRequest.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/NetUtil.java b/libdata/src/main/java/com/migu/libnetwork/NetUtil.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/OkHttpClientUtil.java b/libdata/src/main/java/com/migu/libnetwork/OkHttpClientUtil.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/OkHttpSsl.java b/libdata/src/main/java/com/migu/libnetwork/OkHttpSsl.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/RetrofitClientUtil.java b/libdata/src/main/java/com/migu/libnetwork/RetrofitClientUtil.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/RetryFunction.java b/libdata/src/main/java/com/migu/libnetwork/RetryFunction.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/RxJavaFileObserver.java b/libdata/src/main/java/com/migu/libnetwork/RxJavaFileObserver.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/RxJavaObserver.java b/libdata/src/main/java/com/migu/libnetwork/RxJavaObserver.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/SSLSocketFactoryCompat.kt b/libdata/src/main/java/com/migu/libnetwork/SSLSocketFactoryCompat.kt
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/SearchApi.java b/libdata/src/main/java/com/migu/libnetwork/SearchApi.java
old mode 100755
new mode 100644
diff --git a/libdata/src/main/java/com/migu/libnetwork/TrustAllHostnameVerifier.kt b/libdata/src/main/java/com/migu/libnetwork/TrustAllHostnameVerifier.kt
old mode 100755
new mode 100644
diff --git a/libdata/src/main/res/layout/libdata_activity_main.xml b/libdata/src/main/res/layout/libdata_activity_main.xml
old mode 100755
new mode 100644
diff --git a/libdata/src/main/res/values/colors.xml b/libdata/src/main/res/values/colors.xml
old mode 100755
new mode 100644
diff --git a/libdata/src/main/res/values/dimens.xml b/libdata/src/main/res/values/dimens.xml
old mode 100755
new mode 100644
diff --git a/libdata/src/main/res/values/strings.xml b/libdata/src/main/res/values/strings.xml
old mode 100755
new mode 100644
diff --git a/libdata/src/test/java/com/migu/libdata/ExampleUnitTest.java b/libdata/src/test/java/com/migu/libdata/ExampleUnitTest.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/.gitignore b/mediaplayertv/.gitignore
old mode 100755
new mode 100644
diff --git a/mediaplayertv/build.gradle b/mediaplayertv/build.gradle
old mode 100755
new mode 100644
diff --git a/mediaplayertv/consumer-rules.pro b/mediaplayertv/consumer-rules.pro
old mode 100755
new mode 100644
diff --git a/mediaplayertv/libs/HyMediaPlayer-v1.2.15.aar b/mediaplayertv/libs/HyMediaPlayer-v1.2.15.aar
old mode 100755
new mode 100644
diff --git a/mediaplayertv/libs/exoplayer-ffmpeg-extension-2.19.1.aar b/mediaplayertv/libs/exoplayer-ffmpeg-extension-2.19.1.aar
old mode 100755
new mode 100644
diff --git a/mediaplayertv/proguard-rules.pro b/mediaplayertv/proguard-rules.pro
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/androidTest/java/com/pukka/mediaplayertv/ExampleInstrumentedTest.java b/mediaplayertv/src/androidTest/java/com/pukka/mediaplayertv/ExampleInstrumentedTest.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/AndroidManifest.xml b/mediaplayertv/src/main/AndroidManifest.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/AbstractMediaPlayer.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/AbstractMediaPlayer.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/AndroidMediaFormat.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/AndroidMediaFormat.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/AndroidMediaPlayer.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/AndroidMediaPlayer.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/AndroidTrackInfo.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/AndroidTrackInfo.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/ConfigPlayer.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/ConfigPlayer.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaController.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaController.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaDataSource.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaDataSource.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaFormat.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaFormat.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaPlayer.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaPlayer.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/IRenderView.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/IRenderView.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/ISurfaceTextureHolder.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/ISurfaceTextureHolder.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/ISurfaceTextureHost.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/ISurfaceTextureHost.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/ITrackInfo.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/ITrackInfo.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/MeasureHelper.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/MeasureHelper.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/MediaInfo.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/MediaInfo.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/MediaPlayerControl.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/MediaPlayerControl.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/SoftMediaPlayer.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/SoftMediaPlayer.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/SurfaceRenderView.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/SurfaceRenderView.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/TvBaseController.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/TvBaseController.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/TvMediaController.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/TvMediaController.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/VideoViewTv.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/VideoViewTv.java
old mode 100755
new mode 100644
index 7970046..a7ccec1
--- a/mediaplayertv/src/main/java/com/pukka/videoplayertv/VideoViewTv.java
+++ b/mediaplayertv/src/main/java/com/pukka/videoplayertv/VideoViewTv.java
@@ -328,11 +328,6 @@ public class VideoViewTv extends FrameLayout implements MediaPlayerControl {
}
private void initVideoView() {
- // Android 4.2 兼容:ExoPlayer 2.19 官方支持 API 19+,低版本强制切系统 MediaPlayer 内核,
- // 避免 PkExoMediaPlayer/ExoPlayer 类在 Dalvik 上触发 VerifyError
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
- terminalType = 0;
- }
if (!checkAudioCapability()){
terminalType = 0;
}
@@ -567,9 +562,7 @@ public class VideoViewTv extends FrameLayout implements MediaPlayerControl {
reset();
mMediaPlayer.release();
if(mMediaPlayer != null){
- // terminalType 短路:非 Exo 内核时跳过 instanceof,避免 Dalvik 解析
- // PkExoMediaPlayer 引发的 ExoPlayer 类加载 VerifyError(Android 4.2)
- if(terminalType == 2 && mMediaPlayer instanceof PkExoMediaPlayer){
+ if(mMediaPlayer instanceof PkExoMediaPlayer){
PkExoMediaPlayer pkExoMediaPlayer = (PkExoMediaPlayer)mMediaPlayer;
pkExoMediaPlayer.resetListeners();
}
@@ -1498,8 +1491,7 @@ public class VideoViewTv extends FrameLayout implements MediaPlayerControl {
onPauseListener = null;
onResumeListener = null;
if(mMediaPlayer != null){
- // terminalType 短路:同上,避免非 Exo 内核时 Dalvik 触发 PkExoMediaPlayer 类解析
- if(terminalType == 2 && mMediaPlayer instanceof PkExoMediaPlayer){
+ if(mMediaPlayer instanceof PkExoMediaPlayer){
PkExoMediaPlayer pkExoMediaPlayer = (PkExoMediaPlayer)mMediaPlayer;
pkExoMediaPlayer.resetListeners();
}
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/ExoMediaSourceHelper.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/ExoMediaSourceHelper.java
old mode 100755
new mode 100644
index 132c076..22e255b
--- a/mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/ExoMediaSourceHelper.java
+++ b/mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/ExoMediaSourceHelper.java
@@ -64,6 +64,14 @@ public final class ExoMediaSourceHelper {
}
public MediaSource getMediaSource(String uri, Map headers, boolean isCache) {
+ // 防御性边界检查:上游偶发传入空 URL(典型如 Jetsenx SDK 返回的双清晰度 URL 都为空),
+ // 此时 Uri.parse("") 会得到 Uri.EMPTY,DefaultDataSource 因缺少 http/https scheme
+ // 会退回到 FileDataSource,再用空路径去打开本地文件,触发 FileNotFoundException: ENOENT
+ // 导致播放崩溃。返回 null 让调用方走 if (mMediaSource == null) return; 兜底。
+ if (TextUtils.isEmpty(uri)) {
+ android.util.Log.w("ExoMediaSourceHelper", "getMediaSource called with empty uri, return null to avoid FileDataSource ENOENT crash");
+ return null;
+ }
Uri contentUri = Uri.parse(uri);
// if ("rtmp".equals(contentUri.getScheme())) {
// return new ProgressiveMediaSource.Factory(new RtmpDataSource.Factory())
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkExoMediaPlayer.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkExoMediaPlayer.java
old mode 100755
new mode 100644
index f2fa9ed..ed53274
--- a/mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkExoMediaPlayer.java
+++ b/mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkExoMediaPlayer.java
@@ -6,6 +6,7 @@ import android.media.MediaFormat;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
+import android.provider.Settings;
import android.view.Surface;
import android.view.SurfaceHolder;
@@ -20,6 +21,9 @@ import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.analytics.DefaultAnalyticsCollector;
+import com.google.android.exoplayer2.audio.AudioCapabilities;
+import com.google.android.exoplayer2.audio.AudioSink;
+import com.google.android.exoplayer2.audio.DefaultAudioSink;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@@ -32,6 +36,7 @@ import com.google.android.exoplayer2.video.VideoSize;
import com.pukka.libcore.base.BuildConfig;
import com.pukka.libcore.threading.DispatchQueue;
import com.pukka.libcore.utils.LogUtils;
+import com.pukka.libcore.utils.SharedPreferenceUtils;
import com.pukka.videoplayertv.AbstractMediaPlayer;
import com.pukka.videoplayertv.IMediaDataSource;
import com.pukka.videoplayertv.ITrackInfo;
@@ -48,6 +53,13 @@ import java.util.Map;
public final class PkExoMediaPlayer extends AbstractMediaPlayer implements Player.Listener {
private final static String TAG = "PkExoMediaPlayer";
private final static String TAG2 = "PkExoMediaPlayer|播放流程";
+ /**
+ * v1.3.36: 进程级禁用 AC3 caps 注入。
+ * 直通初始化失败自动降级后置位,本进程内不再注入(设备级失败,重试无意义)。
+ */
+ private static volatile boolean sAc3InjectionDisabled = false;
+ /** 当前播放器实例的 AudioSink 是否注入了 AC3 caps(onPlayerError 降级判定用) */
+ private volatile boolean mAc3Injected = false;
/**
* 视频/音频开始渲染
*/
@@ -106,9 +118,54 @@ public final class PkExoMediaPlayer extends AbstractMediaPlayer implements Playe
DefaultLoadControl mLoadControl = new DefaultLoadControl.Builder()
.setBufferDurationsMs(50000, 50000, 1500, 2000)
.build();
+ if (mRenderersFactory == null) {
+ mRenderersFactory = new DefaultRenderersFactory(mAppContext) {
+ @Override
+ protected AudioSink buildAudioSink(Context context, boolean enableFloatOutput,
+ boolean enableAudioTrackPlaybackParams, boolean enableOffload) {
+ AudioCapabilities caps = AudioCapabilities.getCapabilities(context);
+ boolean ac3Supported = caps.supportsEncoding(C.ENCODING_AC3);
+ // 5.1 开关开启且系统未禁止环绕声时,强制注入 AC3 直通能力。
+ // 修复:HAL 从 PCM 切回 AUTO 有延迟,系统能力查询可能返回过时结果,
+ // 导致 ExoPlayer 不尝试 AC3 直通。
+ // v1.3.36 关键修复:DefaultAudioSink.Builder(context) 传入 context 时,
+ // getFormatSupport() 首次调用会懒创建 AudioCapabilitiesReceiver 并用系统
+ // 实时 caps 覆盖注入值(反编译 2.19.1 字节码确认)→ 注入从未生效。
+ // 必须用无参 Builder()(context=null)才能让注入的 caps 持续有效。
+ // v1.3.36 降级保护:直通初始化失败后 sAc3InjectionDisabled 置位,本进程不再注入。
+ if (!ac3Supported && !sAc3InjectionDisabled) {
+ boolean surroundEnabled = SharedPreferenceUtils.getBoolean("Surround51Enabled", true);
+ if (surroundEnabled) {
+ int encodedSurround = Settings.Global.getInt(
+ context.getContentResolver(), "encoded_surround_output", 0);
+ // encoded_surround_output: 0=AUTO, 1=NEVER, 2=ALWAYS(部分固件映射异常), 3=MANUAL
+ if (encodedSurround != 1) {
+ LogUtils.d(TAG, "buildAudioSink: 系统未报告AC3,强制注入。"
+ + " encodedSurround=" + encodedSurround);
+ caps = new AudioCapabilities(
+ new int[]{C.ENCODING_PCM_16BIT, C.ENCODING_AC3}, 8);
+ mAc3Injected = true;
+ }
+ }
+ } else if (ac3Supported) {
+ LogUtils.d(TAG, "buildAudioSink: 系统已报告AC3直通能力");
+ } else {
+ LogUtils.d(TAG, "buildAudioSink: 注入已禁用(本进程曾直通失败),走系统 caps");
+ }
+ return new DefaultAudioSink.Builder()
+ .setAudioCapabilities(caps)
+ .setEnableFloatOutput(enableFloatOutput)
+ .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
+ .setOffloadMode(enableOffload
+ ? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED
+ : DefaultAudioSink.OFFLOAD_MODE_DISABLED)
+ .build();
+ }
+ }.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON);
+ }
ExoPlayer mInternalPlayer = new ExoPlayer.Builder(
mAppContext,
- mRenderersFactory == null ? mRenderersFactory = new DefaultRenderersFactory(mAppContext).setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER) : mRenderersFactory,
+ mRenderersFactory,
new DefaultMediaSourceFactory(mAppContext),
mTrackSelector == null ? mTrackSelector = new DefaultTrackSelector(mAppContext) : mTrackSelector,
mLoadControl,
@@ -608,10 +665,64 @@ public final class PkExoMediaPlayer extends AbstractMediaPlayer implements Playe
LogUtils.d(TAG, "onPlayerError");
String errorMessage = "code:" + error.errorCode + " ||message:" + error.getMessage();
LogUtils.d(TAG2, "播放器报错 " + errorMessage);
+ // v1.3.36: AC3 注入会话中的音频初始化类错误 → 疑似直通初始化失败
+ // (平台有 raw decoder 但 policy 无 DIRECT 压缩输出时,AudioTrack(ENCODING_AC3) init 失败)。
+ // 此时注入的 caps 不可达,摘除注入并自动重建播放器软解降级重试(一次为限),不上抛错误。
+ if (mAc3Injected && !sAc3InjectionDisabled) {
+ int code = error.errorCode;
+ if (code == PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED
+ || code == PlaybackException.ERROR_CODE_DECODER_INIT_FAILED
+ || code == PlaybackException.ERROR_CODE_UNSPECIFIED) {
+ LogUtils.d(TAG, "onPlayerError: 注入会话音频初始化失败(code=" + code
+ + "),自动降级为软解重试");
+ sAc3InjectionDisabled = true;
+ degradeAndRetry(error);
+ return;
+ }
+ }
initSeekTime();
notifyOnError(error.errorCode, errorMessage.contains("Format")?264:9999);
}
+ /**
+ * v1.3.36: 关闭 AC3 注入重建播放器,软解降级重试。
+ * 当前片源内闭环恢复(保留进度),仅在重试再次失败时才上抛原错误。
+ */
+ private void degradeAndRetry(PlaybackException error) {
+ final long resumePos = mInternalPlayer != null
+ ? Math.max(0, mInternalPlayer.getCurrentPosition()) : 0;
+ DispatchQueue.main.async(() -> {
+ LogUtils.d(TAG, "degradeAndRetry: 关闭AC3注入重建播放器, resumePos=" + resumePos);
+ try {
+ if (mInternalPlayer != null) {
+ mInternalPlayer.removeListener(PkExoMediaPlayer.this);
+ mInternalPlayer.release();
+ mInternalPlayer = null;
+ }
+ mRenderersFactory = null; // 置空以重建 AudioSink(sAc3InjectionDisabled=true,不再注入)
+ mAc3Injected = false;
+ mInternalPlayer = initPlayer();
+ mInternalPlayer.setWakeMode(C.WAKE_MODE_LOCAL);
+ setOptions();
+ initMediaListener();
+ if (mSpeedPlaybackParameters != null) {
+ mInternalPlayer.setPlaybackParameters(mSpeedPlaybackParameters);
+ }
+ mIsPreparing = true;
+ mInternalPlayer.setMediaSource(mMediaSource);
+ mInternalPlayer.setPlayWhenReady(true);
+ mInternalPlayer.prepare();
+ if (resumePos > 3000) {
+ mInternalPlayer.seekTo(resumePos);
+ }
+ } catch (Throwable t) {
+ LogUtils.d(TAG, "degradeAndRetry 失败,上抛原错误:" + t);
+ initSeekTime();
+ notifyOnError(error.errorCode, 9999);
+ }
+ });
+ }
+
@Override
public void onRenderedFirstFrame() {
LogUtils.d(TAG, "onRenderedFirstFrame");
diff --git a/mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkHighExoMediaPlayer.java b/mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkHighExoMediaPlayer.java
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-hdpi/ic_media_pause.png b/mediaplayertv/src/main/res/drawable-hdpi/ic_media_pause.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-hdpi/player_load_bg.png b/mediaplayertv/src/main/res/drawable-hdpi/player_load_bg.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-hdpi/player_load_logo.png b/mediaplayertv/src/main/res/drawable-hdpi/player_load_logo.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading00.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading00.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading000.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading000.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading01.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading01.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading02.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading02.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading03.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading03.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading04.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading04.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading05.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading05.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading06.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading06.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading07.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading07.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading08.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading08.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading09.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading09.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading10.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading10.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading11.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading11.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading12.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading12.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading13.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading13.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading14.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading14.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading15.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading15.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading16.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading16.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading17.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading17.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading18.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading18.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading19.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading19.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading20.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading20.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading21.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading21.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/loading22.png b/mediaplayertv/src/main/res/drawable-xxhdpi/loading22.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/progress_loading.png b/mediaplayertv/src/main/res/drawable-xxhdpi/progress_loading.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable-xxhdpi/switch_keyboard_bg.png b/mediaplayertv/src/main/res/drawable-xxhdpi/switch_keyboard_bg.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/bg_btn_video_selector.xml b/mediaplayertv/src/main/res/drawable/bg_btn_video_selector.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/bg_control_bottom.xml b/mediaplayertv/src/main/res/drawable/bg_control_bottom.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/bg_control_top.xml b/mediaplayertv/src/main/res/drawable/bg_control_top.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/bg_pause_video_selector.xml b/mediaplayertv/src/main/res/drawable/bg_pause_video_selector.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/bg_white_round_border_normal.xml b/mediaplayertv/src/main/res/drawable/bg_white_round_border_normal.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/bg_white_round_border_pressed.xml b/mediaplayertv/src/main/res/drawable/bg_white_round_border_pressed.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/bg_white_round_border_selector.xml b/mediaplayertv/src/main/res/drawable/bg_white_round_border_selector.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/loading_icon.png b/mediaplayertv/src/main/res/drawable/loading_icon.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/loading_player_animation.xml b/mediaplayertv/src/main/res/drawable/loading_player_animation.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/ondemand_seekbar_style.xml b/mediaplayertv/src/main/res/drawable/ondemand_seekbar_style.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/player_seek_thumb.xml b/mediaplayertv/src/main/res/drawable/player_seek_thumb.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/seekbar_focused_background.xml b/mediaplayertv/src/main/res/drawable/seekbar_focused_background.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/svg_media_pause_icon.xml b/mediaplayertv/src/main/res/drawable/svg_media_pause_icon.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/svg_media_play_icon.xml b/mediaplayertv/src/main/res/drawable/svg_media_play_icon.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/videotip1.png b/mediaplayertv/src/main/res/drawable/videotip1.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/videotip2.png b/mediaplayertv/src/main/res/drawable/videotip2.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/drawable/volume_seek_progress.xml b/mediaplayertv/src/main/res/drawable/volume_seek_progress.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/layout/media_controller_tv.xml b/mediaplayertv/src/main/res/layout/media_controller_tv.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/layout/media_vod_menu_controller.xml b/mediaplayertv/src/main/res/layout/media_vod_menu_controller.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/mipmap-hdpi/dot.png b/mediaplayertv/src/main/res/mipmap-hdpi/dot.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/mipmap-hdpi/ic_media_play.png b/mediaplayertv/src/main/res/mipmap-hdpi/ic_media_play.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/mipmap-hdpi/icon_backward.png b/mediaplayertv/src/main/res/mipmap-hdpi/icon_backward.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/mipmap-hdpi/icon_forward.png b/mediaplayertv/src/main/res/mipmap-hdpi/icon_forward.png
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values-sw1080dp-land/dimens.xml b/mediaplayertv/src/main/res/values-sw1080dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values-sw360dp-land/dimens.xml b/mediaplayertv/src/main/res/values-sw360dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values-sw480dp-land/dimens.xml b/mediaplayertv/src/main/res/values-sw480dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values-sw540dp-land/dimens.xml b/mediaplayertv/src/main/res/values-sw540dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values-sw617dp-land/dimens.xml b/mediaplayertv/src/main/res/values-sw617dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values-sw720dp-land/dimens.xml b/mediaplayertv/src/main/res/values-sw720dp-land/dimens.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values/colors.xml b/mediaplayertv/src/main/res/values/colors.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values/dimens.xml b/mediaplayertv/src/main/res/values/dimens.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values/dimens_sw.xml b/mediaplayertv/src/main/res/values/dimens_sw.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values/h_dimens.xml b/mediaplayertv/src/main/res/values/h_dimens.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values/strings.xml b/mediaplayertv/src/main/res/values/strings.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/main/res/values/w_dimens.xml b/mediaplayertv/src/main/res/values/w_dimens.xml
old mode 100755
new mode 100644
diff --git a/mediaplayertv/src/test/java/com/pukka/mediaplayertv/ExampleUnitTest.java b/mediaplayertv/src/test/java/com/pukka/mediaplayertv/ExampleUnitTest.java
old mode 100755
new mode 100644
diff --git a/settings.gradle b/settings.gradle
old mode 100755
new mode 100644
diff --git a/test/voice_helper/README.md b/test/voice_helper/README.md
new file mode 100644
index 0000000..9528eaf
--- /dev/null
+++ b/test/voice_helper/README.md
@@ -0,0 +1,167 @@
+# 语音协议广播测试脚本
+
+`adb shell am broadcast` 形式的联调脚本集合,覆盖 [可行性评估文档](../../../docs/2026-08-22-语音影视接入可行性评估.md) §6.1 中定义的统一协议(`action=com.pukka.jiecheng.action.VOICE_COMMAND`),用于在没有第三方语音 App 时手工驱动 voice 模块。
+
+> 适用范围:`bash`(Git Bash 即可)。脚本里所有路径用 Unix 风格。
+
+## 1. 目录结构
+
+```
+test/voice_helper/
+├── lib/
+│ └── common.sh # 公共函数:payload 构造 / 广播发送 / 应用级操作
+├── jump_film.sh # 单片跳转 JUMP/FILM
+├── jump_category.sh # 分类跳转 JUMP/CATEGORY
+├── search.sh # 语音搜索 SEARCH
+├── control.sh # 播放控制 CONTROL/
+├── cold_start.sh # 冷启动单片跳转(force-stop + 投递 + 拉起)
+├── throttle_test.sh # 2 秒内连续投递同类命令(验证节流)
+├── security_test.sh # 非白名单 sourceApp 投递(验证签名校验)
+├── listen_callback.sh # 反向广播回调监听(adb logcat)
+├── run_all.sh # 顺序执行全部用例
+└── README.md
+```
+
+## 2. 协议约定(与文档 §6.1 一致)
+
+```
+action: com.pukka.jiecheng.action.VOICE_COMMAND
+permission: com.pukka.jiecheng.permission.VOICE_COMMAND
+component: com.pukka.jiecheng/.voice.receiver.VoiceCommandReceiver
+
+extras:
+ requestId string 第三方生成的请求 ID,回传时带回
+ sourceApp string 第三方包名
+ sourcePlatform string 第三方平台标识(用于选择 VendorAParser/BParser)
+ commandType string JUMP / SEARCH / CONTROL
+ payload string JSON,业务字段
+ callbackAction string 反向广播 action,由第三方传入
+ timestamp long 毫秒
+```
+
+`payload` 按 `commandType` 分支:
+
+| commandType | payload 字段 |
+|---|---|
+| `JUMP` | `jumpType` (`FILM`/`CATEGORY`/`ACTOR`/`DIRECTOR`) + 对应 `*Id`/`*Name`,可带 `autoPlay` |
+| `SEARCH` | `filmName` / `actorName` / `directorName` / `category` / `unionStyle` / `region` / `releaseDate` |
+| `CONTROL`| `action` (`PLAY`/`PAUSE`/`FULLSCREEN_ENTER`/`FULLSCREEN_EXIT`/`SEEK_FORWARD`/`SEEK_BACKWARD`/`SEEK_TO`/`PREV`/`NEXT`/`SELECT_INDEX`/`PAGE_NEXT`/`PAGE_PREV`/`GOTO_PAGE`) + `seconds` / `index` / `page` |
+
+## 3. 快速开始
+
+```bash
+# 设备已连接 adb 后:
+cd test/voice_helper
+
+# 单片跳转(默认 filmId=film-demo-001 / autoPlay=true)
+./jump_film.sh
+
+# 自定义参数
+./jump_film.sh film-12345 "我的影片" true
+
+# 分类跳转
+./jump_category.sh cat-action "动作片"
+
+# 语音搜索
+./search.sh "流浪地球"
+./search.sh "流浪地球" "吴京"
+
+# 播放控制
+./control.sh PAUSE
+./control.sh SEEK_FORWARD seconds=10
+./control.sh SEEK_TO seconds=300
+./control.sh SELECT_INDEX index=3
+./control.sh GOTO_PAGE page=2
+
+# 冷启动(force-stop → 投递 → 拉起 MainActivity)
+./cold_start.sh
+
+# 2 秒内连续 5 次 PAUSE,验证节流
+./throttle_test.sh PAUSE 5
+
+# 非白名单 sourceApp 投递(验证拦截)
+./security_test.sh com.attacker.malicious
+
+# 反向广播监听(需 voice 模块按 tag 打印回调日志,tag 默认 VoiceCommand)
+./listen_callback.sh # 监听全部
+./listen_callback.sh vt-1756000000-12345 # 只过滤指定 requestId
+
+# 一键全量
+./run_all.sh
+```
+
+## 4. 环境变量
+
+| 变量 | 默认值 | 说明 |
+|---|---|---|
+| `ACTION_VOICE_COMMAND` | `com.pukka.jiecheng.action.VOICE_COMMAND` | 广播 action |
+| `PERMISSION_VOICE` | `com.pukka.jiecheng.permission.VOICE_COMMAND` | 协议权限 |
+| `RECEIVER_COMPONENT` | `com.pukka.jiecheng/.voice.receiver.VoiceCommandReceiver` | 显式组件 |
+| `MAIN_ACTIVITY_COMPONENT` | `com.pukka.jiecheng/.MainActivity` | 冷启动入口 |
+| `TEST_APP_PKG` | `com.pukka.jiecheng` | 应用包名 |
+| `DEFAULT_SOURCE_APP` | `com.test.voicehelper` | 模拟第三方包名 |
+| `DEFAULT_PLATFORM` | `vendorA` | 模拟平台 |
+| `DEFAULT_CALLBACK_ACTION`| `com.test.voicehelper.action.VOICE_CALLBACK` | 反向广播 action |
+| `TEST_REQUEST_PREFIX` | `vt-` | requestId 前缀 |
+| `SKIP_PERMISSION_CHECK` | `0` | 开发期临时绕开 signature-level permission 校验时置 1 |
+| `LOG_TAG` | `VoiceCommand` | `listen_callback.sh` 监听的 logcat tag |
+
+示例:自定义第三方包名
+
+```bash
+TEST_REQUEST_PREFIX=demo- DEFAULT_SOURCE_APP=com.xiaomi.voice ./jump_film.sh
+```
+
+## 5. 关于 `signature-level permission`
+
+文档 §6.2 规定 Receiver 使用自定义 `signature-level permission`(`com.pukka.jiecheng.permission.VOICE_COMMAND`)。`adb shell` 默认以 `shell` 用户身份投递,**不持有第三方签名权限**,广播会被系统直接丢弃。
+
+联调期三种处理方式:
+
+1. **临时降级(推荐)**:在 `app/build.gradle` 给 `debug` flavor 注入 `manifestPlaceholder`:
+ ```groovy
+ debug {
+ manifestPlaceholders = [voiceProtectionLevel: "normal"]
+ }
+ release {
+ manifestPlaceholders = [voiceProtectionLevel: "signature"]
+ }
+ ```
+ 然后在 `AndroidManifest.xml`:
+ ```xml
+
+ ```
+2. **拷贝脚本到端内**:在应用内临时放一个 `BroadcastSender` Activity 直接发广播,绕过 shell 权限。
+3. **改 adb 用户**:通过 `adb root` 切到 root 身份,仅在 userdebug 镜像生效。
+
+脚本本身不强制任何一种处理,遇到权限拦截请按上面方式放开。
+
+## 6. 验证 checklist(对应文档 §12)
+
+联调完成后逐项打勾:
+
+- [ ] 应用未启动时第三方冷启动并执行命令(`cold_start.sh`)
+- [ ] 应用未激活时被守卫拦截,回传 `DEVICE_NOT_ACTIVATED`
+- [ ] 正在播放时收到搜索 / 分类命令先退出再跳转
+- [ ] 2 秒内连续多次控制命令只保留最后一条(`throttle_test.sh`)
+- [ ] 第三方收到成功 / 失败回传(`listen_callback.sh` 配合 logcat)
+- [ ] `SELECT_INDEX` / `GOTO_PAGE` 在播放页列表中正确选中
+- [ ] 白名单外的 `sourceApp` 被拦截、不回传(`security_test.sh`)
+
+## 7. 调试技巧
+
+```bash
+# 查看应用包内注册的 Receiver
+adb shell dumpsys package com.pukka.jiecheng | grep -i receiver
+
+# 查看设备上所有待发广播
+adb shell dumpsys activity broadcasts
+
+# 查看最近一次 am broadcast 的返回结果(带 requestId 过滤)
+adb shell am broadcast -a com.pukka.jiecheng.action.VOICE_COMMAND -n ... --es requestId probe
+
+# 实时过滤 voice 模块日志
+adb logcat -v threadtime VoiceCommand:V VoiceCommandLogger:V "*:S"
+```
\ No newline at end of file
diff --git a/test/voice_helper/cold_start.sh b/test/voice_helper/cold_start.sh
new file mode 100644
index 0000000..a3f9f61
--- /dev/null
+++ b/test/voice_helper/cold_start.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+# cold_start.sh - 冷启动单片跳转测试
+# 流程: force-stop -> 投递协议 -> 第三方拉起 MainActivity -> 等待启动链路完成 -> 排队命令回放
+# 用法: cold_start.sh [filmId] [filmName]
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_receiver_exists
+
+FILM_ID="${1:-film-demo-001}"
+FILM_NAME="${2:-冷启动测试片}"
+
+print_header "冷启动单片跳转测试"
+
+# 1. 强制停止应用,模拟完全未启动状态
+force_stop_app
+sleep 1
+
+# 2. 第三方先投递协议(应用未启动,命令应进入 CommandQueue 排队)
+REQUEST_ID=$(gen_request_id)
+PAYLOAD=$(build_jump_payload FILM \
+ "filmId=${FILM_ID}" \
+ "filmName=${FILM_NAME}" \
+ "autoPlay=true")
+
+log_info "[1/4] 应用未启动时投递协议"
+send_voice_broadcast "JUMP" "${PAYLOAD}" "${REQUEST_ID}"
+
+# 3. 第三方再显式拉起 MainActivity
+sleep 1
+log_info "[2/4] 第三方显式拉起 MainActivity"
+launch_main_activity
+
+# 4. 等待启动链路完成(激活 / 升级 / 会员校验)
+log_info "[3/4] 等待启动链路完成"
+wait_for_app_ready 15000 || true
+
+# 5. 给排队命令回放留出时间
+log_info "[4/4] 等待排队命令消费(约 5 秒)"
+sleep 5
+
+log_ok "冷启动测试结束"
+log_info "requestId=${REQUEST_ID},请人工验证 VideoDetailActivity 是否进入"
\ No newline at end of file
diff --git a/test/voice_helper/control.sh b/test/voice_helper/control.sh
new file mode 100644
index 0000000..42be32d
--- /dev/null
+++ b/test/voice_helper/control.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# control.sh - 播放控制测试
+# 用法: control.sh [k=v ...]
+# action 取值(与文档 §6.1 CONTROL action 字段保持一致):
+# PLAY | PAUSE | FULLSCREEN_ENTER | FULLSCREEN_EXIT |
+# SEEK_FORWARD | SEEK_BACKWARD | SEEK_TO |
+# PREV | NEXT | SELECT_INDEX | PAGE_NEXT | PAGE_PREV | GOTO_PAGE
+#
+# 示例:
+# control.sh PLAY
+# control.sh SEEK_FORWARD seconds=10
+# control.sh SEEK_TO seconds=300
+# control.sh SELECT_INDEX index=3
+# control.sh GOTO_PAGE page=2
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_receiver_exists
+
+if [ $# -lt 1 ]; then
+ log_err "缺少 action"
+ echo "用法: $0 [k=v ...]" >&2
+ exit 1
+fi
+
+ACTION="$1"; shift
+
+print_header "播放控制 CONTROL/${ACTION}"
+PAYLOAD=$(build_control_payload "${ACTION}" "$@")
+send_voice_broadcast "CONTROL" "${PAYLOAD}"
+log_ok "预期在播放页执行 ${ACTION}"
\ No newline at end of file
diff --git a/test/voice_helper/device.sh b/test/voice_helper/device.sh
new file mode 100644
index 0000000..55803a2
--- /dev/null
+++ b/test/voice_helper/device.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+# device.sh - 测试目标设备配置
+# 所有 voice_helper 下的脚本通过 lib/common.sh 自动 source 本文件
+#
+# 用法:
+# 多设备场景:把 ADB_SERIAL 改成目标设备的 serial
+# 单设备场景:留空(注释掉 export 行),common.sh 会自动选用唯一在线设备
+#
+# 优先级:命令行 export > 本文件 > common.sh 默认空
+# ADB_SERIAL=xxx ./jump_film.sh # 命令行覆盖本文件
+
+# 当前测试机:1f66b893154cf507
+ADB_SERIAL="${ADB_SERIAL:-1f66b893154cf507}"
+export ADB_SERIAL
diff --git a/test/voice_helper/jump_category.sh b/test/voice_helper/jump_category.sh
new file mode 100644
index 0000000..a0f7b59
--- /dev/null
+++ b/test/voice_helper/jump_category.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# jump_category.sh - 分类跳转测试
+# 用法: jump_category.sh [categoryId] [categoryName]
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_receiver_exists
+
+CATEGORY_ID="${1:-cat-demo-001}"
+CATEGORY_NAME="${2:-动作片}"
+
+print_header "分类跳转 JUMP/CATEGORY"
+PAYLOAD=$(build_jump_payload CATEGORY \
+ "categoryId=${CATEGORY_ID}" \
+ "categoryName=${CATEGORY_NAME}")
+
+send_voice_broadcast "JUMP" "${PAYLOAD}"
+log_ok "预期进入 ListActivity 并加载该分类"
\ No newline at end of file
diff --git a/test/voice_helper/jump_film.sh b/test/voice_helper/jump_film.sh
new file mode 100644
index 0000000..870653d
--- /dev/null
+++ b/test/voice_helper/jump_film.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+# jump_film.sh - 单片跳转测试
+# 用法: jump_film.sh [filmId] [filmName] [autoPlay]
+# filmId 默认 film-demo-001
+# filmName 默认等于 filmId(仅一个参数时避免重复输入)
+# autoPlay true / false,默认 true
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_receiver_exists
+
+FILM_ID="${1:-film-demo-001}"
+# 缺省 filmName 时与 filmId 同名,避免用户重复输入
+FILM_NAME="${2:-${FILM_ID}}"
+AUTO_PLAY="${3:-true}"
+
+print_header "单片跳转 JUMP/FILM"
+PAYLOAD=$(build_jump_payload FILM \
+ "filmId=${FILM_ID}" \
+ "filmName=${FILM_NAME}" \
+ "autoPlay=${AUTO_PLAY}")
+
+send_voice_broadcast "JUMP" "${PAYLOAD}"
+log_ok "预期进入 VideoDetailActivity(autoPlay=${AUTO_PLAY})"
\ No newline at end of file
diff --git a/test/voice_helper/lib/common.sh b/test/voice_helper/lib/common.sh
new file mode 100644
index 0000000..4017060
--- /dev/null
+++ b/test/voice_helper/lib/common.sh
@@ -0,0 +1,342 @@
+#!/usr/bin/env bash
+# common.sh - 语音协议广播测试公共函数
+# 其他脚本通过 source "$(dirname "$0")/lib/common.sh" 加载
+#
+# 协议参考 docs/2026-08-22-语音影视接入可行性评估.md §6.1
+
+set -o pipefail
+
+# 加载 device.sh 设备配置(位于 common.sh 同级父目录)
+# 必须在协议常量前 source,确保 ADB_SERIAL 已被设置
+_DVH_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+if [ -f "${_DVH_LIB_DIR}/../device.sh" ]; then
+ # shellcheck disable=SC1091
+ source "${_DVH_LIB_DIR}/../device.sh"
+fi
+unset _DVH_LIB_DIR
+
+# ===== 协议常量(与文档 §6.1 保持一致)=====
+: "${ACTION_VOICE_COMMAND:=com.pukka.jiecheng.action.VOICE_COMMAND}"
+: "${PERMISSION_VOICE:=com.pukka.jiecheng.permission.VOICE_COMMAND}"
+# 应用 applicationId:默认商用版(tv.huashi.jetsenx.jetsenfilx.jsttv);
+# 家用版 APK 安装时通过 APP_PKG=tv.huashi.jetsenx.jetsenfilx.jsttv.home 覆盖。
+# 下方 RECEIVER_COMPONENT / SKYWORTH_RECEIVER_COMPONENT / TEST_APP_PKG 都基于 APP_PKG 派生。
+: "${APP_PKG:=tv.huashi.jetsenx.jetsenfilx.jsttv}"
+: "${RECEIVER_COMPONENT:=${APP_PKG}/com.pukka.jiecheng.voice.receiver.VoiceCommandReceiver}"
+: "${TEST_APP_PKG:=${APP_PKG}}"
+: "${MAIN_ACTIVITY_COMPONENT:=com.pukka.jiecheng/.MainActivity}"
+: "${DEFAULT_SOURCE_APP:=com.test.voicehelper}"
+: "${DEFAULT_PLATFORM:=vendorA}"
+: "${DEFAULT_CALLBACK_ACTION:=com.test.voicehelper.action.VOICE_CALLBACK}"
+: "${TEST_REQUEST_PREFIX:=vt-}"
+
+# ===== 创维(skyworth)广播常量(SkyworthVoiceAdapter.java)=====
+# action 与 extras key 与 SkyworthVoiceAdapter.ACTION_VOD / EXTRA_VIDEO_LIST 保持一致
+: "${ACTION_SKYWORTH_VOD:=com.skyworth.smarthotel.action.voice.RecognizeResult.vod}"
+: "${SKYWORTH_RECEIVER_COMPONENT:=${APP_PKG}/com.pukka.jiecheng.voice.adapter.SkyworthVoiceAdapter}"
+# 白名单 sender(SkyworthVoiceAdapter.SENDER_WHITELIST),adb 投递时通过 --p 指定
+# 发送方包名模拟创维 SDK,绕开白名单校验的测试见 skyworth_security_test.sh
+: "${DEFAULT_SKYWORTH_SENDER:=com.skyworth.smarthotel}"
+
+# 当开发期把 permission 临时降到 normal / 关闭时可置 1
+: "${SKIP_PERMISSION_CHECK:=0}"
+
+# 当本机 adb 上挂了多台设备时,通过 ADB_SERIAL 指定唯一目标,避免脚本误连
+: "${ADB_SERIAL:=}"
+if [ -n "${ADB_SERIAL}" ]; then
+ ADB="adb -s ${ADB_SERIAL}"
+else
+ ADB="adb"
+fi
+
+# ===== 颜色输出(仅在交互终端生效)=====
+if [ -t 1 ] && command -v tput >/dev/null 2>&1 && [ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]; then
+ C_RED=$(tput setaf 1); C_GRN=$(tput setaf 2); C_YEL=$(tput setaf 3)
+ C_CYN=$(tput setaf 6); C_RST=$(tput sgr0)
+else
+ C_RED=""; C_GRN=""; C_YEL=""; C_CYN=""; C_RST=""
+fi
+
+log_info() { printf "%s[INFO]%s %s\n" "${C_CYN}" "${C_RST}" "$*"; }
+log_ok() { printf "%s[OK]%s %s\n" "${C_GRN}" "${C_RST}" "$*"; }
+log_warn() { printf "%s[WARN]%s %s\n" "${C_YEL}" "${C_RST}" "$*"; }
+log_err() { printf "%s[ERR]%s %s\n" "${C_RED}" "${C_RST}" "$*" >&2; }
+
+# ===== 校验 =====
+require_adb() {
+ if ! command -v adb >/dev/null 2>&1; then
+ log_err "未找到 adb,请安装 Android Platform Tools 后重试"
+ exit 1
+ fi
+ if ! ${ADB} get-state >/dev/null 2>&1; then
+ log_err "adb 未连接设备,请先执行 adb connect / adb devices"
+ exit 1
+ fi
+
+ # 解析 adb devices 输出,仅保留 device 状态(排除 unauthorized / offline)
+ local online count
+ online=$(${ADB} devices | awk 'NR>1 && $2=="device" {print $1}')
+ count=$(printf '%s\n' "${online}" | grep -c .)
+
+ if [ -n "${ADB_SERIAL}" ]; then
+ if ! printf '%s\n' "${online}" | grep -qx "${ADB_SERIAL}"; then
+ log_err "指定的 ADB_SERIAL=${ADB_SERIAL} 未处于 device 状态,请检查 adb devices"
+ exit 1
+ fi
+ elif [ "${count}" -gt 1 ]; then
+ log_err "检测到 ${count} 个在线设备,请先 export ADB_SERIAL= 指定目标设备,例如:"
+ log_err " ADB_SERIAL=$(printf '%s' "${online}" | head -n1) ./jump_film.sh \"镖人\" true"
+ exit 1
+ else
+ # 只有一个设备时默认使用它,避免每次都要 export
+ export ADB_SERIAL="${online}"
+ log_info "自动选择唯一在线设备:${ADB_SERIAL}"
+ fi
+}
+
+check_receiver_exists() {
+ local found
+ found=$(${ADB} shell dumpsys package "${TEST_APP_PKG}" 2>/dev/null \
+ | grep -i "VoiceCommandReceiver" || true)
+ if [ -z "${found}" ]; then
+ log_warn "VoiceCommandReceiver 未在 ${TEST_APP_PKG} 中注册"
+ log_warn "请确认 voice 模块已实现并打包进 APK,预期 Broadcast 会被丢弃"
+ fi
+}
+
+# 校验创维 Adapter 已注册(Manifest 中 SkyworthVoiceAdapter + action)
+check_skyworth_adapter_exists() {
+ local found
+ found=$(${ADB} shell dumpsys package "${TEST_APP_PKG}" 2>/dev/null \
+ | grep -i "SkyworthVoiceAdapter" || true)
+ if [ -z "${found}" ]; then
+ log_warn "SkyworthVoiceAdapter 未在 ${TEST_APP_PKG} 中注册"
+ log_warn "请确认创维语音 Adapter 已打包进 APK,预期 Broadcast 会被丢弃"
+ fi
+}
+
+# ===== 工具函数 =====
+gen_request_id() {
+ local rand
+ rand=$(printf '%05d' "$((RANDOM % 100000))")
+ printf '%s%d-%s' "${TEST_REQUEST_PREFIX}" "$(date +%s)" "${rand}"
+}
+
+json_escape() {
+ local v="$1"
+ v="${v//\\/\\\\}"
+ v="${v//\"/\\\"}"
+ printf '%s' "${v}"
+}
+
+# JUMP / SEARCH payload(全部字段均为字符串)
+# 用法: build_jump_payload "FILM" "filmId=xxx" "filmName=yyy" "autoPlay=true"
+build_jump_payload() {
+ local jump_type="$1"; shift
+ local first=1 json="{"
+ json="${json}\"jumpType\":\"${jump_type}\""
+ while [ $# -gt 0 ]; do
+ local kv="$1" key val
+ key="${kv%%=*}"
+ val="${kv#*=}"
+ val=$(json_escape "${val}")
+ json="${json},\"${key}\":\"${val}\""
+ shift
+ done
+ json="${json}}"
+ printf '%s' "${json}"
+}
+
+build_search_payload() {
+ local first=1 json="{"
+ while [ $# -gt 0 ]; do
+ local kv="$1" key val
+ key="${kv%%=*}"
+ val="${kv#*=}"
+ val=$(json_escape "${val}")
+ if [ $first -eq 1 ]; then first=0; else json="${json},"; fi
+ json="${json}\"${key}\":\"${val}\""
+ shift
+ done
+ json="${json}}"
+ printf '%s' "${json}"
+}
+
+# CONTROL payload:第一个参数是 action,其余 key=value 中 value 视为数字字面量
+# 用法: build_control_payload "PAUSE"
+# build_control_payload "SEEK_FORWARD" "seconds=10"
+build_control_payload() {
+ local action="$1"; shift
+ local json="{\"action\":\"${action}\""
+ while [ $# -gt 0 ]; do
+ local kv="$1" key val
+ key="${kv%%=*}"
+ val="${kv#*=}"
+ json="${json},\"${key}\":${val}"
+ shift
+ done
+ json="${json}}"
+ printf '%s' "${json}"
+}
+
+# ===== 核心发送函数 =====
+# 用法: send_voice_broadcast [requestId] [sourceApp] [sourcePlatform] [callbackAction]
+send_voice_broadcast() {
+ local command_type="$1"
+ local payload_json="$2"
+ local request_id="${3:-$(gen_request_id)}"
+ local source_app="${4:-${DEFAULT_SOURCE_APP}}"
+ local source_platform="${5:-${DEFAULT_PLATFORM}}"
+ local callback_action="${6:-${DEFAULT_CALLBACK_ACTION}}"
+
+ require_adb
+
+ local timestamp
+ timestamp=$(date +%s%3N 2>/dev/null || echo $(( $(date +%s) * 1000 )))
+
+ log_info "commandType=${command_type} requestId=${request_id}"
+ log_info "sourceApp=${source_app} platform=${source_platform}"
+ log_info "payload=${payload_json}"
+
+ # adb shell 接收一个字符串后整体交给设备 shell 执行,避免 adb 自身再做 token 化
+ ${ADB} shell "am broadcast \
+ -a '${ACTION_VOICE_COMMAND}' \
+ -n '${RECEIVER_COMPONENT}' \
+ --es requestId '${request_id}' \
+ --es sourceApp '${source_app}' \
+ --es sourcePlatform '${source_platform}' \
+ --es commandType '${command_type}' \
+ --es payload '${payload_json}' \
+ --es callbackAction '${callback_action}' \
+ --el timestamp '${timestamp}' \
+ --include-stopped-packages" 2>&1 | sed 's/^/ [adb] /'
+
+ log_ok "已投递(requestId=${request_id})"
+}
+
+# ===== 创维(skyworth)广播 =====
+# 模拟创维 SDK 投递 RecognizeResult.vod 广播给 SkyworthVoiceAdapter。
+#
+# 关于 videoList 的说明:
+# 创维 SDK 用 --es 投递 JSON 数组字符串(与 SkyworthVoiceAdapter.resolveVideoList 对应):
+# am broadcast -a com.skyworth.smarthotel.action.voice.RecognizeResult.vod \
+# --es videoList '["哪吒之魔童降世","我是哪吒"]'
+# Adapter 取第一个非空元素,多值场景可模拟创维候选列表。
+#
+# 可选参数(通过环境变量控制,均为可选):
+# SKYWORTH_SKIP_COMPONENT=1 省略 -n 参数(不显式指定 Receiver 组件)
+# SKYWORTH_SKIP_SENDER=1 省略 -p 参数(不指定 sender 包名,Adapter 内部用白名单默认值兜底)
+#
+# 用法: send_skyworth_broadcast [senderPkg] [extraKv...]
+# filmName 候选影片名(videoList[0])
+# senderPkg 发送方包名,默认 DEFAULT_SKYWORTH_SENDER(白名单内);传空串或 SKYWORTH_SKIP_SENDER=1 时省略 -p
+# extraKv 透传给 am broadcast 的附加 --es 参数,形如 "key=value"
+send_skyworth_broadcast() {
+ local film_name="$1"
+ local sender_pkg="${2:-${DEFAULT_SKYWORTH_SENDER}}"
+ shift 2 2>/dev/null || shift $# # 兼容只传 filmName 的情况
+
+ require_adb
+
+ # videoList 为 JSON 数组字符串(--es),与创维 SDK 真实投递格式一致
+ local video_list_json
+ video_list_json="[\"$(json_escape "${film_name}")\"]"
+
+ log_info "action=${ACTION_SKYWORTH_VOD}"
+ if [ -n "${SKYWORTH_SKIP_SENDER}" ] || [ -z "${sender_pkg}" ]; then
+ log_info "sender=SKIP(-p omitted) videoList=${video_list_json}"
+ else
+ log_info "sender=${sender_pkg} videoList=${video_list_json}"
+ fi
+ if [ -n "${SKYWORTH_SKIP_COMPONENT}" ]; then
+ log_info "component=SKIP(-n omitted)"
+ fi
+
+ # 附加 extras(可选):默认注入 autoPlay=true(创维链路默认自动播片)
+ # 用户传的 extraKv 会追加在后面,按 am broadcast 规则后者覆盖前者,
+ # 因此调用方通过 "autoPlay=false" 即可临时关闭自动播放。
+ local extra_args="--es autoPlay 'true' "
+ local kv
+ for kv in "$@"; do
+ local key val
+ key="${kv%%=*}"
+ val="${kv#*=}"
+ extra_args="${extra_args}--es ${key} '${val}' "
+ done
+
+ # 组装 -n 和 -p(按开关可选)
+ local n_arg=""
+ local p_arg=""
+ if [ -z "${SKYWORTH_SKIP_COMPONENT}" ]; then
+ n_arg="-n '${SKYWORTH_RECEIVER_COMPONENT}' "
+ fi
+ if [ -z "${SKYWORTH_SKIP_SENDER}" ] && [ -n "${sender_pkg}" ]; then
+ p_arg="-p '${sender_pkg}' "
+ fi
+
+ ${ADB} shell "am broadcast \
+ -a '${ACTION_SKYWORTH_VOD}' \
+ ${n_arg}${p_arg}--es videoList '${video_list_json}' \
+ ${extra_args}--include-stopped-packages" 2>&1 | sed 's/^/ [adb] /'
+
+ log_ok "已投递创维 VOD 广播(filmName=${film_name})"
+}
+
+# ===== 应用级操作 =====
+launch_main_activity() {
+ log_info "拉起 MainActivity:${MAIN_ACTIVITY_COMPONENT}"
+ ${ADB} shell am start -n "${MAIN_ACTIVITY_COMPONENT}" >/dev/null 2>&1
+}
+
+force_stop_app() {
+ log_info "强制停止应用:${TEST_APP_PKG}"
+ ${ADB} shell am force-stop "${TEST_APP_PKG}" >/dev/null 2>&1
+}
+
+# 验证当前前台 Activity 是否为预期页面(按类名片段匹配)
+# 用法: wait_for_activity [timeoutMs]
+# 返回 0 表示命中,1 表示超时
+wait_for_activity() {
+ local activity_name="${1}"
+ local timeout_ms="${2:-10000}"
+ local interval_ms=500 elapsed=0
+ log_info "等待前台 Activity 命中 ${activity_name}(最长 ${timeout_ms}ms)..."
+ while [ ${elapsed} -lt "${timeout_ms}" ]; do
+ local top
+ top=$(${ADB} shell "dumpsys window 2>/dev/null | grep -oE 'mCurrentFocus=[^}]+' | head -n 1" || true)
+ case "${top}" in
+ *"${activity_name}"*)
+ log_ok "前台命中:${top}"
+ return 0
+ ;;
+ esac
+ sleep "$(awk "BEGIN { print ${interval_ms}/1000 }")"
+ elapsed=$((elapsed + interval_ms))
+ done
+ log_warn "等待超时(${timeout_ms}ms),当前前台:$(${ADB} shell "dumpsys window 2>/dev/null | grep -oE 'mCurrentFocus=[^}]+' | head -n 1" || true)"
+ return 1
+}
+
+wait_for_app_ready() {
+ local timeout_ms="${1:-15000}"
+ local interval_ms=300 elapsed=0
+ log_info "等待应用启动链路完成(最长 ${timeout_ms}ms)..."
+ while [ ${elapsed} -lt "${timeout_ms}" ]; do
+ local top
+ top=$(${ADB} shell "dumpsys window 2>/dev/null | grep -oE 'mCurrentFocus=[^}]+' | head -n 1" || true)
+ case "${top}" in
+ *"${TEST_APP_PKG}"* | *"com.pukka.jiecheng"* | *"${MAIN_ACTIVITY_COMPONENT%%/*}"*)
+ log_ok "应用已就绪:${top}"
+ return 0
+ ;;
+ esac
+ sleep "$(awk "BEGIN { print ${interval_ms}/1000 }")"
+ elapsed=$((elapsed + interval_ms))
+ done
+ log_warn "等待超时(${timeout_ms}ms),继续执行"
+ return 1
+}
+
+print_header() {
+ printf "\n%s================ %s ================%s\n" "${C_CYN}" "$*" "${C_RST}"
+}
\ No newline at end of file
diff --git a/test/voice_helper/listen_callback.sh b/test/voice_helper/listen_callback.sh
new file mode 100644
index 0000000..fad4e07
--- /dev/null
+++ b/test/voice_helper/listen_callback.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+# listen_callback.sh - 监听反向广播回调
+# 用法: listen_callback.sh [requestId]
+# 不传 requestId: 监听所有 VoiceCommand 相关日志
+# 传入 requestId: 仅过滤该请求的回调日志
+#
+# 实现:adb logcat 过滤,由 voice 模块按 requestId 写入 logcat(推荐 tag: VoiceCommand)
+# 也可通过 dumpsys activity broadcasts 查看待发/已发广播
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+
+REQUEST_ID="${1:-}"
+FILTER_TAG="${LOG_TAG:-VoiceCommand}"
+
+print_header "监听反向广播回调"
+log_info "tag=${FILTER_TAG} requestId=${REQUEST_ID:-<全部>}"
+log_info "退出: Ctrl+C"
+log_warn "请确认 voice 模块在回传时调用 Log.i(\"${FILTER_TAG}\", ...) 打印 requestId/resultCode"
+
+adb logcat -c
+${ADB:-adb} logcat -v threadtime "${FILTER_TAG}:V" "*:S" \
+ | while IFS= read -r line; do
+ if [ -z "${REQUEST_ID}" ] || [[ "${line}" == *"${REQUEST_ID}"* ]]; then
+ printf "%s[CALLBACK]%s %s\n" "${C_GRN}" "${C_RST}" "${line}"
+ fi
+ done
\ No newline at end of file
diff --git a/test/voice_helper/run_all.sh b/test/voice_helper/run_all.sh
new file mode 100644
index 0000000..fb3f410
--- /dev/null
+++ b/test/voice_helper/run_all.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+# run_all.sh - 一键顺序执行全部用例
+# 适用于联调验证:冷启动 -> 单片跳转(应进入播放页)-> 节流 -> 全屏 -> 分类跳转 -> 搜索 -> 安全 -> 创维链路
+# 用法: run_all.sh [all|unified|skyworth]
+# all 全部执行(默认,统一协议 + 创维)
+# unified 只跑统一协议链路(VoiceCommandReceiver)
+# skyworth 只跑创维链路(SkyworthVoiceAdapter)
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_receiver_exists
+
+SCOPE="${1:-all}"
+
+run_unified() {
+ print_header "Run All: 冷启动 -> 单片跳转 -> 节流 -> 全屏 -> 分类 -> 搜索 -> 安全"
+ log_warn "各步骤之间会 sleep 等待页面稳定,请勿中途打断"
+
+ # 1. 冷启动单片跳转(force-stop + 投递 + 拉起)
+ "${SCRIPT_DIR}/cold_start.sh"
+ sleep 5
+
+ # 2. 节流测试(需在播放页)
+ "${SCRIPT_DIR}/throttle_test.sh" PAUSE 5
+ sleep 2
+
+ # 3. 全屏切换
+ "${SCRIPT_DIR}/control.sh" FULLSCREEN_ENTER
+ sleep 2
+ "${SCRIPT_DIR}/control.sh" FULLSCREEN_EXIT
+ sleep 2
+
+ # 4. 分类跳转(会退出播放态)
+ "${SCRIPT_DIR}/jump_category.sh"
+ sleep 3
+
+ # 5. 语音搜索
+ "${SCRIPT_DIR}/search.sh" "测试片"
+ sleep 3
+
+ # 6. 安全校验(非白名单 sourceApp)
+ "${SCRIPT_DIR}/security_test.sh"
+ sleep 1
+}
+
+run_skyworth() {
+ print_header "Run Skyworth: 创维 VOD 链路(正常 -> 空候选 -> 多候选 -> 安全 -> 冷启动)"
+
+ # 1. 正常点播(白名单 sender + 有效片名)
+ "${SCRIPT_DIR}/skyworth_vod.sh"
+ sleep 3
+
+ # 2. 空候选(应 dropped)
+ "${SCRIPT_DIR}/skyworth_empty_test.sh" empty
+ sleep 2
+
+ # 3. 多候选跳空(应取首个非空)
+ "${SCRIPT_DIR}/skyworth_empty_test.sh" multi
+ sleep 3
+
+ # 4. 安全校验(非白名单 sender,应 dropped)
+ "${SCRIPT_DIR}/skyworth_security_test.sh"
+ sleep 2
+
+ # 5. 冷启动(force-stop + 排队 + 回放)
+ "${SCRIPT_DIR}/skyworth_cold_start.sh"
+}
+
+case "${SCOPE}" in
+ unified) run_unified ;;
+ skyworth) run_skyworth ;;
+ all)
+ run_unified
+ sleep 3
+ run_skyworth
+ ;;
+ *)
+ echo "用法: $0 [all|unified|skyworth]" >&2
+ exit 1
+ ;;
+esac
+
+print_header "全部用例执行完毕"
+log_ok "请结合 logcat / dumpsys 验证执行链路与回传"
diff --git a/test/voice_helper/search.sh b/test/voice_helper/search.sh
new file mode 100644
index 0000000..0dec67d
--- /dev/null
+++ b/test/voice_helper/search.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+# search.sh - 语音搜索测试
+# 用法: search.sh [actorName] [directorName] [category] [unionStyle] [region] [releaseDate]
+# 示例: search.sh "流浪地球"
+# search.sh "流浪地球" "吴京"
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_receiver_exists
+
+if [ $# -lt 1 ]; then
+ log_err "至少需要一个搜索关键词"
+ echo "用法: $0 [actorName] [directorName] [category] [unionStyle] [region] [releaseDate]" >&2
+ exit 1
+fi
+
+# 按协议 §6.1 SEARCH payload 字段顺序做位置映射
+KEYS=(filmName actorName directorName category unionStyle region releaseDate)
+
+print_header "语音搜索 SEARCH"
+
+ARGS=()
+for i in $(seq 1 $#); do
+ idx=$((i - 1))
+ if [ $idx -lt ${#KEYS[@]} ]; then
+ val="${!i}"
+ if [ -n "${val}" ]; then
+ ARGS+=("${KEYS[$idx]}=${val}")
+ fi
+ fi
+done
+
+PAYLOAD=$(build_search_payload "${ARGS[@]}")
+send_voice_broadcast "SEARCH" "${PAYLOAD}"
+log_ok "预期进入 SearchActivity 并自动搜索,焦点落首张结果卡片"
\ No newline at end of file
diff --git a/test/voice_helper/security_test.sh b/test/voice_helper/security_test.sh
new file mode 100644
index 0000000..d25fadf
--- /dev/null
+++ b/test/voice_helper/security_test.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# security_test.sh - 安全校验测试
+# 用一个非白名单 sourceApp 投递协议,验证被守卫拦截(文档 §6.2)
+# 用法: security_test.sh [fakeSourceApp]
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+
+FAKE_APP="${1:-com.attacker.malicious}"
+print_header "安全校验测试(sourceApp=${FAKE_APP})"
+
+PAYLOAD=$(build_jump_payload FILM \
+ "filmId=hack-001" \
+ "filmName=白盒测试片")
+
+send_voice_broadcast "JUMP" "${PAYLOAD}" "$(gen_request_id)" "${FAKE_APP}"
+log_ok "投递完成,预期 sourceApp 未在白名单时被丢弃,不回传结果"
+log_warn "(如果脚本同时验证 permission 拦截,可执行:adb shell pm list permissions | grep VOICE)"
\ No newline at end of file
diff --git a/test/voice_helper/skyworth_cold_start.sh b/test/voice_helper/skyworth_cold_start.sh
new file mode 100644
index 0000000..d96bdfd
--- /dev/null
+++ b/test/voice_helper/skyworth_cold_start.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+# skyworth_cold_start.sh - 创维 VOD 冷启动测试(两阶段跳转:Adapter 阶段1先唤醒→阶段2导航就绪回放)
+# 流程: force-stop -> 投递创维广播(Adapter 内部 enqueueForColdStart + 启动 IndexActivity)
+# -> IndexActivity 首帧就绪后自动 replayColdStartQueue -> 进入详情/搜索页
+# 用法: skyworth_cold_start.sh [filmName]
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_skyworth_adapter_exists
+
+FILM_NAME="${1:-冷启动测试片}"
+
+print_header "创维 VOD 冷启动测试(filmName=${FILM_NAME})"
+
+# 1. 强制停止应用,模拟完全未启动状态
+force_stop_app
+sleep 1
+
+# 2. 投广播(Adapter 未激活时:enqueue 命令 + 自己 startActivity(IndexActivity) 唤醒应用)
+# 不再手动 launch_main_activity,更贴近 OEM 真实流程。
+log_info "[1/3] 应用未启动时投递创维广播(Adapter 内部唤醒+排队)"
+send_skyworth_broadcast "${FILM_NAME}"
+
+# 3. 等应用进程启动 + 导航就绪 + 首帧命令回放(coldStartQueue drainAll 一次性消费)
+log_info "[2/3] 等待进程启动 + 导航就绪 + 命令回放(约 10 秒)"
+sleep 10
+
+# 4. 给搜索请求+详情页跳转留出时间(网络请求+页面渲染)
+log_info "[3/3] 等待详情页跳转(约 5 秒)"
+sleep 5
+
+log_ok "冷启动测试结束"
+log_info "请人工验证是否进入 VideoDetailActivity / SearchActivity"
+log_info "若停留在 IndexActivity 未跳转,请 logcat 过滤 VoiceCommandLogger 查看 enqueue/replay/guard 链路日志"
diff --git a/test/voice_helper/skyworth_empty_test.sh b/test/voice_helper/skyworth_empty_test.sh
new file mode 100644
index 0000000..5e9f4b7
--- /dev/null
+++ b/test/voice_helper/skyworth_empty_test.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+# skyworth_empty_test.sh - 创维 videoList 空候选测试
+# 投递 videoList 为空串的广播,验证 Adapter firstNonEmpty() 后走 dropped 分支
+#
+# 可选参数(通过环境变量控制,均为可选):
+# SKYWORTH_SKIP_COMPONENT=1 省略 -n 参数(不显式指定 Receiver 组件)
+# SKYWORTH_SKIP_SENDER=1 省略 -p 参数(不指定 sender 包名,Adapter 内部用白名单默认值兜底)
+#
+# 用法: skyworth_empty_test.sh [mode]
+# mode: empty 只投一个空串候选(默认)
+# multi 投 [空串, 真实片名],验证跳过空候选取首个非空
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_skyworth_adapter_exists
+
+MODE="${1:-empty}"
+
+# 组装 -n 和 -p(按开关可选,与 send_skyworth_broadcast 保持一致)
+n_arg=""
+p_arg=""
+if [ -z "${SKYWORTH_SKIP_COMPONENT}" ]; then
+ n_arg="-n '${SKYWORTH_RECEIVER_COMPONENT}' "
+fi
+if [ -z "${SKYWORTH_SKIP_SENDER}" ]; then
+ p_arg="-p '${DEFAULT_SKYWORTH_SENDER}' "
+fi
+
+print_header "创维 videoList 空候选测试(mode=${MODE})"
+
+if [ "${MODE}" = "multi" ]; then
+ # JSON 数组多个候选,模拟创维识别返回多个结果且首个为空
+ log_info "投递 [\"\", \"镖人\"],预期跳过空候选、取 \"镖人\" 走正常链路"
+ ${ADB} shell "am broadcast \
+ -a '${ACTION_SKYWORTH_VOD}' \
+ ${n_arg}${p_arg}--es videoList '[\"\", \"镖人\"]' \
+ --include-stopped-packages" 2>&1 | sed 's/^/ [adb] /'
+ log_ok "预期进入 VideoDetailActivity 或 SearchActivity"
+else
+ log_info "投递 [\"\"],预期 Adapter 判定 videoList empty 走 dropped"
+ ${ADB} shell "am broadcast \
+ -a '${ACTION_SKYWORTH_VOD}' \
+ ${n_arg}${p_arg}--es videoList '[\"\"]' \
+ --include-stopped-packages" 2>&1 | sed 's/^/ [adb] /'
+ log_ok "预期被丢弃,无任何页面跳转(logcat: dropped skyworth videoList empty)"
+fi
+
+log_warn "验证方法:adb logcat -v threadtime VoiceCommand:V '*:S' 过滤 dropped 日志"
diff --git a/test/voice_helper/skyworth_security_test.sh b/test/voice_helper/skyworth_security_test.sh
new file mode 100644
index 0000000..6fd2ed0
--- /dev/null
+++ b/test/voice_helper/skyworth_security_test.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# skyworth_security_test.sh - 创维广播白名单校验测试
+# 用非白名单 sender 包名投递创维 VOD 广播,验证被 SkyworthVoiceAdapter 丢弃
+# 用法: skyworth_security_test.sh [fakeSenderPkg]
+# fakeSenderPkg 默认 com.attacker.malicious
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_skyworth_adapter_exists
+
+FAKE_SENDER="${1:-com.attacker.malicious}"
+
+print_header "创维广播安全校验(sender=${FAKE_SENDER})"
+send_skyworth_broadcast "越权点播片" "${FAKE_SENDER}"
+
+log_ok "投递完成,预期 sender 不在白名单时被丢弃(logcat: dropped sender not in whitelist)"
+log_warn "验证方法:adb logcat -v threadtime VoiceCommand:V '*:S' 过滤 dropped 日志"
diff --git a/test/voice_helper/skyworth_vod.sh b/test/voice_helper/skyworth_vod.sh
new file mode 100644
index 0000000..e0a5f30
--- /dev/null
+++ b/test/voice_helper/skyworth_vod.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+# skyworth_vod.sh - 创维 VOD 语音点播正常路径测试
+# 模拟创维 SDK 投递 RecognizeResult.vod 广播(白名单 sender + videoList),
+# 验证链路:SkyworthVoiceAdapter -> SkyworthVodParser -> OPEN_FILM_DETAIL ->
+# DefaultPageNavigator.searchAndOpenFilmDetail()
+# 用法: skyworth_vod.sh [filmName] [senderPkg]
+# filmName 默认 "镖人"(需是片库中真实存在的片名,否则走 SearchActivity 兜底)
+# senderPkg 默认白名单 com.skyworth.smarthotel
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_skyworth_adapter_exists
+
+FILM_NAME="${1:-镖人}"
+SENDER_PKG="${2:-${DEFAULT_SKYWORTH_SENDER}}"
+
+print_header "创维 VOD 点播(filmName=${FILM_NAME})"
+send_skyworth_broadcast "${FILM_NAME}" "${SENDER_PKG}"
+
+log_ok "预期进入 VideoDetailActivity(影片名命中时)或 SearchActivity(无命中兜底)"
+log_info "验证方法:观察前台页面,或 logcat 过滤 tag=VoiceCommand 查看 parsed 日志"
diff --git a/test/voice_helper/throttle_test.sh b/test/voice_helper/throttle_test.sh
new file mode 100644
index 0000000..f806823
--- /dev/null
+++ b/test/voice_helper/throttle_test.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+# throttle_test.sh - 节流测试
+# 同类 CONTROL 命令 2 秒内连续投递多条,验证只保留最后一条(文档 §7.5)
+# 用法: throttle_test.sh [action] [count]
+# action 默认 PAUSE
+# count 默认 5(2 秒内投递 5 次)
+
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+# shellcheck disable=SC1091
+source "${SCRIPT_DIR}/lib/common.sh"
+
+require_adb
+check_receiver_exists
+
+ACTION="${1:-PAUSE}"
+COUNT="${2:-5}"
+
+print_header "节流测试 CONTROL/${ACTION} × ${COUNT}(2 秒内)"
+log_warn "请先确保正在播放页(run_all.sh 已跳转到播放页),否则会被守卫拦截"
+
+for i in $(seq 1 "${COUNT}"); do
+ PAYLOAD=$(build_control_payload "${ACTION}")
+ log_info "[${i}/${COUNT}] 投递 ${ACTION}"
+ send_voice_broadcast "CONTROL" "${PAYLOAD}" "throttle-$(date +%s%3N)-${i}" >/dev/null
+ sleep 0.3
+done
+
+log_ok "投递完成,预期服务端只执行最后一条 ${ACTION}"
+log_info "可通过 logcat 过滤 VoiceCommandLogger 验证 throttle 日志"
\ No newline at end of file
diff --git a/盛阳等三方支付EPG传参App开发需求.md b/盛阳等三方支付EPG传参App开发需求.md
deleted file mode 100644
index 4e7cb34..0000000
--- a/盛阳等三方支付EPG传参App开发需求.md
+++ /dev/null
@@ -1,243 +0,0 @@
-# 盛阳等三方支付EPG传参App开发需求.md
-
-## 一、项目概述
-
-开发一款基于橘视厅 SDK 的视频点播 App,部署于酒店 IPTV 机顶盒(Android 4.2+)。支持两种入口:EPG 调起直接播放 和 App 内浏览点播。媒资列表、详情、分集、播放地址全部由橘视厅 SDK 提供,不依赖后端。
-
-## 二、页面结构与流转(核心)
-
-### 2.1 流转图
-
-```
-EPG Intent 调起 App 独立启动
-(含设备标识 + 节目编码) (无节目编码)
- │ │
- ▼ ▼
-┌───────────────────────────────────────────────────┐
-│ 启动页 │
-│ 1. 解析 Intent 传参 │
-│ 2. 调 init 接口 -> 获取 sign │
-│ 3. 调 checkBenefits -> 获取 thirdCardNo │
-│ 4. 初始化橘视厅 SDK │
-└───────────────────────┬───────────────────────────┘
- │
- ┌────────────┴────────────┐
- │ │
- 有 programCode 无 programCode
- │ │
- ▼ ▼
- ┌───────────────┐ ┌───────────────┐
- │ 播放页 │ │ 媒资列表页 │
- │ (直接获取地址 │ │ (SDK 提供数据) │
- │ 全屏播放) │ └───────┬───────┘
- └───────┬───────┘ │
- │ ▼
- │ 返回 ┌───────────────┐
- └──────────────► │ 详情页 │
- │ (SDK 提供数据) │
- │ 选集/介绍/播放 │
- └───────┬───────┘
- │ 点击播放
- ▼
- ┌───────────────┐
- │ 播放页 │
- │ (全屏播放) │
- └───────┬───────┘
- │ 返回/播完
- ▼
- 回到详情页或列表页
- 用户可选其他影片
-```
-
-### 2.2 页面职责
-
-| 页面 | 数据来源 | 职责 |
-|------|---------|------|
-| **启动页** | 后端 init/checkBenefits | 设备初始化、权益校验、SDK 初始化 |
-| **媒资列表页** | **橘视厅 SDK** | 栏目分类、影片海报网格、分页加载 |
-| **详情页** | **橘视厅 SDK** | 影片介绍、电视剧选集列表、播放入口 |
-| **播放页** | **橘视厅 SDK** + MediaPlayer | 获取播放地址、全屏播放、遥控器控制 |
-
-## 三、EPG Intent 接收(核心入口)
-
-### 3.1 Intent 注册
-
-```xml
-
-
-
-
-
-
-```
-
-### 3.2 Intent 参数
-
-| 参数 | 类型 | 必填 | 说明 |
-|------|------|------|------|
-| mac | String | 否* | 设备 MAC |
-| wifiMac | String | 否 | WiFi MAC |
-| deviceSn | String | 否 | 设备 SN |
-| deviceUid | String | 否* | 设备唯一标识 |
-| programCode | String | 否 | **节目编码。有值=直接播放,无值=进入列表** |
-
-> *mac 和 deviceUid 至少传一个,用于后端 init 接口识别设备。优先级:EPG 传入 > 本地采集。
-
-### 3.3 启动分支逻辑
-
-```
-收到 Intent
- │
- ├─ 步骤1:收集设备标识(优先 EPG 传入,兜底本地采集)
- ├─ 步骤2:调 init 接口 -> 获取 sign
- ├─ 步骤3:调 checkBenefits -> 获取 thirdCardNo + status
- ├─ 步骤4:用 thirdCardNo 初始化橘视厅 SDK
- │
- ├─ 有 programCode?
- │ ├─ YES -> 调 SDK 获取播放地址 -> 跳转播放页(全屏)
- │ └─ NO -> 跳转媒资列表页
- │
- └─ 播放页返回后 -> 进入媒资列表页(用户可继续浏览)
-```
-
-## 四、init 与 checkBenefits 接口
-
-### 4.1 设备初始化接口
-
-```
-POST http://{后端地址}/api/jst/init
-Content-Type: application/json
-
-请求体:
-{
- "mac": "EC:F7:2B:03:1F:97",
- "deviceUid": "TEST-UID-001"
-}
-
-响应体:
-{
- "code": 0,
- "msg": "success",
- "sign": "OToxNzg1...", // 鉴权凭证,后续接口 Header 携带
- "data": {
- "device": { "deviceId": 9 }
- }
-}
-```
-
-### 4.2 权益查询接口
-
-```
-POST http://{后端地址}/api/jst/checkBenefits
-Header: sign: {init 返回的 sign}
-Content-Type: application/json
-
-响应体:
-{
- "code": 0,
- "msg": "success",
- "data": {
- "status": "ACTIVE", // ACTIVE=已激活 / UNBOUND=未绑定
- "thirdCardNo": "DbzgHwPRK18pDKVZIS3AdBfQw", // 橘视厅 SDK uid 参数
- "cardNo": "7D9AFE7677A758CD",
- "expireAt": "2026-07-29 17:25:05"
- }
-}
-```
-
-### 4.3 心跳续期接口
-
-```
-POST http://{后端地址}/api/jst/heartBeat
-Header: sign: {init 返回的 sign}
-
-// 每 30 分钟调用一次,sign 过期后需重新调 init
-```
-
-> **后端只负责以上三个接口。媒资、详情、分集、播放地址全部由橘视厅 SDK 提供。**
-
-## 五、橘视厅 SDK 能力(媒资+播放,全部 SDK 提供)
-
-### 5.1 SDK 初始化
-
-checkBenefits 返回 `thirdCardNo` 后,初始化 SDK:
-
-```java
-JetsenxSdk.init(
- agcode, // thirdCardNo(checkBenefits 返回)
- mac, // 设备 MAC
- uid, // thirdCardNo(checkBenefits 返回)
- "huashitv", // tenant(固定值)
- secret, // 密钥(由橘视厅分配)
- accesser, // 访问者标识(由橘视厅分配)
- 2, // utype(固定值)
- "", // extendJson
- "" // comboCode
-);
-```
-
-### 5.2 SDK 提供的能力
-
-| 能力 | SDK 方法 | 说明 |
-|------|---------|------|
-| 媒资列表 | SDK 提供 | 栏目分类、影片列表、海报、分页 |
-| 内容详情 | SDK 提供 | 影片介绍、演员、评分等 |
-| 电视剧分集 | SDK 提供 | 选集列表,每集 contentcode |
-| 播放地址(付费) | `JetsenxSdk.getVideoUrls(productCode, programCode, callback)` | 返回 VideoInfo[],含 720P/1080P 地址 |
-| 播放地址(免费) | `JetsenxSdk.getFreeVideoUrls(programCode, callback)` | parentContentType="8" 时调用 |
-| 产品定价 | `JetsenxSdk.getAllAssetPrices(accesser, secret, callback)` | 获取 productCode(packageCode 字段) |
-
-### 5.3 播放地址获取要点
-
-```
-1. productCode = getAllAssetPrices() 返回的第一个 packageCode
-2. 根据 parentContentType 选择接口:
- - "8" -> getFreeVideoUrls(programCode)
- - 其他 -> getVideoUrls(productCode, programCode)
-3. 返回 VideoInfo[],按 format 字段区分清晰度
-4. programCode 必须是分集级编码(非电视剧父级编码)
-```
-
-## 六、播放页
-
-### 6.1 技术方案
-
-使用系统原生 **MediaPlayer + SurfaceView**(兼容 Android 4.2,**不用 ExoPlayer**):
-
-- 支持 HLS(.m3u8)格式
-- 全屏横屏播放
-- 遥控器控制:OK键暂停/播放、左右键快进快退、返回键退出
-- 播放地址带时效性 `auth_key`
-
-### 6.2 播放完成流转
-
-```
-播放完成 / 用户按返回
- ├── EPG 直接播放进入的 -> 返回媒资列表页
- └── 详情页进入的 -> 返回详情页(可继续选集播放)
-```
-
-## 七、技术约束(Android 4.2 兼容)
-
-| 依赖 | 版本 | 原因 |
-|------|------|------|
-| minSdk | 17 | Android 4.2 |
-| Retrofit | **2.6.0** | 2.9+ 传递依赖 OkHttp 3.14 不兼容 |
-| OkHttp | **3.12.0**(force) | 3.12.1+ 含 Android10Platform,VerifyError |
-| Gson | **2.8.6** | 2.9+ 含 ReflectionHelper,VerifyError |
-| Coroutines | **1.6.4** | 1.7+ 要求 minSdk 21 |
-| 视频播放 | **MediaPlayer** | ExoPlayer 2.14+ 不兼容 |
-| 主题 | **AppCompat** | MaterialComponents 主 dex 方法数超限 |
-| multidex | 启用 | 主 dex 保留 androidx.appcompat.** / kotlinx.coroutines.** / retrofit2.** |
-
-## 八、参考代码
-
-测试阶段已验证的代码在 `partner-test-app/` 目录:
-
-| 文件 | 作用 |
-|------|------|
-| `JetsenxSdkUtil.kt` | SDK 封装:init + getVideoUrls + getFreeVideoUrls |
-| `PartnerSignManager.kt` | 后端接口:init + checkBenefits + heartBeat |
-| `PartnerApiService.kt` | Retrofit 接口定义 |
-| `VideoPlayerActivity.kt` | MediaPlayer + SurfaceView 播放实现 |
-| `multidex-config.pro` | Android 4.2 multidex 配置 |