feat: dev-5.1 分支基线(v1.3.35 发版后代码 + 5.1 声道改造文档)

- 以 main(ccc65af) 为基线提交当前工作区代码
- 新增 5.1 声道改造文档两份
- .gitignore 补充 .vscode/、.screenshots/
This commit is contained in:
freeboss
2026-11-02 19:25:12 +08:00
parent ccc65afcbc
commit b7b5518834
1394 changed files with 13694 additions and 3710 deletions
View File
+4 -1
View File
@@ -91,7 +91,10 @@ public class ActivityMonitor implements Application.ActivityLifecycleCallbacks {
public Activity getLastAct(){
return activityStack.lastElement();
if (!activityStack.isEmpty()) {
return activityStack.lastElement();
}
return null;
}
public String getPreAct(){
@@ -0,0 +1,141 @@
/**
* @file BenefitRevokedDialog.java
* @description 权益被服务端撤销时的提示对话框:唯一交互入口是"确定"按钮,
* 点击后退出程序。对话框不可被返回键/外部点击关闭,确保用户
* 看到"权益已取消"的提示并主动确认退出。
* @author ainetv
* @date 2026-08-24
*/
package com.pukka.jiecheng.utils;
import android.app.Activity;
import android.app.Dialog;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import com.pukka.jiecheng.R;
import com.pukka.libcore.utils.AppManager;
/**
* 权益被服务端撤销(心跳响应 commandType="revokeCardSecret")时弹出。
*
* <p>设计约束:
* <ul>
* <li>屏蔽所有关闭手势(setCancelable + setCanceledOnTouchOutside + onBackPressed 全堵死)</li>
* <li>只能点击"确定"按钮退出,按钮同时兼容遥控器 DPAD_CENTER / ENTER</li>
* <li>点击确定 → {@link AppManager#appExit()},先 setKeep(false) 再 appExit,
* 与 MainActivity.exitApp 一致;1 秒延迟 killProcess 给神策退出埋点上报留窗口</li>
* </ul>
*/
public class BenefitRevokedDialog extends Dialog {
private Activity activity;
private View btnConfirm;
private boolean isShowing = false;
public BenefitRevokedDialog(@NonNull Activity activity) {
super(activity, R.style.FullScreenDialog);
this.activity = activity;
initView();
}
private void initView() {
setContentView(R.layout.dialog_benefit_revoked);
// 屏蔽所有关闭路径:返回键 / 外部点击 / Dialog 自带的 cancel 动作
setCancelable(false);
setCanceledOnTouchOutside(false);
Window window = getWindow();
if (window != null) {
window.setLayout(WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT);
window.setGravity(android.view.Gravity.CENTER);
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
}
btnConfirm = findViewById(R.id.btnConfirm);
btnConfirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
exitApp();
}
});
// 遥控器中心键 / 遥控器 OK 键兼容:与 ActivitedDialog 一致
btnConfirm.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN &&
(keyCode == KeyEvent.KEYCODE_DPAD_CENTER
|| keyCode == KeyEvent.KEYCODE_ENTER
|| keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)) {
dismiss();
exitApp();
return true;
}
return false;
}
});
}
/**
* 退出整个程序:对齐 MainActivity.exitApp,先 setKeep(false) 让 appExit 走
* killProcess 分支;1 秒延迟给神策埋点上报留窗口,与项目约定一致。
*/
private void exitApp() {
AppManager.getAppManager().setKeep(false);
AppManager.getAppManager().appExit();
}
@Override
public void show() {
if (isShowing) {
return;
}
isShowing = true;
super.show();
requestConfirmFocus();
}
@Override
public void dismiss() {
if (!isShowing) {
return;
}
isShowing = false;
super.dismiss();
}
/**
* 屏蔽 Activity 层的返回键关闭 Dialog。
* 与 setCancelable(false) 一起保证 Dialog 不会被任何方式关掉。
*/
@Override
public void onBackPressed() {
// 故意 no-op:用户必须点"确定"退出程序
}
private void requestConfirmFocus() {
if (btnConfirm == null) {
return;
}
btnConfirm.setFocusable(true);
btnConfirm.setFocusableInTouchMode(true);
btnConfirm.requestFocus();
btnConfirm.post(new Runnable() {
@Override
public void run() {
if (!btnConfirm.hasFocus()) {
btnConfirm.requestFocus();
}
}
});
}
}
View File
View File
+8 -5
View File
@@ -60,14 +60,17 @@ public class ChannelManger {
// publicKeyStr_Hotel = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnemyIjwf1S5PsqgPhwiPGcF2Efa4UvFiFpg34+d2ZHQ6mkTTaussXCWQQRZGUfGbvkhIJOspGxmxtaMYgNxMQv9BT5B6xcnMILqYzG40hc2N3EiLm4hDYP12f8pshikh5aNxolbgkdA+Y2vrMf2KNucArMh8gT1mUuQRyUMiefy+N/kk2nK2p6SSX0628/BtczNgj13MaMHMlr8krNTlzB/22M8WMSk10nFS4ULp1KOPia8VxqzMIFKeREPIHIUnarz+llFIj6zKGg0ocqS6KEml7jv1yCgBpmyA2kAGEgnuTreO/5h5DqXByKLBEW8bYDiMUb7H/58IRerrITXfVQIDAQAB";
//
// }
JetsenxSdkUtil.ACCESSER = "JSTASJD";
JetsenxSdkUtil.SECRET = "kMeWsBu0QsTXpnft";
// 橘视厅5.1环绕声业务渠道参数(agcode/servicegroup 与原渠道一致,业务组合与业务线切换到5.1业务)
JetsenxSdkUtil.ACCESSER = "JSTAS51";
JetsenxSdkUtil.SECRET = "oam25ZqvGy3gVY0x";
JetsenxSdkUtil.AGCODE = "1100101010200492358120169";
BaseConst.agentVendorCode = JetsenxSdkUtil.AGCODE;
BaseConst.serviceComboCode = "1100121010200494755850172";
BaseConst.serviceComboCode = "1100121044377892530980001";
BaseConst.serviceGroupCode = "1100122010201752209710182";
BaseConst.businessLine = "4100450011381755998420012";
publicKeyStr_Hotel_release = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtlfaNsJwQ8wuVdITMLJPrXMeD9idJ0aTziOgkZZhigwzhyPNDAqw+nCKeJGGoTJyhbjPXKtnyI8eNpj+xhCCMXUD3BxOVMFWBhFxzIl83IQJG43mPRXwsJrI4oCYDaKv5aeZZBqF0OLPglXU+q19/28ltONvxcKp8YxXagmixzz1SDgLITM3s3ExqiaScK1dcpPtXbCpI4GXPP9l0ekkCBAo/d1oQqbxHvdE5ufAhEZ042k5tVHUgX9cUoedA9F2JcPhc+zUNCJUAQLvfVXxqNdhwxA4TY+64OX2iSje/cTqxp+4RcrX4rbUIYZitVQCRexZjKz7Yd/KXp8CKCho6wIDAQAB";
BaseConst.businessLine = "4100450044057893707260002";
// 橘视厅5.1业务免登录公钥(官方下发,debug/release 暂用同一把)
publicKeyStr_Hotel = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsEYpWdT+OumCBtx3lncrklXF/4O/oY+FpTNUE6D2/RwBbV6ZsodYZre0ndtLULBh+HgWyn/k+L1HbIhtS/bnt4PZpM2i9zM1LjMNPxCgZWAqJ1vZnfEQxUORcE3Sqj/So5KZC0JoHOsvtT/l9/+BsArKI1QdXplW3XE6M9TwxsMG8hclo+GqVxR4KIleAULliPoF9xYWEBeMxT/FGVgkDA7TJLqNzVDOQpjLc37YS4rGijMSSqBNaFNGdAqNdjFTYkodrjNe9CUzVIojVJezQcfYG92Sal2vECO81cIdUEo12zKm7Vz5wbl6NRR6Wwqj8oBh37E+auo6chAgcHA0jQIDAQAB";
publicKeyStr_Hotel_release = publicKeyStr_Hotel;
BoxSharedParameters.getInstance().setNewTvPass(true);
}
View File
View File
View File
View File
View File
View File
View File
View File
+325 -47
View File
@@ -1,7 +1,7 @@
package com.pukka.jiecheng.utils;
import android.content.Context;
import android.os.Handler;
import android.os.Build;
import android.text.TextUtils;
import com.jetsenxsdk.api.JetsenxSdk;
@@ -34,49 +34,40 @@ import java.util.Map;
* @since V0.1.0.1
*/
public class JetsenxSdkUtil {
public static String AGCODE = "1100101010208919998410060";
public static String AGCODE = "1100101010200492358120169";
public static String TENANT = "huashitv";
public static String ACCESSER = "JSTPKTEST";
public static String SECRET = "utudfD9gVU94r52D";
public static String ACCESSER = "JSTAS51";
public static String SECRET = "oam25ZqvGy3gVY0x";
private static final String TAG = "JetsenxSdkUtil";
private static Handler orderTimeOutHandler = new Handler();
// 显式绑定主线程 Looper:本类可能被后台线程(如 JsapiOrderMonitor.checkJob 的 Dispatchers.IO、
// AsHeartBeatManager 心跳的 DispatchQueue.global)首次触发类初始化,无参 new Handler() 会取当前线程
// 的 Looper(后台线程为 null)抛 "Can't create handler inside thread that has not called Looper.prepare()"。
// private static Handler orderTimeOutHandler = new Handler(Looper.getMainLooper());
private static Thread thread;
private static String productCodeSp;
/** 橘视厅5.1环绕声业务产品包编码(官方渠道参数下发,直接使用,避免 getAllAssetPrices 取包不确定) */
private static final String PRODUCT_CODE_51 = "2100130010166202685000333";
private static String sUid = "";
private static String productCodeSp = PRODUCT_CODE_51;
// volatile:sUid 由后台线程(checkBenefits 回调 / JsapiOrderMonitor)写入、
// 主线程读取,保证跨线程可见,避免读到旧空值
private static volatile String sUid = "";
/** 兜底请求最小间隔:3 秒内只触发一次 /api/jst/checkBenefits */
private static final long FALLBACK_THROTTLE_MS = 3000L;
/**
* 第三方变体(thirdparty)EPG 传入的 MAC 覆盖。
* 上次发起兜底请求的时间戳(System.currentTimeMillis(),0 表示从未触发)。
*
* 背景:EPG 在后端用该 MAC 注册设备并下发卡密(thirdCardNo),SDK init 必须用同一 MAC
* 才能与后端权益绑定匹配;若回退 MacUtils.getMac 本机自采集 MAC,会导致权益不生效。
* 为空时回退本机自采集(原版行为不变)。
* 进程级静态:ThirdPartyEntryActivity 解析 Intent extras 时设置。
* 3 秒节流:VideoDetailActivity onResume、MainActivity 启动检查等高频入口
* 在窗口期内只放行一次请求,避免 fallback 变成轮询;窗口期外允许重试,
* 网络瞬时失败无需杀进程即可恢复。仅放行方推进时间戳,被挡调用不影响窗口。
*/
private static volatile String sMacOverride = "";
/**
* 设置 SDK init 的 MAC 覆盖值(EPG 传入的 mac 优先)。
* 传空字符串/null 清除覆盖,恢复本机自采集。
*/
public static void setMacOverride(String mac) {
String normalized = mac == null ? "" : mac.trim();
if (normalized.equals(sMacOverride)) {
return;
}
sMacOverride = normalized;
LogUtils.d(TAG, "setMacOverride: " + (TextUtils.isEmpty(normalized) ? "<cleared>" : normalized));
}
/**
* 读取 SDK init 的 MAC 覆盖值(空串表示未设置)。
*/
public static String getMacOverride() {
return sMacOverride;
}
private static final java.util.concurrent.atomic.AtomicLong sLastFallbackAt =
new java.util.concurrent.atomic.AtomicLong(0L);
/**
* 标记当前是否有 re-init 正在进行,保证 globalInit 串行执行
@@ -92,7 +83,25 @@ public class JetsenxSdkUtil {
public interface InitMemberCodeCallback {
void onSuccess(String memberCode);
void onFail(String message);
// 状态码在前;0 表示无 SDK 错误码(缺标识 / 同步异常等内部场景),非 0 为 SDK 返回的错误码
void onFail(int errorCode, String message);
}
/**
* 将 Jetsenx SDK onFail 回调返回的字符串错误码转换为 int。
*
* <p>SDK 内部 JetsenxInitCallback#onFail(String errorCode, String message) 第一参
* 是错误码字符串,非数字格式或为空时返回 0 表示"未知错误码"。</p>
*/
private static int parseErrorCode(String code) {
if (TextUtils.isEmpty(code)) {
return 0;
}
try {
return Integer.parseInt(code);
} catch (NumberFormatException e) {
return 0;
}
}
/**
@@ -129,15 +138,17 @@ public class JetsenxSdkUtil {
/**
* 全局初始化方法,复用已缓存的 uid
*
* <p>sUid 为空时不直接失败,而是经 {@link #recoverUidAndInit} 做一次进程级兜底恢复
* (/api/jst/checkBenefits 拉取卡密);恢复成功则继续正常初始化,失败(无权益/网络异常)
* 统一映射为 {@link InitMemberCodeCallback#onFail}。</p>
*
* @param context 上下文
* @param callback 初始化结果回调
*/
public static void globalInit(Context context, InitMemberCodeCallback callback) {
if (TextUtils.isEmpty(sUid)) {
LogUtils.d(TAG, "globalInit skipped: no verified uid cached in current process");
if (callback != null) {
callback.onFail("缺少可复用的SDK初始化标识");
}
LogUtils.d(TAG, "globalInit: no cached uid, falling back to checkBenefits recovery");
recoverUidAndInit(context, toFallbackCallback(callback));
return;
}
synchronized (sPendingReInitQueue) {
@@ -166,11 +177,11 @@ public class JetsenxSdkUtil {
}
@Override
public void onFail(String message) {
public void onFail(int errorCode, String message) {
dispatchNextReInit();
if (callback != null) {
try {
callback.onFail(message);
callback.onFail(errorCode, message);
} catch (Exception e) {
LogUtils.e(TAG, "globalInit onFail callback exception: " + e.getMessage());
}
@@ -183,7 +194,8 @@ public class JetsenxSdkUtil {
dispatchNextReInit();
if (callback != null) {
try {
callback.onFail("SDK初始化调用异常: " + e.getMessage());
// -1: SDK init 同步异常场景,无 SDK 错误码可用
callback.onFail(-1, "SDK初始化调用异常: " + e.getMessage());
} catch (Exception ce) {
LogUtils.e(TAG, "globalInit onFail callback exception: " + ce.getMessage());
}
@@ -235,6 +247,204 @@ public class JetsenxSdkUtil {
return sUid;
}
/**
* 带兜底恢复的 SDK 全局初始化。
*
* 流程:
* 1. 若 sUid 非空,直接走 [globalInit](已有串行化与重入队列保护),onSuccess → onReady,onFail → onNetworkError。
* 2. 若 sUid 为空:
* a. 首次调用:通过 [AsApiRequester.requestAsCheckBenefitsApi] 拉一次 /api/jst/checkBenefits
* (含 sign 缺失自动首签流程,沿用 checkBenefits 既有行为)。
* b. 业务码 10200(Activated) 且 thirdCardNo 非空 → cacheReusableUid + globalInit。
* c. 业务码 10900(CodeActivated) 且 thirdCardNo 非空 → cacheReusableUid + globalInit(副卡也支持回放)。
* d. 业务码 10300/10400/10500/10600/10700/10800 → onNoEntitlement。
* e. 网络异常 / 解析失败 / 签名缺失 → onNetworkError。
* f. 业务码 Activated 但 thirdCardNo 为空 → onNoEntitlement(不写入空卡密)。
* 3. 3 秒节流:窗口期内的高频调用直接 onNoEntitlement(0, "权益信息为空"),
* 不发请求也不走"网络异常"分支,避免覆盖真实业务状态;
* 窗口期外允许重试,网络瞬时失败无需杀进程即可恢复。
*
* 与 [globalInit] 的差异:兜底逻辑(recoverUidAndInit)已由二者共用,
* 差异仅在回调形态——本方法为三态回调,可区分"无权益"与"网络异常";
* globalInit 为二态回调,兜底失败统一映射 onFail。
* 回调一定在调用线程(主线程或业务线程)上触发(沿用 AppScope 调度)。
*
* @param context 上下文(Application 优先,避免 Activity 泄漏)
* @param callback 三态回调;callback.onReady 等价 globalInit.onSuccess,
* callback.onNetworkError 等价 globalInit.onFail,
* callback.onNoEntitlement 是本方法新增的分支
*/
public static void ensureGlobalInitAsync(Context context, FallbackInitCallback callback) {
if (context == null) {
if (callback != null) {
callback.onNetworkError(1003, "context is null (stale Activity?)");
}
return;
}
final Context appContext = context.getApplicationContext();
// 第 1 步:sUid 非空 → 直接复用 globalInit
if (!TextUtils.isEmpty(sUid)) {
globalInit(appContext, new InitMemberCodeCallback() {
@Override
public void onSuccess(String memberCode) {
if (callback != null) callback.onReady(memberCode);
}
@Override
public void onFail(int errorCode, String message) {
// globalInit 自身失败归类为网络/SDK 异常(权益状态未知)
if (callback != null) callback.onNetworkError(errorCode, message);
}
});
return;
}
recoverUidAndInit(appContext, callback);
}
/**
* 三态回调到二态回调的适配:globalInit 无 sUid 时走兜底恢复,
* onReady 等价 onSuccess,onNoEntitlement / onNetworkError 统一映射 onFail。
*/
static FallbackInitCallback toFallbackCallback(InitMemberCodeCallback callback) {
return new FallbackInitCallback() {
@Override
public void onReady(String memberCode) {
if (callback != null) callback.onSuccess(memberCode);
}
@Override
public void onNoEntitlement(int businessCode, String message) {
if (callback != null) callback.onFail(businessCode, message);
}
@Override
public void onNetworkError(int businessCode, String message) {
if (callback != null) callback.onFail(businessCode, message);
}
};
}
/**
* 节流闸门:距上次放行不足 {@link #FALLBACK_THROTTLE_MS} 时拒绝,且不推进时间戳。
* CAS 保证并发下只有一个赢家。
*
* @param nowMillis 当前时间戳(System.currentTimeMillis()),注入参数便于单测
*/
static boolean tryAcquireFallback(long nowMillis) {
long last = sLastFallbackAt.get();
if (nowMillis - last < FALLBACK_THROTTLE_MS) {
return false;
}
return sLastFallbackAt.compareAndSet(last, nowMillis);
}
/**
* sUid 为空时的兜底恢复:/api/jst/checkBenefits 拉取卡密,3 秒节流。
*
* <p>恢复成功后 cacheReusableUid 并回到 globalInit 正常串行初始化——此时 sUid 已非空,
* 不会再进入本方法;失败路径回调后即终止,节流窗口内的高频重入也会被闸门挡下,
* 调用图为单向无递归。供 globalInit 与 ensureGlobalInitAsync 共用,避免两套逻辑分叉。</p>
*/
private static void recoverUidAndInit(Context context, FallbackInitCallback callback) {
if (context == null) {
if (callback != null) {
callback.onNetworkError(1003, "context is null (stale Activity?)");
}
return;
}
final Context appContext = context.getApplicationContext();
// 3 秒节流:窗口期内的高频入口直接挡下,不发请求。
// 命中时视为"权益信息未知"(无权益)走 onNoEntitlement,而不是 onNetworkError,
// 避免 UI 端把"无可用卡密"误显示为"错误码:0 网络异常,请重试"。
if (!tryAcquireFallback(System.currentTimeMillis())) {
LogUtils.d(TAG, "recoverUidAndInit skipped: throttled within " + FALLBACK_THROTTLE_MS + "ms");
// 必须仍 fire callback,否则上层闸门(VideoDetailActivity.mPlayGateInFlight)永远不被清位,
// 后续 fullVideo() 直接被闸门挡下无法播放。
if (callback != null) {
callback.onNoEntitlement(0, "权益信息为空(请求频繁)");
}
return;
}
LogUtils.d(TAG, "recoverUidAndInit: sUid empty, fetching via /api/jst/checkBenefits");
// 第 3 步:sUid 为空 + 首次 → 异步拉 checkBenefits
// 10200 经 onActivated、10900 经 onCodeActivated 回调,result.code 保留真实业务码,
// result.thirdCardNo 为卡密,二者统一交给 handleCheckBenefitsForFallback 处理。
com.pukka.jiecheng.request.AsApiRequester.requestAsCheckBenefitsApi(appContext, new com.pukka.jiecheng.request.AsApiRequester.CheckStatusCallback() {
@Override
public void onActivated(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) {
handleCheckBenefitsForFallback(result, appContext, callback);
}
@Override
public void onCodeActivated(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) {
handleCheckBenefitsForFallback(result, appContext, callback);
}
@Override
public void onPreset(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) {
if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_PRESET, "设备未激活");
}
@Override
public void onActivating(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) {
if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_ACTIVATING, "权益激活中");
}
@Override
public void onActivateFailed(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) {
if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_ACTIVATE_FAIL, "激活失败");
}
@Override
public void onUnbound(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) {
if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_UNBOUND, "设备未绑定");
}
@Override
public void onExpired(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) {
if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_EXPIRED, "权益已过期");
}
@Override
public void onOccupied(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) {
if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_OCCUPIED, "权益已被占用");
}
@Override
public void onUnknown(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) {
if (callback != null) callback.onNoEntitlement(result != null ? result.code : 0, "权益状态未知");
}
@Override
public void onFail(int errorCode, String message) {
if (callback != null) callback.onNetworkError(errorCode, message);
}
});
}
/**
* 把 Activated/CodeActivated 响应归一化为 fallback 写入分支。
*
* 关键不变量:仅当 thirdCardNo 非空时才 cacheReusableUid,避免写入空串。
* Activated 但 thirdCardNo 为空 → 视为 NO_ENTITLEMENT(与 MainActivity.onActivated
* 对 home 版的处理一致:见 MainActivity.java:1047-1055)。
*/
private static void handleCheckBenefitsForFallback(
com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result,
Context appContext,
FallbackInitCallback callback
) {
if (result == null || TextUtils.isEmpty(result.thirdCardNo)) {
LogUtils.d(TAG, "ensureGlobalInitAsync: activated but thirdCardNo empty, treat as no entitlement");
// 透传真实业务码(如 10900),供家用版调用方区分"激活码已激活但无权益码"场景
if (callback != null) callback.onNoEntitlement(result == null ? 0 : result.code, "权益信息为空");
return;
}
LogUtils.d(TAG, "ensureGlobalInitAsync: checkBenefits returned activated, caching uid and globalInit");
cacheReusableUid(result.thirdCardNo);
globalInit(appContext, new InitMemberCodeCallback() {
@Override
public void onSuccess(String memberCode) {
if (callback != null) callback.onReady(memberCode);
}
@Override
public void onFail(int errorCode, String message) {
if (callback != null) callback.onNetworkError(errorCode, message);
}
});
}
public static void clearReusableUid() {
sUid = "";
LogUtils.d(TAG, "clearReusableUid: sUid cleared");
@@ -248,9 +458,7 @@ public class JetsenxSdkUtil {
* @param callback 初始化结果回调
*/
public static void init(Context context, String uid, InitMemberCodeCallback callback) {
// EPG 传入的 MAC 覆盖优先(thirdparty 变体):后端权益以 EPG 注册的 MAC 绑定,
// 本机自采集 MAC 与之不匹配会导致 SDK 鉴权失败、权益不生效
String mac = TextUtils.isEmpty(sMacOverride) ? MacUtils.getMac(context) : sMacOverride;
String mac = MacUtils.getMac(context);
String utype = "2";
if (!TextUtils.isEmpty(uid) && uid.matches("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")) {
mac = uid;
@@ -288,7 +496,7 @@ public class JetsenxSdkUtil {
LogUtils.d("onFail s=" + s);
LogUtils.d("onFail s1=" + s1);
if (callback != null) {
callback.onFail(StringUtils.isEmpty(s1) ? "SDK初始化失败" : s1);
callback.onFail(parseErrorCode(s), StringUtils.isEmpty(s1) ? "SDK初始化失败" : s1);
}
}
});
@@ -566,7 +774,8 @@ public class JetsenxSdkUtil {
playCodeBean.setSpecCode(info.specCode);
PlayRecordManager.playCodeMaps.put(720,playCodeBean);
}else if (info.getFormat().contains("1080")
&&TextUtils.isEmpty(videoUrl_1080)){
&&TextUtils.isEmpty(videoUrl_1080)
&&!Surround51Manager.isSurroundFormat(info.getFormat())){
videoUrl_1080 = info.getVideoUrl();
PlayCodeBean playCodeBean = new PlayCodeBean();
playCodeBean.setMovieCode(info.movieCode);
@@ -575,6 +784,23 @@ public class JetsenxSdkUtil {
PlayRecordManager.playCodeMaps.put(1080,playCodeBean);
}
}
// 5.1 环绕声独立档(format='1080P 5.1 环绕声',H265+AC3_51,单AC3音轨无2.0兜底):
// 开关开启时作为 1080 档播放地址,输出路径由播放器按设备能力自动决策
// (AC3直通 > 平台硬解 > FFmpeg软解自动下混);开关关闭时主循环已排除 5.1 档,走普通 2.0 档。
if (Surround51Manager.is51Preferred()) {
for (VideoInfo info : videoInfos) {
if (Surround51Manager.isSurroundFormat(info.getFormat())) {
videoUrl_1080 = info.getVideoUrl();
PlayCodeBean playCodeBean = new PlayCodeBean();
playCodeBean.setMovieCode(info.movieCode);
playCodeBean.setProgramCode(info.progCode);
playCodeBean.setSpecCode(info.specCode);
PlayRecordManager.playCodeMaps.put(1080, playCodeBean);
LogUtils.d(TAG, "videoUrl_51 applied:" + videoUrl_1080);
break;
}
}
}
LogUtils.d(TAG, "videoUrl_1080:" + videoUrl_1080);
LogUtils.d(TAG, "videoUrl_720:" + videoUrl_720);
if (null != onGetAllVideoUrlCallback) {
@@ -614,7 +840,8 @@ public class JetsenxSdkUtil {
playCodeBean.setSpecCode(info.specCode);
PlayRecordManager.playCodeMaps.put(720,playCodeBean);
}else if (info.getFormat().contains("1080")
&&TextUtils.isEmpty(videoUrl_1080)){
&&TextUtils.isEmpty(videoUrl_1080)
&&!Surround51Manager.isSurroundFormat(info.getFormat())){
videoUrl_1080 = info.getVideoUrl();
PlayCodeBean playCodeBean = new PlayCodeBean();
playCodeBean.setMovieCode(info.movieCode);
@@ -623,6 +850,23 @@ public class JetsenxSdkUtil {
PlayRecordManager.playCodeMaps.put(1080,playCodeBean);
}
}
// 5.1 环绕声独立档(format='1080P 5.1 环绕声',H265+AC3_51,单AC3音轨无2.0兜底):
// 开关开启时作为 1080 档播放地址,输出路径由播放器按设备能力自动决策
// (AC3直通 > 平台硬解 > FFmpeg软解自动下混);开关关闭时主循环已排除 5.1 档,走普通 2.0 档。
if (Surround51Manager.is51Preferred()) {
for (VideoInfo info : videoInfos) {
if (Surround51Manager.isSurroundFormat(info.getFormat())) {
videoUrl_1080 = info.getVideoUrl();
PlayCodeBean playCodeBean = new PlayCodeBean();
playCodeBean.setMovieCode(info.movieCode);
playCodeBean.setProgramCode(info.progCode);
playCodeBean.setSpecCode(info.specCode);
PlayRecordManager.playCodeMaps.put(1080, playCodeBean);
LogUtils.d(TAG, "videoUrl_51 applied:" + videoUrl_1080);
break;
}
}
}
LogUtils.d(TAG, "videoUrl_1080:" + videoUrl_1080);
LogUtils.d(TAG, "videoUrl_720:" + videoUrl_720);
if (null != onGetAllVideoUrlCallback) {
@@ -653,4 +897,38 @@ public class JetsenxSdkUtil {
void onErr(String code, String msg);
}
/**
* sUid 兜底恢复的回调接口。
*
* 三个分支语义明确分离:
* - onReady: sUid 已恢复,SDK 初始化成功,调用方可以继续走原有业务流(如 getAllVideoUrl)。
* - onNoEntitlement: 服务端明确说无权益(Preset/Activating/ActivateFailed/Unbound/Expired/Occupied
* 等业务码,或 Activated 但 thirdCardNo 为空),调用方应走"请购买/请激活"分支。
* - onNetworkError: 网络/解析异常或签名失败,调用方应展示"网络异常请重试",**不能**误判为未激活。
*
* 设计依据:见 docs/superpowers/plans/2026-09-14-suid-fallback-recovery.md 第 3 节。
*/
public interface FallbackInitCallback {
/** sUid 已恢复,SDK 初始化成功(memberCode 与 globalInit 一致) */
void onReady(String memberCode);
/**
* 服务端确认无权益或当前卡密不可用。
* @param businessCode 服务端业务码(10700/10800/10600/10500/10400/10300 等),
* 或 Activated/CodeActivated 响应的真实业务码(如 10900)——
* 当 Activated 但 {@code thirdCardNo} 为空时,
* {@code handleCheckBenefitsForFallback} 早退并提示"权益信息为空"。
* {@code 0} 为合成哨兵码,仅在 result 为 null 时使用(非真实业务码)。
* @param message 给调用方的提示文案,可直接展示给用户
*/
void onNoEntitlement(int businessCode, String message);
/**
* 网络异常、解析失败或签名失败——权益状态未知。
* 业务码约定:1004 = 网络/解析异常;1001 = 签名缺失;1003 = context null;
* 0 = 本地合成码(3 秒节流窗口内被挡下,提示"请求过于频繁,请稍后重试")
*/
void onNetworkError(int businessCode, String message);
}
}
+3 -9
View File
@@ -16,6 +16,7 @@ import com.pukka.jiecheng.index.IndexAmberManager;
import com.pukka.jiecheng.list.ListActivity;
import com.pukka.jiecheng.login.LoginActivity;
import com.pukka.jiecheng.order.OrderActivity;
import com.pukka.jiecheng.setting.SettingActivity;
import com.pukka.jiecheng.search.SearchActivity;
import com.pukka.jiecheng.video.VideoDetailActivity;
import com.pukka.jiecheng.web.SimpleWebActivity;
@@ -82,7 +83,6 @@ public class MgRoute {
// return;
// }
Intent intent = new Intent(context, OrderActivity.class);
intent.putExtra(OrderActivity.go_type_param,go_type);
context.startActivity(intent);
}
@@ -104,14 +104,8 @@ public class MgRoute {
* 跳转到设置面
*/
public static void go2Setting(Context context) {
// Intent intent = new Intent(context, MainWebActivity.class);
// Bundle bundle = new Bundle();
// StringBuilder sbUrl = new StringBuilder();
// sbUrl.append(BaseUrl.getSingleton().getBaseEpgHttpUrl());
// sbUrl.append(MgRoute.urlSetting);
// bundle.putString("url",sbUrl.toString());
// intent.putExtras(bundle);
// context.startActivity(intent);
Intent intent = new Intent(context, SettingActivity.class);
context.startActivity(intent);
}
/**
View File
View File
View File
View File
View File
View File
View File
View File
View File
@@ -0,0 +1,121 @@
package com.pukka.jiecheng.utils;
import android.content.Context;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.os.Build;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.pukka.libcore.utils.LogUtils;
import com.pukka.libcore.utils.SharedPreferenceUtils;
/**
* 5.1 环绕声能力检测与播放开关。
* <p>开关(默认开启)关闭时不选 5.1 独立档(H265+AC3_51),播放普通 2.0 档;
* 开启时按设备能力自动选择输出路径:AC3直通(HDMI bitstream) &gt; 平台硬解(6ch LPCM) &gt; FFmpeg软解(下混 2.0)。</p>
* <p>CDN 侧 5.1 档为单 AC3 音轨流(无 2.0 兜底音轨),故能力不足或开关关闭时必须回退普通档 URL。</p>
*/
public final class Surround51Manager {
private static final String TAG = "Surround51Manager";
/** SP 键:5.1 环绕声播放开关(设备信息页可切换) */
private static final String SP_KEY_51_ENABLED = "Surround51Enabled";
private static volatile Boolean sAc3Passthrough;
private static volatile Boolean sPlatformAc3Decoder;
private Surround51Manager() {
}
/** 5.1 环绕声播放开关,默认开启 */
public static boolean is51Enabled() {
return SharedPreferenceUtils.getBoolean(SP_KEY_51_ENABLED, true);
}
public static void set51Enabled(boolean enabled) {
SharedPreferenceUtils.put(SP_KEY_51_ENABLED, enabled);
clearCache();
}
/** 清除静态缓存(开关切换后需重新检测) */
public static void clearCache() {
sAc3Passthrough = null;
sPlatformAc3Decoder = null;
}
/**
* 是否优先选用 5.1 独立档:开关开启 且 Android 5.1(API 22) 以上。
* API 22 及以下设备解码能力有限,固定走普通 2.0 档。
*/
public static boolean is51Preferred() {
return is51Enabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1;
}
/** 判断 SDK 返回档位是否 5.1 环绕声档(format='1080P 5.1 环绕声') */
public static boolean isSurroundFormat(String format) {
String fmt = format == null ? "" : format;
return fmt.contains("5.1") || fmt.contains("环绕声");
}
/**
* 设备是否支持 AC3 源码直通(HDMI/SPDIF bitstream)。
* 结果受系统编码环绕声设置(encoded_surround_output)与当前接入的音频设备能力影响。
*/
public static boolean supportsAc3Passthrough(Context context) {
if (sAc3Passthrough == null) {
try {
sAc3Passthrough = AudioCapabilities.getCapabilities(context)
.supportsEncoding(C.ENCODING_AC3);
} catch (Throwable e) {
LogUtils.d(TAG, "supportsAc3Passthrough err:" + e);
sAc3Passthrough = false;
}
}
return sAc3Passthrough;
}
/** 平台是否自带 AC3 硬解码器(如带 Dolby/MS12 授权的芯片),硬解输出 6ch LPCM 由系统按输出能力下混 */
public static boolean hasPlatformAc3Decoder() {
if (sPlatformAc3Decoder == null) {
boolean found = false;
try {
for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) {
// v1.3.36: isAlias() 是 API 29+ 方法,API 28 及以下调用抛 NoSuchMethodError
//(腾讯T5/Android 9 实测),导致整个循环被 catch 中断、检测永远为"无解码器"。
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && info.isAlias())
|| info.isEncoder()) {
continue;
}
for (String type : info.getSupportedTypes()) {
if ("audio/ac3".equalsIgnoreCase(type)) {
found = true;
break;
}
}
if (found) {
break;
}
}
} catch (Throwable e) {
LogUtils.d(TAG, "hasPlatformAc3Decoder err:" + e);
found = false;
}
sPlatformAc3Decoder = found;
}
return sPlatformAc3Decoder;
}
/** 当前设备 5.1 音频输出路径描述(设备信息页展示用) */
public static String getAudioPathDesc(Context context) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
return "不支持(系统版本过低)";
}
if (supportsAc3Passthrough(context)) {
return "AC3源码直通";
}
if (hasPlatformAc3Decoder()) {
return "硬解码";
}
return "软解";
}
}
View File
View File
View File
View File
View File
+3 -9
View File
@@ -1,8 +1,8 @@
package com.pukka.jiecheng.utils;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class UrlParamBuilder {
@@ -60,15 +60,9 @@ public class UrlParamBuilder {
urlBuilder.append("&");
}
try {
urlBuilder.append(URLEncoder.encode(entry.getKey(), "UTF-8"))
urlBuilder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8))
.append("=")
.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
urlBuilder.append(entry.getKey())
.append("=")
.append(entry.getValue());
}
.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
}
}
View File
View File