feat: dev-5.1 分支基线(v1.3.35 发版后代码 + 5.1 声道改造文档)
- 以 main(ccc65af) 为基线提交当前工作区代码
- 新增 5.1 声道改造文档两份
- .gitignore 补充 .vscode/、.screenshots/
This commit is contained in:
0
app/src/androidTest/androidmanifest.xml
Executable file → Normal file
0
app/src/androidTest/androidmanifest.xml
Executable file → Normal file
0
app/src/androidTest/java/com/migu/jiecheng/ExampleInstrumentedTest.java
Executable file → Normal file
0
app/src/androidTest/java/com/migu/jiecheng/ExampleInstrumentedTest.java
Executable file → Normal file
0
app/src/androidTest/java/com/migu/jiecheng/IndexNavType6TestActivity.java
Executable file → Normal file
0
app/src/androidTest/java/com/migu/jiecheng/IndexNavType6TestActivity.java
Executable file → Normal file
0
app/src/androidTest/java/com/migu/jiecheng/IndexNavType6UiTest.java
Executable file → Normal file
0
app/src/androidTest/java/com/migu/jiecheng/IndexNavType6UiTest.java
Executable file → Normal file
0
app/src/aishiActivation/res/values/strings.xml → app/src/as8Scan/res/values/strings.xml
Executable file → Normal file
0
app/src/aishiActivation/res/values/strings.xml → app/src/as8Scan/res/values/strings.xml
Executable file → Normal file
0
app/src/aishiScan/res/values/strings.xml → app/src/asAScan/res/values/strings.xml
Executable file → Normal file
0
app/src/aishiScan/res/values/strings.xml → app/src/asAScan/res/values/strings.xml
Executable file → Normal file
4
app/src/ashome/res/values/strings.xml
Normal file
4
app/src/ashome/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">橘视厅家用版</string>
|
||||
</resources>
|
||||
4
app/src/fxScan/res/values/strings.xml
Normal file
4
app/src/fxScan/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">橘视厅扫码版</string>
|
||||
</resources>
|
||||
4
app/src/home/res/values/strings.xml
Normal file
4
app/src/home/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">橘视厅家用版</string>
|
||||
</resources>
|
||||
96
app/src/main/AndroidManifest.xml
Executable file → Normal file
96
app/src/main/AndroidManifest.xml
Executable file → Normal file
@@ -4,10 +4,6 @@
|
||||
package="com.pukka.jiecheng"
|
||||
tools:ignore="MissingLeanbackLauncher">
|
||||
|
||||
<!-- Android 4.2 兼容(minSdk 17):覆盖 minSdk>17 的依赖库声明,运行时兼容性靠
|
||||
降级依赖版本 + 不触碰高版本 API 路径保证。清单随探测编译逐步补充。 -->
|
||||
<uses-sdk tools:overrideLibrary="com.github.gzuliyujiang.oaid" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
android:required="false" />
|
||||
@@ -41,6 +37,23 @@
|
||||
<uses-permission android:name="android.permission.REPLACE_EXISTING_PACKAGE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
<!-- ===== 语音协议(文档 §6.2):自定义 signature-level permission ===== -->
|
||||
<!-- debug flavor 通过 voiceProtectionLevel=normal 临时放开(联调期 adb shell 可投递);
|
||||
release 走 signature 严格白名单。 -->
|
||||
<permission
|
||||
android:name="com.pukka.jiecheng.permission.VOICE_COMMAND"
|
||||
android:protectionLevel="${voiceProtectionLevel}" />
|
||||
<uses-permission android:name="com.pukka.jiecheng.permission.VOICE_COMMAND" />
|
||||
|
||||
<!-- ===== 语音冷启动保活(文档 2026-08-28-语音冷启动链路设计方案) ===== -->
|
||||
<!-- BOOT_COMPLETED 启 FGS 必须先声明权限;FOREGROUND_SERVICE 是 Android 9+ 启前台服务的前置 -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<!-- 语音冷启动绕过 Background Activity Start 限制::voice 进程挂 1px 悬浮窗,
|
||||
AMS 视为有 visible window,后续 startActivity 走豁免路径。
|
||||
标准权限,部分 OEM ROM 自动授予;无需运行时申请。 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<application
|
||||
android:name=".MyApplication"
|
||||
android:allowBackup="false"
|
||||
@@ -71,6 +84,10 @@
|
||||
android:name=".device.DeviceActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="landscape" />
|
||||
<activity
|
||||
android:name=".setting.SettingActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="landscape" />
|
||||
<activity
|
||||
android:name=".order.OrderActivity"
|
||||
android:exported="false"
|
||||
@@ -85,6 +102,11 @@
|
||||
android:exported="false"
|
||||
android:screenOrientation="landscape"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize" />
|
||||
<!-- 家用版微信扫码登录 Activity(第一阶段占位),仅应用内启动 -->
|
||||
<activity
|
||||
android:name=".login.WxMpLoginActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="landscape" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -98,17 +120,6 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ThirdPartyEntryActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:theme="@style/whiteTheme">
|
||||
<intent-filter>
|
||||
<action android:name="com.pukka.jiecheng.action.THIRD_PARTY_PLAY" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".TestActivity"
|
||||
android:exported="true"
|
||||
@@ -156,6 +167,61 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- 语音命令接收器(文档 §5.2) -->
|
||||
<receiver
|
||||
android:name=".voice.receiver.VoiceCommandReceiver"
|
||||
android:exported="true"
|
||||
android:permission="com.pukka.jiecheng.permission.VOICE_COMMAND">
|
||||
<intent-filter>
|
||||
<action android:name="com.pukka.jiecheng.action.VOICE_COMMAND" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 语音冷启动链路:开机拉起 :voice 进程 FGS(文档 2026-08-28-语音冷启动链路设计方案) -->
|
||||
<receiver
|
||||
android:name=".voice.receiver.BootCompletedReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:directBootAware="true"
|
||||
android:process=":voice"
|
||||
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 自广播兜底接收器:处理 KEEPALIVE_BOOT 自定义 action。
|
||||
解决 BOOT_COMPLETED 因 STOPPED / OEM 拦截失败时 :voice 进程无法自启的问题。
|
||||
同包 explicit broadcast(Intent.setPackage)不受 STOPPED 状态限制。
|
||||
exported=false 不暴露给外部,仅接收应用自身 AlarmManager 派发的 PendingIntent。 -->
|
||||
<receiver
|
||||
android:name=".voice.receiver.KeepAlivePingReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":voice">
|
||||
<intent-filter>
|
||||
<action android:name="com.pukka.jiecheng.action.KEEPALIVE_BOOT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- 语音冷启动保活服务:保持 :voice 进程隔离。
|
||||
设计意图:主进程被 LMK 杀掉后 receiver 仍能存活,持续监听 OEM 创维广播。
|
||||
cold-launch 受 Android 12+ Background Activity Start 限制的问题,由
|
||||
VoiceOverlayHelper 在 :voice 进程挂 1px TYPE_APPLICATION_OVERLAY 悬浮窗
|
||||
提供 visible window 豁免(AMS 视为前台可见窗口),配合
|
||||
SkyworthVoiceAdapter.coldLaunchMainActivity 优先路径
|
||||
showOverlay → ctx.startActivity(cold) → hideOverlay 绕过。
|
||||
FullScreenIntent 通知作为 OEM ROM 拦截悬浮窗场景下的次选兜底保留。
|
||||
当前设备 Android 11 不需要 USE_FULL_SCREEN_INTENT 权限;如后续适配
|
||||
Android 14 设备需在 manifest 声明该权限。 -->
|
||||
<service
|
||||
android:name=".voice.service.VoiceKeepAliveService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse"
|
||||
android:process=":voice" />
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
0
app/src/main/java/com/pukka/jiecheng/JcConfig.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/JcConfig.java
Executable file → Normal file
335
app/src/main/java/com/pukka/jiecheng/MainActivity.java
Executable file → Normal file
335
app/src/main/java/com/pukka/jiecheng/MainActivity.java
Executable file → Normal file
@@ -4,6 +4,8 @@ package com.pukka.jiecheng;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
@@ -33,8 +35,10 @@ import com.pukka.jiecheng.downapk.UpgradeHelper;
|
||||
import com.pukka.jiecheng.BuildConfig;
|
||||
import com.pukka.jiecheng.index.IndexActivity;
|
||||
import com.pukka.jiecheng.login.bean.LoginLogonBean;
|
||||
import com.pukka.jiecheng.monitor.NetworkMonitor;
|
||||
import com.pukka.jiecheng.newtv.NewTvView;
|
||||
import com.pukka.jiecheng.permission.AgreementStoragePermissionGateway;
|
||||
import com.pukka.jiecheng.request.AsApiErrorCode;
|
||||
import com.pukka.jiecheng.request.AsApiRequester;
|
||||
import com.pukka.jiecheng.request.AsHeartBeatManager;
|
||||
import com.pukka.jiecheng.utils.ActivitedDialog;
|
||||
@@ -63,6 +67,11 @@ import com.pukka.libcore.utils.SpTimeUtils;
|
||||
import com.pukka.libcore.utils.StringUtils;
|
||||
import com.tencent.bugly.crashreport.BuglyLog;
|
||||
|
||||
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;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -76,7 +85,8 @@ import io.reactivex.rxjava3.core.Single;
|
||||
* @Date:2025-05-14-15
|
||||
* @Description:启动页 路由控制
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity implements AsApiRequester.CheckStatusCallback {
|
||||
public class MainActivity extends AppCompatActivity
|
||||
implements AsApiRequester.CheckStatusCallback, NetworkMonitor.NetworkReceiver {
|
||||
private static final String TAG = "MainActivity";
|
||||
private final ViewTreeObserver.OnGlobalFocusChangeListener globalFocusChangeListener = (oldFocus, newFocus) ->
|
||||
LogUtils.d(TAG, "focus changed old=" + formatFocusView(oldFocus) + ", new=" + formatFocusView(newFocus));
|
||||
@@ -84,6 +94,8 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
* 用户协议中的同意按钮点击后发送通知
|
||||
*/
|
||||
private boolean isShowToLogin = true;
|
||||
// 家用版标志:激活界面全程不可见,未激活时静默调用家用版激活码激活
|
||||
private final boolean isHomeEdition = AppConfiguration.getInstance().isHomeEdition();
|
||||
private final AsApiRequester asApiRequester = AsApiRequester.INSTANCE;
|
||||
private Context context;
|
||||
private UserAgreementView userAgreementView;
|
||||
@@ -108,38 +120,64 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
private String intentRoomNo;
|
||||
// 防重入:checkBenefits 多次回调(如 onPreset/onUnbound 等)时只触发一次自动激活
|
||||
private boolean autoActivateAttempted = false;
|
||||
// 家用版静默激活进行中:避免并发回调在第一次静默激活 in-flight 期间误拉网络错误全屏。
|
||||
// 仅由 onActivateFailed(家用版静默激活的真正失败回调)清掉。
|
||||
private boolean isSilentActivationInFlight = false;
|
||||
|
||||
// 无激活码版(as8Scan,NEED_ACTIVATION_CODE=false)默认激活码:
|
||||
// 该变体不展示激活码输入面板,由后端按固定激活码 "8888" 完成激活,
|
||||
// 网络恢复自动重试:true 时收到网络恢复事件直接 return,防并发重试
|
||||
private boolean isRetryingNetworkError = false;
|
||||
// NetworkMonitor 实例:与 BroadcastReceiver 同生命周期
|
||||
private NetworkMonitor networkMonitor;
|
||||
|
||||
// 无激活码版(扫码变体,NEED_ACTIVATION_CODE=false)默认激活码:
|
||||
// 该变体不展示激活码输入面板,由后端按 BuildConfig.DEFAULT_ACTIVATION_CODE 配置的激活码完成激活,
|
||||
// 房间号为空字符串。由 tryAutoActivateWithDefaultCode 直接提交。
|
||||
private static final String DEFAULT_ACTIVATION_CODE = "88888";
|
||||
// 各渠道默认值由 build.gradle 的 productFlavors 定义:as8Scan="88888",fxScan="UK4KQ",普通渠道=""
|
||||
private static final String DEFAULT_ACTIVATION_CODE = BuildConfig.DEFAULT_ACTIVATION_CODE;
|
||||
|
||||
private final AgreementStoragePermissionGateway agreementStoragePermissionGateway = new AgreementStoragePermissionGateway();
|
||||
|
||||
// 语音冷启动待入队命令:见 docs/2026-08-28-语音冷启动链路设计方案.md §4.1
|
||||
// extras / SP 键统一定义在 VoiceExtras,本处只持有内存态。
|
||||
private Bundle pendingVoiceExtras;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
context = this;
|
||||
LogUtils.d("appStart --- activity.onCreate:start");
|
||||
setContentView(R.layout.activity_main);
|
||||
// 第三方变体:直接跳转 ThirdPartyEntryActivity
|
||||
if (AppConfiguration.getInstance().isThirdPartyVariant()) {
|
||||
Intent intent = new Intent(MainActivity.this, ThirdPartyEntryActivity.class);
|
||||
// 透传当前 Intent 的 extras(包含 EPG 传入的参数)
|
||||
intent.putExtras(getIntent());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
//registerGlobalFocusListener();
|
||||
AppManager.getAppManager().addActivity(this);
|
||||
BuildConfigUtils.initBuildConfig(ContextUtils.getApplicationContext());
|
||||
ChannelManger.initChannel();
|
||||
// 解析启动 Intent 中的激活参数(用于自动激活场景)
|
||||
parseAutoActivateIntent();
|
||||
// 捕获语音冷启动待入队 extras(见 docs/2026-08-28-语音冷启动链路设计方案.md §4.1)
|
||||
capturePendingVoiceExtras();
|
||||
initView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cold-launch 经 FLAG_ACTIVITY_CLEAR_TOP 重用现有 MainActivity 实例时(launchMode=standard),
|
||||
* Android 不会回调 onCreate,新 Intent 通过本方法进入。{@code getIntent()} 默认仍返回原始
|
||||
* Intent,必须 setIntent(intent) 才能让 capturePendingVoiceExtras 看到新 Intent 上的
|
||||
* {@code __voice_pending} extras,否则主进程未激活路径下入队会静默丢失。
|
||||
*
|
||||
* <p>详见 docs/2026-08-28-语音冷启动链路设计方案.md §4.1。
|
||||
*/
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
if (intent == null) return;
|
||||
LogUtils.d(TAG, "onNewIntent, hasVoicePending="
|
||||
+ intent.getBooleanExtra(VoiceExtras.EXTRA_VOICE_PENDING, false)
|
||||
+ ", hasActivateCode=" + !TextUtils.isEmpty(intent.getStringExtra("activate_code")));
|
||||
setIntent(intent);
|
||||
// 入队仍走 capturePendingVoiceExtras → goToActivity → enqueuePendingVoiceCommand 单点。
|
||||
capturePendingVoiceExtras();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析启动 Intent 中的 activate_code / room_no 参数。
|
||||
* 仅当两个参数同时存在时,启用后续的 checkBenefits 自动激活流程。
|
||||
@@ -238,6 +276,8 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
submitDeviceActivateCode(activateCode, roomNo);
|
||||
}
|
||||
});
|
||||
// 注册网络变化广播:用于 init 网络失败全屏页展示期间,网络恢复时自动重试
|
||||
registerNetworkMonitor();
|
||||
nextInit();
|
||||
if (userAgreementView.isShow()) {
|
||||
userAgreementView.setVisibility(View.VISIBLE);
|
||||
@@ -402,6 +442,11 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
() -> {
|
||||
// 家用版:静默激活成功不触碰任何激活 UI;真正的进首页由 onActivated 回调驱动
|
||||
if (isHomeEdition) {
|
||||
LogUtils.d(TAG, "home edition activate submitted, wait onActivated to route");
|
||||
return;
|
||||
}
|
||||
deviceNotActivatedView.setSubmitting(false);
|
||||
deviceNotActivatedView.showActivateMessage("激活成功,正在刷新服务状态...");
|
||||
ToastUtils.showShortToastSafe("激活成功");
|
||||
@@ -473,7 +518,45 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 家用版非激活终态统一入口:
|
||||
* - 尚未尝试过静默激活 → 用家用版默认激活码静默激活一次(tryAutoActivateFromIntent 内部短路到默认码)
|
||||
* - 已尝试过仍未激活 → 降级到网络错误全屏(含退出按钮),绝不展示激活输入面板
|
||||
*
|
||||
* 竞态防护:AsApiRequester 异步派发 onPreset/onUnbound/onActivateFailed 等多个非激活回调时,
|
||||
* 仅有第一个回调触发 tryAutoActivateFromIntent;后续回调在 autoActivateAttempted=true 短路到
|
||||
* showHomeActivationFailure。但若静默激活仍在 in-flight 期间被多个回调排队,它们会全部跳到
|
||||
* showNetworkErrorFullscreen 形成"网络错误"全屏闪现。解决:用 isSilentActivationInFlight
|
||||
* 标记静默激活进行中,期间到达的非激活回调静默忽略(不拉全屏),让真正的失败回调(onActivateFailed
|
||||
* 或 onUnbound 终态)统一收口。
|
||||
*/
|
||||
private void onHomeNonActivatedState(AsApiRequester.CheckBenefitsResult result) {
|
||||
if (autoActivateAttempted) {
|
||||
// 静默激活进行中:忽略并发回调,避免拉网络错误全屏闪现。
|
||||
if (isSilentActivationInFlight) {
|
||||
LogUtils.d(TAG, "home edition: silent activation in-flight, ignore duplicate non-activated callback");
|
||||
return;
|
||||
}
|
||||
showHomeActivationFailure(result == null ? null : result.msg);
|
||||
return;
|
||||
}
|
||||
tryAutoActivateFromIntent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 家用版静默激活失败降级:不弹激活输入面板,改用网络错误全屏(含退出按钮)兜底。
|
||||
* @param message 后端返回的错误信息,可为空(空则使用通用兜底文案)
|
||||
*/
|
||||
private void showHomeActivationFailure(String message) {
|
||||
LogUtils.e(TAG, "home edition silent activation failed: " + message);
|
||||
showNetworkErrorFullscreen(message);
|
||||
}
|
||||
|
||||
private void showActivateFail(String message) {
|
||||
if (isHomeEdition) {
|
||||
showHomeActivationFailure(message);
|
||||
return;
|
||||
}
|
||||
String safeMessage = StringUtils.isEmpty(message) ? "激活失败,请稍后重试" : message;
|
||||
runOnUiThread(() -> {
|
||||
deviceNotActivatedView.setSubmitting(false);
|
||||
@@ -566,12 +649,97 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
return;
|
||||
}
|
||||
LogUtils.d(TAG,"goToActivity --- start: ");
|
||||
// 语音冷启动待入队命令:捕获到的 extras 入队到 VoiceCommandService.coldStartQueue,
|
||||
// IndexActivity 首帧渲染完成后由既有 replayColdStartVoiceCommands 钩子回放。
|
||||
if (pendingVoiceExtras != null) {
|
||||
enqueuePendingVoiceCommand();
|
||||
}
|
||||
newIntent = new Intent();
|
||||
newIntent.setClass(MainActivity.this, IndexActivity.class);
|
||||
startActivity(newIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从启动 Intent 或 SP 复原语音冷启动 extras。
|
||||
*
|
||||
* <p>两路来源:
|
||||
* <ol>
|
||||
* <li>当前 Intent 带 {@code __voice_pending=true}(典型场景::voice 进程 cold launch MainActivity,
|
||||
* 或主进程已存活但未激活时 VoiceInternalReceiver 冷启 MainActivity)</li>
|
||||
* <li>Intent 不带 extras 但 SP 标记为 true(典型场景:强制升级后新版 App 启动 / appExit 后再次启动)</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>复原后写入 SP 持久化标志,覆盖"激活完成后才走 goToActivity"或"升级后 MainActivity 重启"
|
||||
* 两条路径——只要没消费就一直保留直到 enqueuePendingVoiceCommand 显式清掉。
|
||||
*/
|
||||
private void capturePendingVoiceExtras() {
|
||||
Intent it = getIntent();
|
||||
if (it != null && it.getBooleanExtra(VoiceExtras.EXTRA_VOICE_PENDING, false)) {
|
||||
pendingVoiceExtras = cloneVoiceExtras(it);
|
||||
persistPendingVoice(pendingVoiceExtras);
|
||||
} else if (SharedPreferenceUtils.getBoolean(VoiceExtras.SP_PENDING_VOICE_FLAG, false)) {
|
||||
pendingVoiceExtras = restorePendingVoice();
|
||||
}
|
||||
}
|
||||
|
||||
private static Bundle cloneVoiceExtras(Intent src) {
|
||||
Bundle bundle = new Bundle();
|
||||
for (String key : new String[]{VoiceExtras.EXTRA_OEM_FILM_NAME, VoiceExtras.EXTRA_OEM_AUTOPLAY,
|
||||
VoiceExtras.EXTRA_OEM_REQUEST_ID, VoiceExtras.EXTRA_OEM_SOURCE_APP}) {
|
||||
String value = src.getStringExtra(key);
|
||||
if (value != null) bundle.putString(key, value);
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private static void persistPendingVoice(Bundle bundle) {
|
||||
if (bundle == null) return;
|
||||
SharedPreferenceUtils.put(VoiceExtras.SP_KEY_PENDING_FILM_NAME, bundle.getString(VoiceExtras.EXTRA_OEM_FILM_NAME, ""));
|
||||
SharedPreferenceUtils.put(VoiceExtras.SP_KEY_PENDING_AUTOPLAY, bundle.getString(VoiceExtras.EXTRA_OEM_AUTOPLAY, ""));
|
||||
SharedPreferenceUtils.put(VoiceExtras.SP_KEY_PENDING_REQUEST_ID, bundle.getString(VoiceExtras.EXTRA_OEM_REQUEST_ID, ""));
|
||||
SharedPreferenceUtils.put(VoiceExtras.SP_KEY_PENDING_SENDER, bundle.getString(VoiceExtras.EXTRA_OEM_SOURCE_APP, ""));
|
||||
SharedPreferenceUtils.put(VoiceExtras.SP_PENDING_VOICE_FLAG, true);
|
||||
}
|
||||
|
||||
private static Bundle restorePendingVoice() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(VoiceExtras.EXTRA_OEM_FILM_NAME, SharedPreferenceUtils.getString(VoiceExtras.SP_KEY_PENDING_FILM_NAME, ""));
|
||||
bundle.putString(VoiceExtras.EXTRA_OEM_AUTOPLAY, SharedPreferenceUtils.getString(VoiceExtras.SP_KEY_PENDING_AUTOPLAY, ""));
|
||||
bundle.putString(VoiceExtras.EXTRA_OEM_REQUEST_ID, SharedPreferenceUtils.getString(VoiceExtras.SP_KEY_PENDING_REQUEST_ID, ""));
|
||||
bundle.putString(VoiceExtras.EXTRA_OEM_SOURCE_APP, SharedPreferenceUtils.getString(VoiceExtras.SP_KEY_PENDING_SENDER, ""));
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 pendingVoiceExtras 解析成 VoiceCommand 并入队 coldStartQueue。
|
||||
*
|
||||
* <p>状态清理规则(修复 code-review finding #8):
|
||||
* 仅当 parseJump 成功入队后才清 pendingVoiceExtras + SP 标志。
|
||||
* parseJump 失败时保留状态,下一次 MainActivity.onCreate 重试;
|
||||
* 避免 parser reject 时命令被静默丢失。
|
||||
*/
|
||||
private void enqueuePendingVoiceCommand() {
|
||||
try {
|
||||
String filmName = pendingVoiceExtras.getString(VoiceExtras.EXTRA_OEM_FILM_NAME);
|
||||
String autoPlay = pendingVoiceExtras.getString(VoiceExtras.EXTRA_OEM_AUTOPLAY);
|
||||
String requestId = pendingVoiceExtras.getString(VoiceExtras.EXTRA_OEM_REQUEST_ID);
|
||||
String sender = pendingVoiceExtras.getString(VoiceExtras.EXTRA_OEM_SOURCE_APP);
|
||||
VoiceCommandService service = VoiceCommandService.getInstance(getApplicationContext());
|
||||
VoiceCommand cmd = SkyworthVodParser.parseJump(service, filmName, autoPlay, requestId, sender);
|
||||
if (cmd != null) {
|
||||
service.enqueueForColdStart(cmd);
|
||||
pendingVoiceExtras = null;
|
||||
SharedPreferenceUtils.put(VoiceExtras.SP_PENDING_VOICE_FLAG, false);
|
||||
LogUtils.d(TAG, "voice command enqueued for cold start: " + requestId);
|
||||
} else {
|
||||
LogUtils.w(TAG, "voice command parse returned null, keep pending for retry. requestId=" + requestId);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
LogUtils.e(TAG, "enqueuePendingVoiceCommand failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息接口
|
||||
*/
|
||||
@@ -592,6 +760,8 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
asApiRequester.requestAsInitDeviceApi(this, new AsApiRequester.InitCallback() {
|
||||
@Override
|
||||
public void onSuccess(AsApiRequester.InitResult result) {
|
||||
// 无论初次调用还是重试,结束即允许下一次网络事件再触发
|
||||
isRetryingNetworkError = false;
|
||||
// 初始化成功,启动心跳调度(应用生命周期内保持活跃)
|
||||
AsHeartBeatManager.getInstance().start(getApplicationContext());
|
||||
|
||||
@@ -619,6 +789,8 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
|
||||
@Override
|
||||
public void onFail(int errorCode, String message) {
|
||||
// 重试回调与初次回调共用同一入口,统一清标志
|
||||
isRetryingNetworkError = false;
|
||||
MainActivity.this.onFail(errorCode, message);
|
||||
// runOnUiThread(()->{
|
||||
// deviceNotActivatedView.setErrorInfo(String.valueOf(errorCode));
|
||||
@@ -704,6 +876,8 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
// }
|
||||
// }
|
||||
AppManager.getAppManager().finishActivity(this);
|
||||
// 反注册网络变化广播
|
||||
unregisterNetworkMonitor();
|
||||
if(asApiRequester != null){
|
||||
asApiRequester.onDestroy();
|
||||
}
|
||||
@@ -796,7 +970,7 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(String message) {
|
||||
public void onFail(int errorCode, String message) {
|
||||
handleSdkInitFailure(thirdCardNo, retryCount, "init_fail", message);
|
||||
}
|
||||
}
|
||||
@@ -884,6 +1058,20 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
public void onActivated(AsApiRequester.CheckBenefitsResult result) {
|
||||
hideNetworkErrorFullscreen();
|
||||
dismissLoadingDialog();
|
||||
// 家用版:激活成功后直接进首页,不弹激活成功 dialog、不展示激活界面。
|
||||
// thirdCardNo 为空是合法终态(与 as8Scan/asAScan line 1069–1083 一致,
|
||||
// 此处不展示"缺少设备码"提示且不调用 initJetsenxSdk,否则下游依赖
|
||||
// JetsenxSdkUtil.cachedUid 的代码(如 IndexActivity 视频初始化)会拿到空 uid)。
|
||||
if (isHomeEdition) {
|
||||
if (result != null && !TextUtils.isEmpty(result.thirdCardNo)) {
|
||||
AppConfiguration.getInstance().setExpireTime(result.expireTime);
|
||||
JetsenxSdkUtil.cacheReusableUid(result.thirdCardNo);
|
||||
initJetsenxSdkWithRetry(result.thirdCardNo, 0);
|
||||
}
|
||||
LogUtils.d(TAG, "home edition onActivated, route to home");
|
||||
goToActivity();
|
||||
return;
|
||||
}
|
||||
if(result != null && !TextUtils.isEmpty(result.thirdCardNo)) {
|
||||
|
||||
AppConfiguration.getInstance().setExpireTime(result.expireTime);
|
||||
@@ -939,8 +1127,20 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCodeActivated(AsApiRequester.CheckBenefitsResult result) {
|
||||
// 10900(已通过激活码激活):onActivated 内已按 result.code 特判 10900 分支,委托复用
|
||||
onActivated(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreset(AsApiRequester.CheckBenefitsResult result) {
|
||||
// 家用版:未激活(预置)状态 → 静默调用家用版激活码激活,不展示激活界面
|
||||
if (isHomeEdition) {
|
||||
LogUtils.d(TAG, "home edition onPreset: trigger silent activation");
|
||||
onHomeNonActivatedState(result);
|
||||
return;
|
||||
}
|
||||
showActivationLoadingDialog(getString(R.string.common_loading_default_text));
|
||||
// 设备处于预置状态(未激活):仅展示 loading,由后续异常回调(onActivateFailed/onUnbound 等)
|
||||
// 或用户主动提交激活码触发真正的激活流程;此处不再自动调用 tryAutoActivateFromIntent
|
||||
@@ -948,6 +1148,11 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
|
||||
@Override
|
||||
public void onActivating(AsApiRequester.CheckBenefitsResult result) {
|
||||
// 家用版:激活中 → 静默,不展示激活 loading
|
||||
if (isHomeEdition) {
|
||||
LogUtils.d(TAG, "home edition onActivating: silent");
|
||||
return;
|
||||
}
|
||||
String loadingMessage = result == null || StringUtils.isEmpty(result.msg)
|
||||
? getString(R.string.common_loading_default_text)
|
||||
: result.msg;
|
||||
@@ -958,6 +1163,14 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
@Override
|
||||
public void onActivateFailed(AsApiRequester.CheckBenefitsResult result) {
|
||||
hideNetworkErrorFullscreen();
|
||||
// 家用版:激活失败 → 静默激活一次或降级网络错误屏,不展示激活界面。
|
||||
// onActivateFailed 是家用版静默激活的"终态失败"回调,清掉 in-flight 标记
|
||||
// 让 onHomeNonActivatedState 把降级全屏的决策接管过去。
|
||||
if (isHomeEdition) {
|
||||
isSilentActivationInFlight = false;
|
||||
onHomeNonActivatedState(result);
|
||||
return;
|
||||
}
|
||||
runOnUiThread(()->{
|
||||
dismissLoadingDialog();
|
||||
deviceNotActivatedView.setErrorInfo(String.valueOf(result.code));
|
||||
@@ -978,6 +1191,11 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
@Override
|
||||
public void onUnbound(AsApiRequester.CheckBenefitsResult result) {
|
||||
hideNetworkErrorFullscreen();
|
||||
// 家用版:未绑定 → 静默激活一次或降级网络错误屏,不展示激活界面
|
||||
if (isHomeEdition) {
|
||||
onHomeNonActivatedState(result);
|
||||
return;
|
||||
}
|
||||
runOnUiThread(()->{
|
||||
dismissLoadingDialog();
|
||||
deviceNotActivatedView.setErrorInfo(String.valueOf(result.code));
|
||||
@@ -997,6 +1215,11 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
LogUtils.d(TAG, "onExpired checkBenefits code=" + (result == null ? "null" : result.code)
|
||||
+ ", cardNos already cleared by AsApiRequester dispatcher");
|
||||
hideNetworkErrorFullscreen();
|
||||
// 家用版:过期 → 静默激活一次或降级网络错误屏,不展示激活界面
|
||||
if (isHomeEdition) {
|
||||
onHomeNonActivatedState(result);
|
||||
return;
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
dismissLoadingDialog();
|
||||
deviceNotActivatedView.setErrorInfo(String.valueOf(result.code));
|
||||
@@ -1018,6 +1241,11 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
LogUtils.d(TAG, "onOccupied checkBenefits code=" + (result == null ? "null" : result.code)
|
||||
+ ", cardNos already cleared by AsApiRequester dispatcher");
|
||||
hideNetworkErrorFullscreen();
|
||||
// 家用版:占用 → 静默激活一次或降级网络错误屏,不展示激活界面
|
||||
if (isHomeEdition) {
|
||||
onHomeNonActivatedState(result);
|
||||
return;
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
dismissLoadingDialog();
|
||||
deviceNotActivatedView.setErrorInfo(String.valueOf(result.code));
|
||||
@@ -1039,6 +1267,11 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
LogUtils.d(TAG, "onUnknown checkBenefits code=" + (result == null ? "null" : result.code)
|
||||
+ ", invalid card status (expired/occupied/code-activated) cardNos already cleared by dispatcher");
|
||||
hideNetworkErrorFullscreen();
|
||||
// 家用版:未知状态 → 静默激活一次或降级网络错误屏,不展示激活界面
|
||||
if (isHomeEdition) {
|
||||
onHomeNonActivatedState(result);
|
||||
return;
|
||||
}
|
||||
|
||||
runOnUiThread(() -> {
|
||||
dismissLoadingDialog();
|
||||
@@ -1053,11 +1286,18 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
|
||||
@Override
|
||||
public void onFail(int errorCode, String message) {
|
||||
if (errorCode == 0) {
|
||||
// 1007 = 网络错误(无网络/无法访问服务器/超时,message 已是友好文案):
|
||||
// 拉起全屏网络错误页(含退出按钮),避免网络问题被误提示为"激活失败"
|
||||
if (errorCode == 0 || errorCode == AsApiErrorCode.ERROR_NETWORK.getCode()) {
|
||||
showNetworkErrorFullscreen(message);
|
||||
return;
|
||||
}
|
||||
hideNetworkErrorFullscreen();
|
||||
// 家用版:激活相关失败不弹激活 UI,统一降级到网络错误屏
|
||||
if (isHomeEdition) {
|
||||
showHomeActivationFailure(message);
|
||||
return;
|
||||
}
|
||||
runOnUiThread(()->{
|
||||
dismissLoadingDialog();
|
||||
deviceNotActivatedView.setErrorInfo(String.valueOf(errorCode));
|
||||
@@ -1118,9 +1358,72 @@ public class MainActivity extends AppCompatActivity implements AsApiRequester.Ch
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册网络变化广播:监听 CONNECTIVITY_CHANGE / WIFI_STATE_CHANGED,
|
||||
* 用于 init 网络错误全屏页期间的网络恢复自动重试。
|
||||
*
|
||||
* 与 BaseActivity 的注册方式保持一致(含 SDK 33+ 的 RECEIVER_NOT_EXPORTED 标记)。
|
||||
*/
|
||||
private void registerNetworkMonitor() {
|
||||
networkMonitor = new NetworkMonitor();
|
||||
networkMonitor.setNetworkNativeReceiver(this);
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
|
||||
filter.addAction("android.net.wifi.WIFI_STATE_CHANGED");
|
||||
filter.addAction("android.net.wifi.STATE_CHANGE");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(networkMonitor, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
} else {
|
||||
registerReceiver(networkMonitor, filter);
|
||||
}
|
||||
}
|
||||
|
||||
private void unregisterNetworkMonitor() {
|
||||
if (networkMonitor != null) {
|
||||
try {
|
||||
unregisterReceiver(networkMonitor);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 极少数情况:onDestroy 前已 unregister,忽略避免崩溃
|
||||
LogUtils.d(TAG, "networkMonitor already unregistered");
|
||||
}
|
||||
networkMonitor = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络状态回调(CONNECTIVITY_CHANGE 广播触发,主线程)。
|
||||
*
|
||||
* 行为:
|
||||
* - 网络可用且全屏网络错误页正显示中(说明 init 刚因网络失败)→ 自动触发重试
|
||||
* - 正在重试 in-flight → 忽略本次事件(防重入)
|
||||
* - 非"unknown/none"视为可用(覆盖 wifi/mobile/ethernet/vpn/other 五种)
|
||||
*/
|
||||
@Override
|
||||
public void currentNetType(String netType) {
|
||||
LogUtils.d(TAG, "currentNetType:" + netType);
|
||||
boolean connected = !"unknown".equals(netType) && !"none".equals(netType);
|
||||
if (connected && isNetworkErrorFullscreen && !isRetryingNetworkError) {
|
||||
// 全屏错误页展示期间收到网络恢复 → 自动重试 init
|
||||
isRetryingNetworkError = true;
|
||||
hideNetworkErrorFullscreen();
|
||||
showActivationLoadingDialog("正在重新连接…");
|
||||
LogUtils.d(TAG, "network restored, auto-retry checkUpgradeThenMembership");
|
||||
checkUpgradeThenMembership();
|
||||
}
|
||||
}
|
||||
|
||||
private void exitApp() {
|
||||
AppManager.getAppManager().setKeep(false);
|
||||
// setKeep(true) 让 AppManager.appExit() 走"仅 finishAllActivity"分支,
|
||||
// 不延迟 1s、不 killProcess;主进程降为无 UI 后台但保留内存态,
|
||||
// :voice 进程与 FGS 不受影响,用户退出后仍能接收 OEM 语音广播(见 §4.4)。
|
||||
// 脆弱点守卫:setKeep(true) 与 appExit() 之间禁止插入任何异步。
|
||||
// isKeep 是 AppManager 单例状态;本次执行完后立即 reset 回 false,避免污染
|
||||
// 后续其它 appExit() 调用方(BaseActivity / IndexActivity / DownApkDialog 等)
|
||||
// 的进程语义。env 切换(changeToPreview/changeToDebug)与 BenefitRevokedDialog.exitApp
|
||||
// 仍走旧路径——见设计文档 §4.4。
|
||||
AppManager.getAppManager().setKeep(true);
|
||||
AppManager.getAppManager().appExit();
|
||||
AppManager.getAppManager().setKeep(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
64
app/src/main/java/com/pukka/jiecheng/MyApplication.java
Executable file → Normal file
64
app/src/main/java/com/pukka/jiecheng/MyApplication.java
Executable file → Normal file
@@ -1,10 +1,13 @@
|
||||
|
||||
package com.pukka.jiecheng;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bytedance.boost_multidex.BoostMultiDex;
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
@@ -14,6 +17,9 @@ import com.pukka.jiecheng.device.DeviceIdentityInitManager;
|
||||
import com.pukka.jiecheng.utils.ActivityMonitor;
|
||||
import com.pukka.jiecheng.utils.BuglyInitManager;
|
||||
import com.pukka.jiecheng.utils.CrashHandler;
|
||||
import com.pukka.jiecheng.voice.receiver.KeepAlivePingReceiver;
|
||||
import com.pukka.jiecheng.voice.receiver.VoiceInternalReceiver;
|
||||
import com.pukka.jiecheng.voice.service.VoiceKeepAliveService;
|
||||
import com.pukka.libcore.fresco.Tls12SocketFactory;
|
||||
import com.pukka.libcore.fresco.TrustAllManager;
|
||||
import com.pukka.libcore.utils.AppLifecycleObserver;
|
||||
@@ -31,6 +37,7 @@ import javax.net.ssl.TrustManager;
|
||||
*/
|
||||
public class MyApplication extends Application {
|
||||
private static final String PROCESS_NAME_MAIN = "tv.huashi.jetsenx.jetsenfilx.jsttv";
|
||||
private static final String VOICE_PROCESS_SUFFIX = ":voice";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
@@ -59,12 +66,67 @@ public class MyApplication extends Application {
|
||||
|
||||
// 网络层初始化(必须在 Fresco.initialize 之前,避免首签请求受 SSL 状态影响)
|
||||
initNetworkLayer();
|
||||
// 跨进程语音命令接收器::voice 进程 SkyworthVoiceAdapter 投递 INTERNAL_FORWARD 时由本接收器处理
|
||||
registerVoiceInternalReceiver();
|
||||
// 兜底拉起 :voice 进程 FGS,覆盖桌面图标 cold start / appExit 后再次进入 / force-stop 后首次启动场景
|
||||
ensureVoiceKeepAliveRunning(this);
|
||||
//preInitWebCore();
|
||||
//LogUtils.d("onCreate: channel_id : "+ AppConfiguration.getInstance().getChannelId()+ " isNeadMy : "+ AppConfiguration.getInstance().isNeedMy() + " control(Touch): "+AppConfiguration.getInstance().isTouchMode());
|
||||
}
|
||||
Fresco.initialize(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态注册主进程语音命令接收器。
|
||||
* 接收 :voice 进程 {@link com.pukka.jiecheng.voice.adapter.SkyworthVoiceAdapter}
|
||||
* 投递的 {@link VoiceInternalReceiver#ACTION_VOICE_FORWARD} 显式广播。
|
||||
*
|
||||
* <p>OEM 创维广播({@code ACTION_VOD})的注册已不在此处——移到 :voice 进程
|
||||
* {@link VoiceKeepAliveService} 的 FGS 内动态注册,避免主进程死亡时丢失接收能力。
|
||||
*/
|
||||
private void registerVoiceInternalReceiver() {
|
||||
try {
|
||||
IntentFilter filter = new IntentFilter(VoiceInternalReceiver.ACTION_VOICE_FORWARD);
|
||||
registerReceiver(new VoiceInternalReceiver(), filter);
|
||||
LogUtils.d("onCreate: voice internal receiver registered");
|
||||
} catch (Throwable t) {
|
||||
LogUtils.e("onCreate: voice internal receiver register failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底拉起 :voice 进程 FGS:仅当 :voice 进程不存在时启动,避免通知闪烁。
|
||||
*
|
||||
* <p>触发场景:
|
||||
* <ul>
|
||||
* <li>桌面图标 cold start(开机后用户首次点开 App)</li>
|
||||
* <li>appExit() 后再次进入(主进程降为后台但 :voice 已被 LMK 杀掉)</li>
|
||||
* <li>force-stop 后用户首次启动</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>BOOT_COMPLETED 拉起由 {@link com.pukka.jiecheng.voice.receiver.BootCompletedReceiver}
|
||||
* 负责;本方法只在 MyApplication.onCreate 时(即用户进 App 的时刻)兜底。
|
||||
*
|
||||
* <p>另外通过 {@link KeepAlivePingReceiver#scheduleFirstPing} 注册 5s 后的自广播 ping,
|
||||
* 保证即便 BOOT_COMPLETED 主路径失败,本兜底也失败的情况下,仍有第三层保险接力。
|
||||
*/
|
||||
private void ensureVoiceKeepAliveRunning(Context ctx) {
|
||||
try {
|
||||
ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (am == null) return;
|
||||
String voiceProcess = ctx.getPackageName() + VOICE_PROCESS_SUFFIX;
|
||||
for (ActivityManager.RunningAppProcessInfo p : am.getRunningAppProcesses()) {
|
||||
if (voiceProcess.equals(p.processName)) return;
|
||||
}
|
||||
ContextCompat.startForegroundService(ctx, new Intent(ctx, VoiceKeepAliveService.class));
|
||||
LogUtils.d("onCreate: voice keepalive service start requested");
|
||||
// 第三层兜底:5s 后 KeepAlivePingReceiver 自广播接力(即便上面失败也接续)
|
||||
KeepAlivePingReceiver.scheduleFirstPing(ctx);
|
||||
} catch (Throwable t) {
|
||||
LogUtils.e("onCreate: voice keepalive service start failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化脚手架新网络层(Retrofit + OkHttp + Moshi + Coroutines)。
|
||||
*
|
||||
|
||||
69
app/src/main/java/com/pukka/jiecheng/TestActivity.java
Executable file → Normal file
69
app/src/main/java/com/pukka/jiecheng/TestActivity.java
Executable file → Normal file
@@ -8,14 +8,28 @@ import android.widget.Button;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.pukka.jiecheng.order.OrderActivity;
|
||||
import com.pukka.jiecheng.request.AsApiRequester;
|
||||
import com.pukka.jiecheng.request.AsHeartBeatManager;
|
||||
import com.pukka.libcore.utils.LogUtils;
|
||||
|
||||
/**
|
||||
* @file TestActivity.java
|
||||
* @description 用于测试的 Activity,包含跳转到 MainActivity 的按钮,以实现简单的联调和入口测试
|
||||
* @description 用于测试的 Activity:进入页面即触发首签拿到 sign,启动心跳;
|
||||
* 然后通过按钮跳转到 OrderActivity,验证 sign 已自动同步到后续接口
|
||||
* @author ainetv
|
||||
* @date 2026-03-20
|
||||
*/
|
||||
public class TestActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "TestActivity";
|
||||
|
||||
/**
|
||||
* AsApiRequester 单例(Kotlin object),由 @JvmStatic 暴露给 Java 端调用。
|
||||
* 与 MainActivity 保持一致:业务侧持有单例引用,避免每次现取。
|
||||
*/
|
||||
private final AsApiRequester asApiRequester = AsApiRequester.INSTANCE;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -25,9 +39,60 @@ public class TestActivity extends AppCompatActivity {
|
||||
btnGoToMain.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(TestActivity.this, MainActivity.class);
|
||||
Intent intent = new Intent(TestActivity.this, OrderActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
// 进入页面即触发首签:拿到 sign 后才允许后续接口访问,否则 OrderActivity
|
||||
// 等业务页面的请求都会因缺 sign 被服务端 401
|
||||
triggerInitAndStartHeartBeat();
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发首签接口,并在成功后启动心跳调度。
|
||||
*
|
||||
* <p>sign 同步链路(关键,整个项目都依赖这条路径):</p>
|
||||
* <ol>
|
||||
* <li>requestAsInitDeviceApi 成功后,AsApiRequester 内部调用
|
||||
* {@code Network.tokenStore.refreshAndPersist(sign)}:</li>
|
||||
* <ul>
|
||||
* <li>写入 TokenStore(SpTokenStore,内存 + SP 异步)—— HeaderInterceptor 每次发请求都从这里读</li>
|
||||
* <li>同步写入 SharedPreferenceUtils(key="as_sign")—— 兼容旧 Java 代码直接读 SP</li>
|
||||
* </ul>
|
||||
* <li>HeaderInterceptor.intercept() 在每个 OkHttp 请求发出前注入 {@code sign} 请求头
|
||||
* (首签/刷新自身接口除外),所以从 TestActivity 跳转到 OrderActivity 后,
|
||||
* OrderActivity 发起的任意接口(订单列表、扫码等)都会自动带上正确的 sign,
|
||||
* 无需在跳转处做任何额外传参。</li>
|
||||
* <li>AsHeartBeatManager 内部的心跳请求也走同一套 HeaderInterceptor,
|
||||
* 因此心跳能持续携带有效 sign;服务端后续若在心跳响应里下发新的 sign
|
||||
* ({@code refreshSign} 等命令),TokenStore 也会被同步刷新,下一次请求自动接力。</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>心跳启动必须用 getApplicationContext():AsHeartBeatManager 内部用
|
||||
* DispatchQueue.global.timer 启动了一个常驻定时器(应用生命周期内保持活跃),
|
||||
* Activity context 会泄漏,applicationContext 安全。</p>
|
||||
*/
|
||||
private void triggerInitAndStartHeartBeat() {
|
||||
asApiRequester.requestAsInitDeviceApi(this, new AsApiRequester.InitCallback() {
|
||||
@Override
|
||||
public void onSuccess(AsApiRequester.InitResult result) {
|
||||
// 初始化成功,启动心跳调度(应用生命周期内保持活跃)
|
||||
// 心跳本身也带 sign(HeaderInterceptor 自动注入),并在响应里可下发控制命令
|
||||
AsHeartBeatManager.getInstance().start(getApplicationContext());
|
||||
|
||||
String signPreview = (result != null && result.sign != null && !result.sign.isEmpty())
|
||||
? result.sign.substring(0, Math.min(8, result.sign.length())) + "…"
|
||||
: "null";
|
||||
LogUtils.i(TAG, "init success, sign=" + signPreview + ", roomNo=" + result.roomNo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(int errorCode, String message) {
|
||||
// 首签失败:心跳无法启动(缺 sign 触发前置校验 1001),OrderActivity 跳转后
|
||||
// 所有接口都会 401。仅打日志,不静默吞掉——调试页应该把失败暴露给开发者。
|
||||
LogUtils.e(TAG, "requestAsInitDeviceApi fail, code=" + errorCode + ", msg=" + message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,500 +0,0 @@
|
||||
package com.pukka.jiecheng;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.pukka.jiecheng.config.AppConfiguration;
|
||||
import com.pukka.jiecheng.index.IndexActivity;
|
||||
import com.pukka.jiecheng.request.AsApiRequester;
|
||||
import com.pukka.jiecheng.utils.BuildConfigUtils;
|
||||
import com.pukka.jiecheng.utils.ChannelManger;
|
||||
import com.pukka.jiecheng.utils.JetsenxSdkUtil;
|
||||
import com.pukka.jiecheng.video.VideoDetailActivity;
|
||||
import com.pukka.jiecheng.widget.DeviceUnregisteredView;
|
||||
import com.pukka.libcore.threading.DispatchQueue;
|
||||
import com.pukka.libcore.utils.AppManager;
|
||||
import com.pukka.libcore.utils.ContextUtils;
|
||||
import com.pukka.libcore.utils.LogUtils;
|
||||
import com.pukka.libcore.utils.StringUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.net.NetworkInterface;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 第三方 EPG 调用入口 Activity。
|
||||
*
|
||||
* 仅在 thirdparty flavor 中使用,通过 Intent action
|
||||
* {@code com.pukka.jiecheng.action.THIRD_PARTY_PLAY} 启动。
|
||||
*
|
||||
* 流程:
|
||||
* 1. 解析 Intent extras(mac/wifiMac/deviceSn/deviceUid/programCode)
|
||||
* 2. 注入 ThirdPartyDeviceParams 到 AsApiRequester
|
||||
* 3. 执行 init -> checkBenefits 流程
|
||||
* 4. 权益通过:初始化 JetsenxSdk -> 路由到 VideoDetailActivity 或 IndexActivity
|
||||
* 5. 权益未通过:展示 DeviceUnregisteredView 阻断页
|
||||
*/
|
||||
public class ThirdPartyEntryActivity extends AppCompatActivity implements AsApiRequester.CheckStatusCallback {
|
||||
|
||||
private static final String TAG = "ThirdPartyEntryActivity";
|
||||
|
||||
private static final String EXTRA_MAC = "mac";
|
||||
private static final String EXTRA_WIFI_MAC = "wifiMac";
|
||||
private static final String EXTRA_DEVICE_SN = "deviceSn";
|
||||
private static final String EXTRA_DEVICE_UID = "deviceUid";
|
||||
private static final String EXTRA_PROGRAM_CODE = "programCode";
|
||||
|
||||
/** init 业务错误(第三方后端 code=400 设备未注册)提示文案 */
|
||||
private static final String MSG_DEVICE_NOT_REGISTERED = "设备未注册,请返回首页购买";
|
||||
/** checkBenefits 权益未生效(非 ACTIVE 终态/轮询超时)提示文案 */
|
||||
private static final String MSG_BENEFIT_NOT_BOUND = "设备未绑定权益,请返回首页购买";
|
||||
/** MAC 硬件锚定校验失败(EPG 传入的 mac 不属于本机网卡)提示文案 */
|
||||
private static final String MSG_DEVICE_MISMATCH = "设备校验失败,请返回首页重新进入";
|
||||
|
||||
/** 第三方后端 init 设备未注册业务错误码 */
|
||||
private static final int INIT_CODE_DEVICE_NOT_REGISTERED = 400;
|
||||
/** 权益轮询超时内部错误码(对齐 AsApiRequester POLLING_TIMEOUT 路径) */
|
||||
private static final int CHECK_CODE_POLLING_TIMEOUT = 1008;
|
||||
|
||||
private final AsApiRequester asApiRequester = AsApiRequester.INSTANCE;
|
||||
|
||||
private DeviceUnregisteredView deviceUnregisteredView;
|
||||
private View networkErrorContainer;
|
||||
private TextView networkErrorMessageView;
|
||||
private Button networkErrorRetryButton;
|
||||
private View loadingContainer;
|
||||
private TextView loadingMessageView;
|
||||
|
||||
private final Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private String programCode;
|
||||
|
||||
/** EPG 通过 Intent 传入的 mac(用于 MAC 硬件锚定校验) */
|
||||
private String passedMac;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_thirdparty_entry);
|
||||
AppManager.getAppManager().addActivity(this);
|
||||
// 与原版 MainActivity 对齐的全局初始化:
|
||||
// 1. BuildConfigUtils:同步 FLAVOR/BUILD_TYPE/VERSION 到 libcore(JcConfig 域名选择依赖)
|
||||
// 2. ChannelManger.initChannel():覆盖 SDK 凭证为正式业务参数(ACCESSER/AGCODE/comboCode)。
|
||||
// 缺失会导致 SDK 以默认测试凭证(JSTPKTEST)初始化,后端天卡权益挂在正式凭证下,
|
||||
// playAuth 鉴权返回 -110 未订购。
|
||||
BuildConfigUtils.initBuildConfig(ContextUtils.getApplicationContext());
|
||||
ChannelManger.initChannel();
|
||||
parseIntentExtras();
|
||||
initView();
|
||||
// MAC 硬件锚定校验:EPG 传入的 mac 必须属于本机网卡,防止一个有效凭证多设备共用
|
||||
if (!verifyEpgMacAnchoring()) {
|
||||
showDeviceUnregistered(0, MSG_DEVICE_MISMATCH);
|
||||
return;
|
||||
}
|
||||
initCheckBenefitsFlow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Intent extras,组装 ThirdPartyDeviceParams 并注入 AsApiRequester。
|
||||
*/
|
||||
private void parseIntentExtras() {
|
||||
Intent intent = getIntent();
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
String mac = intent.getStringExtra(EXTRA_MAC);
|
||||
String wifiMac = intent.getStringExtra(EXTRA_WIFI_MAC);
|
||||
String deviceSn = intent.getStringExtra(EXTRA_DEVICE_SN);
|
||||
String deviceUid = intent.getStringExtra(EXTRA_DEVICE_UID);
|
||||
programCode = intent.getStringExtra(EXTRA_PROGRAM_CODE);
|
||||
passedMac = mac;
|
||||
|
||||
AsApiRequester.ThirdPartyDeviceParams params = new AsApiRequester.ThirdPartyDeviceParams(
|
||||
mac, wifiMac, deviceSn, deviceUid);
|
||||
AsApiRequester.setThirdPartyDeviceParams(params);
|
||||
|
||||
// EPG 传入的 mac 同步覆盖到 SDK init:后端权益以 EPG 注册的 MAC 绑定,
|
||||
// JetsenxSdkUtil.init 简化重载内部不再回退本机自采集 MAC(否则权益不生效)
|
||||
JetsenxSdkUtil.setMacOverride(mac);
|
||||
|
||||
LogUtils.d(TAG, "parseIntentExtras: mac=" + mac + ", deviceSn=" + deviceSn
|
||||
+ ", deviceUid=" + deviceUid + ", programCode=" + programCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* MAC 硬件锚定校验(防一个有效凭证多设备共用)。
|
||||
*
|
||||
* 背景:app 完全信任 EPG 传入的 mac 做设备识别与 SDK 鉴权,若恶意 EPG 把已注册设备的
|
||||
* mac 传给任意其他设备,即可用一个凭证给所有设备轮流播放。本校验把设备身份锚定到硬件:
|
||||
* EPG 传入的 mac 必须匹配本机任一网卡的真实硬件 MAC。
|
||||
*
|
||||
* 规则:
|
||||
* - 未传 mac:不校验(走自采集,由后端注册匹配兜底)
|
||||
* - 传入 mac 匹配本机任一网卡(eth0/wlan0 等):通过
|
||||
* - 传入 mac 不匹配任何本机网卡:阻断
|
||||
* - 本机网卡 MAC 全部读取失败:放行(fail-open,避免极端权限环境误伤正常设备)
|
||||
*
|
||||
* 兼容性:EPG 可能读取与本 app 不同的网卡(如 EPG 读 wlan0、app 默认自采集 eth0),
|
||||
* 因此校验集包含本机全部网卡的 MAC,而非仅 app 自采集那一个。
|
||||
*/
|
||||
private boolean verifyEpgMacAnchoring() {
|
||||
if (TextUtils.isEmpty(passedMac)) {
|
||||
LogUtils.d(TAG, "MAC anchoring skip: no mac passed");
|
||||
return true;
|
||||
}
|
||||
Set<String> deviceMacs = collectDeviceMacAddresses();
|
||||
if (deviceMacs.isEmpty()) {
|
||||
LogUtils.w(TAG, "MAC anchoring fail-open: no device mac readable");
|
||||
return true;
|
||||
}
|
||||
boolean matched = deviceMacs.contains(normalizeMac(passedMac));
|
||||
LogUtils.d(TAG, "MAC anchoring result=" + matched
|
||||
+ ", passedMac=" + passedMac + ", deviceMacCount=" + deviceMacs.size());
|
||||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集本机所有网卡的硬件 MAC(归一化形式:小写、去冒号/横线)。
|
||||
* 双通道读取:NetworkInterface 遍历 + sysfs 网卡 address 文件兜底。
|
||||
* 过滤回环、全零、格式非法的地址。
|
||||
*/
|
||||
private Set<String> collectDeviceMacAddresses() {
|
||||
Set<String> macs = new HashSet<>();
|
||||
// 通道一:NetworkInterface 遍历(覆盖 eth0/wlan0/物理及虚拟网卡)
|
||||
try {
|
||||
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||
while (interfaces != null && interfaces.hasMoreElements()) {
|
||||
NetworkInterface ni = interfaces.nextElement();
|
||||
if (ni == null || ni.isLoopback() || ni.isPointToPoint()) {
|
||||
continue;
|
||||
}
|
||||
byte[] hw = ni.getHardwareAddress();
|
||||
addNormalizedMac(macs, bytesToMac(hw));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "collectDeviceMacAddresses NetworkInterface error: " + e.getMessage());
|
||||
}
|
||||
// 通道二:/sys/class/net/*/address 文件(部分系统 NetworkInterface 拿不到硬件地址)
|
||||
try {
|
||||
File[] ifaces = new File("/sys/class/net").listFiles();
|
||||
if (ifaces != null) {
|
||||
for (File iface : ifaces) {
|
||||
BufferedReader reader = null;
|
||||
try {
|
||||
reader = new BufferedReader(new FileReader(new File(iface, "address")));
|
||||
String line = reader.readLine();
|
||||
addNormalizedMac(macs, line);
|
||||
} catch (Exception ignore) {
|
||||
// 单个接口读取失败不影响其他接口
|
||||
} finally {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "collectDeviceMacAddresses sysfs error: " + e.getMessage());
|
||||
}
|
||||
return macs;
|
||||
}
|
||||
|
||||
/** 归一化并加入集合;过滤空值、全零、长度非 12 的非法地址 */
|
||||
private void addNormalizedMac(Set<String> out, String mac) {
|
||||
String normalized = normalizeMac(mac);
|
||||
if (normalized.length() == 12
|
||||
&& !"000000000000".equals(normalized)
|
||||
&& !normalized.matches("0+")) {
|
||||
out.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
/** MAC 归一化:小写、去掉冒号/横线、去首尾空白 */
|
||||
private String normalizeMac(String mac) {
|
||||
if (mac == null) {
|
||||
return "";
|
||||
}
|
||||
return mac.trim().toLowerCase(Locale.ROOT).replace(":", "").replace("-", "");
|
||||
}
|
||||
|
||||
/** 字节数组转冒号分隔的 MAC 字符串 */
|
||||
private String bytesToMac(byte[] bytes) {
|
||||
if (bytes == null || bytes.length != 6) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(17);
|
||||
for (int i = 0; i < bytes.length; i++) {
|
||||
if (i > 0) {
|
||||
sb.append(':');
|
||||
}
|
||||
sb.append(String.format("%02x", bytes[i]));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
deviceUnregisteredView = findViewById(R.id.device_unregistered_view);
|
||||
networkErrorContainer = findViewById(R.id.network_error_container);
|
||||
networkErrorMessageView = findViewById(R.id.network_error_message);
|
||||
networkErrorRetryButton = findViewById(R.id.network_error_retry_btn);
|
||||
loadingContainer = findViewById(R.id.loading_container);
|
||||
loadingMessageView = findViewById(R.id.loading_message);
|
||||
|
||||
networkErrorRetryButton.setFocusable(true);
|
||||
networkErrorRetryButton.setFocusableInTouchMode(true);
|
||||
networkErrorRetryButton.setClickable(true);
|
||||
networkErrorRetryButton.setEnabled(true);
|
||||
networkErrorRetryButton.setOnClickListener(v -> initCheckBenefitsFlow());
|
||||
|
||||
deviceUnregisteredView.setOnExitListener(this::exitApp);
|
||||
}
|
||||
|
||||
/**
|
||||
* init -> checkBenefits 流程。
|
||||
* 参考 MainActivity.checkUpgradeThenMembership,跳过升级检查。
|
||||
*/
|
||||
private void initCheckBenefitsFlow() {
|
||||
hideNetworkError();
|
||||
hideDeviceUnregistered();
|
||||
showLoading("正在初始化...");
|
||||
|
||||
asApiRequester.requestAsInitDeviceApi(this, new AsApiRequester.InitCallback() {
|
||||
@Override
|
||||
public void onSuccess(AsApiRequester.InitResult result) {
|
||||
asApiRequester.requestAsCheckBenefitsApi(
|
||||
ThirdPartyEntryActivity.this, ThirdPartyEntryActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(int errorCode, String message) {
|
||||
if (errorCode == INIT_CODE_DEVICE_NOT_REGISTERED) {
|
||||
// 服务端业务错误:设备未在后端注册,展示阻断页(文案固定)
|
||||
showDeviceUnregistered(0, MSG_DEVICE_NOT_REGISTERED);
|
||||
} else {
|
||||
// 网络/内部错误:展示重试页
|
||||
showNetworkError(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====== CheckStatusCallback ======
|
||||
|
||||
@Override
|
||||
public void onActivated(AsApiRequester.CheckBenefitsResult result) {
|
||||
hideLoading();
|
||||
hideNetworkError();
|
||||
routeAfterActivated(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreset(AsApiRequester.CheckBenefitsResult result) {
|
||||
String msg = (result == null || StringUtils.isEmpty(result.msg))
|
||||
? "正在激活..." : result.msg;
|
||||
showLoading(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivating(AsApiRequester.CheckBenefitsResult result) {
|
||||
String msg = (result == null || StringUtils.isEmpty(result.msg))
|
||||
? "正在激活..." : result.msg;
|
||||
showLoading(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivateFailed(AsApiRequester.CheckBenefitsResult result) {
|
||||
showDeviceUnregistered(0, MSG_BENEFIT_NOT_BOUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnbound(AsApiRequester.CheckBenefitsResult result) {
|
||||
showDeviceUnregistered(0, MSG_BENEFIT_NOT_BOUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExpired(AsApiRequester.CheckBenefitsResult result) {
|
||||
showDeviceUnregistered(0, MSG_BENEFIT_NOT_BOUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOccupied(AsApiRequester.CheckBenefitsResult result) {
|
||||
showDeviceUnregistered(0, MSG_BENEFIT_NOT_BOUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnknown(AsApiRequester.CheckBenefitsResult result) {
|
||||
showDeviceUnregistered(0, MSG_BENEFIT_NOT_BOUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(int errorCode, String message) {
|
||||
if (errorCode == CHECK_CODE_POLLING_TIMEOUT) {
|
||||
// 权益轮询超时(设备已注册但卡一直未生效 ACTIVE):权益未绑定,展示阻断页
|
||||
showDeviceUnregistered(0, MSG_BENEFIT_NOT_BOUND);
|
||||
} else {
|
||||
// 网络错误(0)或内部错误(1001/1003/1004/1007):展示重试页
|
||||
showNetworkError(message);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 权益通过后的路由 ======
|
||||
|
||||
/**
|
||||
* 权益通过后:
|
||||
* 1. 清理 ThirdPartyDeviceParams
|
||||
* 2. 用 thirdCardNo 初始化 JetsenxSdk
|
||||
* 3. 有 programCode -> VideoDetailActivity 播放;无 programCode -> IndexActivity
|
||||
* 4. finish 当前 Activity
|
||||
*/
|
||||
private void routeAfterActivated(AsApiRequester.CheckBenefitsResult result) {
|
||||
AsApiRequester.clearThirdPartyDeviceParams();
|
||||
|
||||
String thirdCardNo = result != null ? result.thirdCardNo : "";
|
||||
if (TextUtils.isEmpty(thirdCardNo)) {
|
||||
showDeviceUnregistered(0, MSG_BENEFIT_NOT_BOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.expireTime != null) {
|
||||
AppConfiguration.getInstance().setExpireTime(result.expireTime);
|
||||
}
|
||||
JetsenxSdkUtil.cacheReusableUid(thirdCardNo);
|
||||
initJetsenxSdkWithRetry(thirdCardNo, 0);
|
||||
|
||||
if (!TextUtils.isEmpty(programCode)) {
|
||||
VideoDetailActivity.start(this, programCode, "",
|
||||
VideoDetailActivity.ModuleSourceType.HOME_PAGE, false);
|
||||
} else {
|
||||
Intent intent = new Intent(this, IndexActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用 thirdCardNo 初始化 JetsenxSdk,失败最多重试 3 次。
|
||||
* 参考 MainActivity.initJetsenxSdkWithRetry,在子线程执行避免 ANR。
|
||||
*/
|
||||
private void initJetsenxSdkWithRetry(String thirdCardNo, int retryCount) {
|
||||
DispatchQueue.global.async(() -> {
|
||||
try {
|
||||
JetsenxSdkUtil.init(
|
||||
ContextUtils.getApplicationContext(),
|
||||
thirdCardNo, new JetsenxSdkUtil.InitMemberCodeCallback() {
|
||||
@Override
|
||||
public void onSuccess(String memberCode) {
|
||||
JetsenxSdkUtil.cacheReusableUid(thirdCardNo);
|
||||
LogUtils.d(TAG, "Jetsenx SDK init success, cache thirdCardNo");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(String message) {
|
||||
LogUtils.e(TAG, "Jetsenx SDK init fail: " + message);
|
||||
if (retryCount < 3) {
|
||||
mainHandler.postDelayed(
|
||||
() -> initJetsenxSdkWithRetry(thirdCardNo, retryCount + 1), 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (Exception e) {
|
||||
LogUtils.e(TAG, "Jetsenx SDK init exception: " + e.getMessage());
|
||||
if (retryCount < 3) {
|
||||
mainHandler.postDelayed(
|
||||
() -> initJetsenxSdkWithRetry(thirdCardNo, retryCount + 1), 1500);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====== UI 状态控制 ======
|
||||
|
||||
private void showLoading(String message) {
|
||||
runOnUiThread(() -> {
|
||||
hideNetworkError();
|
||||
hideDeviceUnregistered();
|
||||
loadingContainer.setVisibility(View.VISIBLE);
|
||||
if (!TextUtils.isEmpty(message)) {
|
||||
loadingMessageView.setText(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void hideLoading() {
|
||||
runOnUiThread(() -> loadingContainer.setVisibility(View.GONE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示设备未注册阻断页。
|
||||
*/
|
||||
private void showDeviceUnregistered(int errorCode, String message) {
|
||||
runOnUiThread(() -> {
|
||||
hideLoading();
|
||||
hideNetworkError();
|
||||
deviceUnregisteredView.setStatusInfo(errorCode, message);
|
||||
deviceUnregisteredView.setVisibility(View.VISIBLE);
|
||||
});
|
||||
}
|
||||
|
||||
private void hideDeviceUnregistered() {
|
||||
deviceUnregisteredView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示网络错误页,重试按钮重新执行 initCheckBenefitsFlow。
|
||||
*/
|
||||
private void showNetworkError(String message) {
|
||||
runOnUiThread(() -> {
|
||||
hideLoading();
|
||||
hideDeviceUnregistered();
|
||||
String displayMessage = StringUtils.isEmpty(message)
|
||||
? getString(R.string.str_network_error_fallback) : message;
|
||||
networkErrorMessageView.setText(displayMessage);
|
||||
networkErrorContainer.setVisibility(View.VISIBLE);
|
||||
networkErrorRetryButton.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
private void hideNetworkError() {
|
||||
networkErrorContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// ====== 生命周期 ======
|
||||
|
||||
private void exitApp() {
|
||||
AppManager.getAppManager().setKeep(false);
|
||||
AppManager.getAppManager().appExit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
exitApp();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
AppManager.getAppManager().finishActivity(this);
|
||||
if (asApiRequester != null) {
|
||||
asApiRequester.onDestroy();
|
||||
}
|
||||
mainHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
}
|
||||
0
app/src/main/java/com/pukka/jiecheng/aboutmy/AboutMyActivity.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/aboutmy/AboutMyActivity.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/aboutmy/LongPressSpanHelper.kt
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/aboutmy/LongPressSpanHelper.kt
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseActivity.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseActivity.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseOnItemClickListener.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseOnItemClickListener.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseOnItemFocusChangeListener.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseOnItemFocusChangeListener.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BasePresenter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BasePresenter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BasePresenterViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BasePresenterViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseRecyclerAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseRecyclerAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseRecyclerViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseRecyclerViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/BaseViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/SystemConfigManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/base/SystemConfigManager.java
Executable file → Normal file
25
app/src/main/java/com/pukka/jiecheng/config/AppConfiguration.java
Executable file → Normal file
25
app/src/main/java/com/pukka/jiecheng/config/AppConfiguration.java
Executable file → Normal file
@@ -12,6 +12,7 @@ import com.pukka.jiecheng.BuildConfig;
|
||||
* 3. 提供构建类型判断 (isRelease)
|
||||
* 4. 提供酒店类型获取 (getHotelType)
|
||||
* 5. 提供是否显示我的页面配置 (isNeedMy)
|
||||
* 6. 提供家用版判断 (isHomeEdition)
|
||||
*
|
||||
* - 屏蔽底层 BuildConfig 细节,解耦业务逻辑。
|
||||
* - 统一入口,便于未来修改配置源。
|
||||
@@ -108,7 +109,7 @@ public class AppConfiguration {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要显示"我的"页面
|
||||
* 是否需要显示“我的”页面
|
||||
* @return true: 显示; false: 隐藏
|
||||
*/
|
||||
public boolean isNeedMy() {
|
||||
@@ -116,11 +117,21 @@ public class AppConfiguration {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为第三方变体
|
||||
* @return true: thirdparty flavor; false: 其他渠道
|
||||
* 是否为家用版(home edition)
|
||||
* @return true: 家用版; false: 商用版
|
||||
*/
|
||||
public boolean isThirdPartyVariant() {
|
||||
return BuildConfig.THIRDPARTY_VARIANT;
|
||||
public boolean isHomeEdition() {
|
||||
return BuildConfig.IS_HOME_EDITION;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为行业版(商用版中非扫码变体):aishi / ruigeer / ceshi 等。
|
||||
* 行业版语义上必须先激活,checkBenefits 业务码只能是 10200;落到 10900 / 10300-10800
|
||||
* 视为后端异常,由 {@link com.pukka.jiecheng.video.VideoDetailActivity}
|
||||
* 走 showEntitlementError 错误视图,不引导付费。
|
||||
*/
|
||||
public boolean isIndustryEdition() {
|
||||
return !isHomeEdition() && !BuildConfig.SCAN_VARIANT;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,6 +139,10 @@ public class AppConfiguration {
|
||||
* @return 心跳请求间隔周期 (毫秒)
|
||||
*/
|
||||
public long getHeartBeatInterval() {
|
||||
// Debug环境下心跳间隔10秒,其他环境使用默认5分钟
|
||||
if (!isRelease()) {
|
||||
return 30000L;
|
||||
}
|
||||
return HEART_BEAT_INTERVAL;
|
||||
}
|
||||
|
||||
|
||||
45
app/src/main/java/com/pukka/jiecheng/device/DeviceActivity.kt
Executable file → Normal file
45
app/src/main/java/com/pukka/jiecheng/device/DeviceActivity.kt
Executable file → Normal file
@@ -5,11 +5,13 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.pukka.jiecheng.R
|
||||
import com.pukka.jiecheng.base.BaseActivity
|
||||
import com.pukka.jiecheng.config.AppConfiguration
|
||||
import com.pukka.jiecheng.utils.Surround51Manager
|
||||
import com.pukka.jiecheng.utils.ToastUtils
|
||||
import com.pukka.libcore.base.BaseConst
|
||||
import com.pukka.libcore.boxShared.BoxSharedParamsUtils
|
||||
@@ -37,7 +39,10 @@ class DeviceActivity : BaseActivity() {
|
||||
var mTvMac: TextView? = null
|
||||
var mTvSn: TextView? = null
|
||||
var mTvExpireTime: TextView? = null
|
||||
var mClCheckUpdate: ConstraintLayout? = null
|
||||
var mTvAudioPath: TextView? = null
|
||||
var mLlSurround51: LinearLayout? = null
|
||||
var mTvSurround51Value: TextView? = null
|
||||
var mClConfirm: ConstraintLayout? = null
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_device)
|
||||
@@ -68,20 +73,40 @@ class DeviceActivity : BaseActivity() {
|
||||
mTvExpireTime!!.text = "暂无数据"
|
||||
}
|
||||
|
||||
mClCheckUpdate = findViewById<ConstraintLayout>(R.id.id_cl_check_update)
|
||||
// 音频能力检测:AC3直通 > 平台硬解 > FFmpeg软解
|
||||
mTvAudioPath = findViewById<TextView>(R.id.id_tv_audio_path)
|
||||
mTvAudioPath!!.text = Surround51Manager.getAudioPathDesc(this)
|
||||
// 5.1 环绕声播放开关:关闭时播放普通 2.0 档
|
||||
mLlSurround51 = findViewById<LinearLayout>(R.id.id_ll_surround51)
|
||||
mTvSurround51Value = findViewById<TextView>(R.id.id_tv_surround51_value)
|
||||
updateSurround51Value()
|
||||
mLlSurround51?.setOnClickListener {
|
||||
Surround51Manager.set51Enabled(!Surround51Manager.is51Enabled())
|
||||
updateSurround51Value()
|
||||
ToastUtils.showShortToastSafe(
|
||||
if (Surround51Manager.is51Enabled()) "已开启 5.1 环绕声,重新进入播放生效" else "已关闭 5.1 环绕声,播放普通 2.0 声道"
|
||||
)
|
||||
}
|
||||
|
||||
mClConfirm = findViewById<ConstraintLayout>(R.id.id_cl_confirm)
|
||||
initGamePad()
|
||||
enableBackButton()
|
||||
}
|
||||
|
||||
private fun updateSurround51Value() {
|
||||
mTvSurround51Value?.text = if (Surround51Manager.is51Enabled()) "开" else "关"
|
||||
}
|
||||
|
||||
private fun initListener() {
|
||||
// mClCheckUpdate!!.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
// // TODO: 2025/6/10 APK更新
|
||||
//// DownApkDialog(this).initDown2()
|
||||
//
|
||||
// //检测更新
|
||||
// // TODO: 2025/6/10 APK更新
|
||||
//// new DownApkDialog(getActivity()).initDown2();
|
||||
mClConfirm?.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
finish()
|
||||
// TODO: 2025/6/10 APK更新
|
||||
// DownApkDialog(this).initDown2()
|
||||
|
||||
//检测更新
|
||||
// TODO: 2025/6/10 APK更新
|
||||
// new DownApkDialog(getActivity()).initDown2();
|
||||
// ToastUtils.showLongBottomToast("暂无新版本")
|
||||
// })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
0
app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityInitManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityInitManager.java
Executable file → Normal file
6
app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityProvider.java
Executable file → Normal file
6
app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityProvider.java
Executable file → Normal file
@@ -9,7 +9,7 @@ import com.pukka.jiecheng.config.AppConfiguration;
|
||||
import com.pukka.libcore.boxShared.BoxSharedParamsUtils;
|
||||
import com.pukka.libcore.utils.LogUtils;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -87,7 +87,7 @@ public class DeviceIdentityProvider {
|
||||
return "";
|
||||
}
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
byte[] hashBytes = digest.digest(source.getBytes(Charset.forName("UTF-8")));
|
||||
byte[] hashBytes = digest.digest(source.getBytes(StandardCharsets.UTF_8));
|
||||
int usedBytes = Math.min(MD5_TRUNCATED_BYTES, hashBytes.length);
|
||||
if (usedBytes <= 0) {
|
||||
return "";
|
||||
@@ -112,7 +112,7 @@ public class DeviceIdentityProvider {
|
||||
return "";
|
||||
}
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hashBytes = digest.digest(source.getBytes(Charset.forName("UTF-8")));
|
||||
byte[] hashBytes = digest.digest(source.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexBuilder = new StringBuilder(hashBytes.length * 2);
|
||||
for (byte hashByte : hashBytes) {
|
||||
hexBuilder.append(String.format(Locale.ROOT, "%02X", hashByte));
|
||||
|
||||
0
app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityStore.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityStore.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/device/OaidProvider.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/device/OaidProvider.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/downapk/DownApkDialog.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/downapk/DownApkDialog.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/downapk/DownloadApkManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/downapk/DownloadApkManager.java
Executable file → Normal file
@@ -0,0 +1,181 @@
|
||||
package com.pukka.jiecheng.downapk
|
||||
|
||||
import android.app.Activity
|
||||
import com.pukka.jiecheng.config.AppConfiguration
|
||||
import com.pukka.jiecheng.device.DeviceIdentityInitManager
|
||||
import com.pukka.jiecheng.network.ApiRequesters
|
||||
import com.pukka.jiecheng.network.api.StoreVersionApi
|
||||
import com.pukka.jiecheng.network.dto.StoreVersionInfoDto
|
||||
import com.pukka.jiecheng.network.dto.StoreVersionReq
|
||||
import com.pukka.jiecheng.network.dto.StoreVersionDto
|
||||
import com.pukka.jiecheng.network.from
|
||||
import com.pukka.jiecheng.network.infra.ApiError
|
||||
import com.pukka.jiecheng.network.map
|
||||
import com.pukka.jiecheng.utils.ToastUtils
|
||||
import com.pukka.libcore.boxShared.MacUtils
|
||||
import com.pukka.libcore.utils.AppUtils
|
||||
import com.pukka.libcore.utils.LogUtils
|
||||
|
||||
/**
|
||||
* App 内手动触发升级入口(酒店版"我的"页面专用)。
|
||||
*
|
||||
* 流程:
|
||||
* 1. 拼装 store-version 请求(设备标识 + 渠道/控制类型)
|
||||
* 2. 调 POST /api/jst/device/store-version
|
||||
* 3. 服务端返回 data.version 后,按当前编译变体选 touchUrl / keyUrl
|
||||
* 4. 复用 [UpgradeHelper.upgrade] 弹框 + 下载 + 安装链路(不引入新的弹框)
|
||||
*
|
||||
* Java 调用方通过 [checkUpdate] 直接进入;采用 Kotlin object + @JvmStatic 是
|
||||
* 为了让 Java 侧只需一行静态调用即可,无需实例化或处理 Kotlin object 单例语义。
|
||||
*/
|
||||
object StoreVersionUpgrader {
|
||||
|
||||
private const val TAG = "StoreVersionUpgrader"
|
||||
private const val MSG_UP_TO_DATE = "已是最新版本"
|
||||
private const val MSG_NETWORK_ERROR = "网络异常,请稍后重试"
|
||||
|
||||
/**
|
||||
* 触发升级检查入口。Java 侧直接调用:
|
||||
*
|
||||
* StoreVersionUpgrader.checkUpdate(activity)
|
||||
*
|
||||
* 已在 App.onCreate 中完成 Network.init,调用前无需额外初始化。
|
||||
*/
|
||||
@JvmStatic
|
||||
fun checkUpdate(activity: Activity?) {
|
||||
if (activity == null || activity.isFinishing()) return
|
||||
val appContext = activity.applicationContext
|
||||
|
||||
// 1) 拼装请求字段:与 AsApiRequester.buildInitReq 同源(设备标识、MAC、渠道、控制类型)
|
||||
// 注意:DeviceIdentityInitManager.getCachedOrInit 首次会同步走 AIDL,冷启动首点可能略慢;
|
||||
// 正常流程下走缓存分支,几乎瞬时返回。
|
||||
val macPair: Array<String> = runCatching { MacUtils.getDeviceMacPair(appContext) }
|
||||
.getOrDefault(arrayOf("", ""))
|
||||
val ethMac = macPair.getOrNull(0).orEmpty()
|
||||
val wifiMac = macPair.getOrNull(1).orEmpty()
|
||||
|
||||
val identity: DeviceIdentityInitManager.Result? =
|
||||
try {
|
||||
DeviceIdentityInitManager.getCachedOrInit(appContext)
|
||||
} catch (e: Throwable) {
|
||||
LogUtils.w(TAG, "getCachedOrInit failed: ${e.message}")
|
||||
null
|
||||
}
|
||||
|
||||
val cfg = AppConfiguration.getInstance()
|
||||
val touch = try { cfg.isTouchMode } catch (e: Throwable) { false }
|
||||
val orgCode = cfg.channelId.orEmpty()
|
||||
// 与 buildInitReq 完全一致:deviceType / controlType 同值;touch→"touch",否则→"remote"
|
||||
val deviceType = if (touch) "touch" else "remote"
|
||||
|
||||
val req = StoreVersionReq(
|
||||
mac = ethMac,
|
||||
wifiMac = wifiMac,
|
||||
deviceSn = identity?.deviceSn.orEmpty(),
|
||||
deviceUid = identity?.deviceUid.orEmpty(),
|
||||
orgCode = orgCode,
|
||||
deviceType = deviceType,
|
||||
controlType = deviceType
|
||||
)
|
||||
|
||||
// 2) 通过 DSL 发起请求。用 app() 而非 ui(lifecycleScope):手动点击是单次过程级动作,
|
||||
// 不需要把 Java 侧的 Activity 桥接到 Kotlin 的 lifecycleScope;同时也避免 Java
|
||||
// 侧接入 lifecycleScope 的样板代码。
|
||||
ApiRequesters.app()
|
||||
.from(StoreVersionApi::class.java)
|
||||
.action { api -> api.query(req) }
|
||||
.map<StoreVersionDto>()
|
||||
.onSuccess { dto -> handleSuccess(activity, touch, dto) }
|
||||
.onFailure { err -> showFailureToast(activity, err) }
|
||||
.launch()
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理成功响应。版本信息完整 + URL 非空 + 远程版本高于本地时才走 [UpgradeHelper.upgrade];
|
||||
* 任一条件不满足则提示"已是最新版本",避免出现空白弹框或无意义升级。
|
||||
*/
|
||||
private fun handleSuccess(activity: Activity, touch: Boolean, dto: StoreVersionDto) {
|
||||
if (activity.isFinishing) return
|
||||
val v: StoreVersionInfoDto = dto.version ?: run {
|
||||
ToastUtils.showLongBottomToast(MSG_UP_TO_DATE)
|
||||
return
|
||||
}
|
||||
val verCode = v.verCode.orEmpty()
|
||||
if (verCode.isEmpty()) {
|
||||
ToastUtils.showLongBottomToast(MSG_UP_TO_DATE)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isRemoteVersionHigher(AppUtils.getVersionCode(), verCode)) {
|
||||
ToastUtils.showLongBottomToast(MSG_UP_TO_DATE)
|
||||
return
|
||||
}
|
||||
|
||||
val url = pickUrl(touch, v)
|
||||
if (url.isEmpty()) {
|
||||
ToastUtils.showLongBottomToast(MSG_UP_TO_DATE)
|
||||
return
|
||||
}
|
||||
|
||||
val isForce = isCompulsory(v.compulsory)
|
||||
activity.runOnUiThread {
|
||||
// lambda 在主线程队列中执行,可能晚于上方 isFinishing 检查,需再次校验
|
||||
if (activity.isFinishing || activity.isDestroyed) return@runOnUiThread
|
||||
UpgradeHelper.upgrade(activity, verCode, v.verName.orEmpty(), url, isForce, null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程版本号是否高于本地版本号。逻辑与 MainActivity.isRemoteVersionHigher 完全一致:
|
||||
* 任一为空 → false;转 Int 比较;异常兜底 → false。
|
||||
*
|
||||
* 暴露为 internal 便于单元测试覆盖所有分支。
|
||||
*/
|
||||
internal fun isRemoteVersionHigher(currentVersion: String?, remoteVersion: String?): Boolean {
|
||||
if (currentVersion.isNullOrEmpty() || remoteVersion.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
return try {
|
||||
currentVersion.toInt() < remoteVersion.toInt()
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(TAG, "version compare error: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误兜底:网络/HTTP/解析/业务错误统一转成可读消息并 toast。
|
||||
* 保留服务端 msg(如业务错误码说明)作为首选文案。
|
||||
*/
|
||||
private fun showFailureToast(activity: Activity, err: ApiError) {
|
||||
if (activity.isFinishing) return
|
||||
val msg = err.message.takeIf { !it.isNullOrEmpty() } ?: MSG_NETWORK_ERROR
|
||||
ToastUtils.showLongBottomToast(msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 选择策略:touch 模式优先 [StoreVersionInfoDto.touchUrl],空则回退 [keyUrl];
|
||||
* remote 模式始终取 [keyUrl]。版本信息缺失返回空串。
|
||||
*
|
||||
* 暴露为 internal 便于单元测试覆盖所有分支。
|
||||
*/
|
||||
internal fun pickUrl(touch: Boolean, v: StoreVersionInfoDto?): String {
|
||||
if (v == null) return ""
|
||||
return if (touch) {
|
||||
v.touchUrl.takeUnless { it.isNullOrEmpty() } ?: v.keyUrl.orEmpty()
|
||||
} else {
|
||||
v.keyUrl.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否强制升级。判定与 MainActivity.isCompulsory 完全一致:
|
||||
* "true" (大小写不敏感) 或 "1" → 强制;其余一律非强制。
|
||||
*
|
||||
* 暴露为 internal 便于单元测试。
|
||||
*/
|
||||
internal fun isCompulsory(compulsory: String?): Boolean {
|
||||
if (compulsory.isNullOrEmpty()) return false
|
||||
return "true".equals(compulsory, ignoreCase = true) || compulsory == "1"
|
||||
}
|
||||
}
|
||||
0
app/src/main/java/com/pukka/jiecheng/downapk/UpgradeHelper.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/downapk/UpgradeHelper.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/downapk/amber/DownApkAmberManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/downapk/amber/DownApkAmberManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/FilterActivity.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/FilterActivity.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/FilterFragment.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/FilterFragment.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/HeaderVisibilityHelper.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/HeaderVisibilityHelper.java
Executable file → Normal file
2
app/src/main/java/com/pukka/jiecheng/filter/adapter/ContentAdapter.java
Executable file → Normal file
2
app/src/main/java/com/pukka/jiecheng/filter/adapter/ContentAdapter.java
Executable file → Normal file
@@ -90,7 +90,7 @@ public class ContentAdapter extends RecyclerView.Adapter<ContentAdapter.ItemHold
|
||||
// itemHolder.id_tv_playtime.setText("");
|
||||
// }
|
||||
String info = ContentDetailUtils.getProgramInformation(contentInfoChooseList.get(position).getContenttype(),
|
||||
contentInfoChooseList.get(position).getEpisodecount());
|
||||
contentInfoChooseList.get(position).getEpisodeindex());
|
||||
itemHolder.mask.setVisibility(TextUtils.isEmpty(info) ? View.GONE : View.VISIBLE);
|
||||
itemHolder.id_tv_playtime.setText(info);
|
||||
|
||||
|
||||
0
app/src/main/java/com/pukka/jiecheng/filter/adapter/HeaderAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/adapter/HeaderAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/adapter/TopAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/adapter/TopAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/adapter/TopAdapter2.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/adapter/TopAdapter2.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/bean/TopBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/bean/TopBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/bean/TopListBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/filter/bean/TopListBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/history/CenterLinearLayoutManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/history/CenterLinearLayoutManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/history/HistoryActivity.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/history/HistoryActivity.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/history/HistoryItemAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/history/HistoryItemAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/history/HistoryTextBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/history/HistoryTextBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/history/HistoryTextPresenter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/history/HistoryTextPresenter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/HotelMyActivity.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/HotelMyActivity.java
Executable file → Normal file
269
app/src/main/java/com/pukka/jiecheng/index/IndexActivity.java
Executable file → Normal file
269
app/src/main/java/com/pukka/jiecheng/index/IndexActivity.java
Executable file → Normal file
@@ -1,6 +1,7 @@
|
||||
|
||||
package com.pukka.jiecheng.index;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
@@ -37,12 +38,14 @@ import com.pukka.jiecheng.index.bean.NavResideBean;
|
||||
import com.pukka.jiecheng.index.dialog.BackImgDialog;
|
||||
import com.pukka.jiecheng.index.dialog.FullimgDialog;
|
||||
import com.pukka.jiecheng.index.fragment.ContentFragment;
|
||||
import com.pukka.jiecheng.index.fragment.HomeMyFragment;
|
||||
import com.pukka.jiecheng.index.fragment.HotelMyFragment;
|
||||
import com.pukka.jiecheng.index.fragment.MyFragment;
|
||||
import com.pukka.jiecheng.index.fragment.ShortVideoFragment;
|
||||
import com.pukka.jiecheng.index.viewmodel.IndexViewModel;
|
||||
import com.pukka.jiecheng.index.viewmodel.NavViewModel;
|
||||
import com.pukka.jiecheng.index.viewmodel.TopViewModel;
|
||||
import com.pukka.jiecheng.login.WxMpLoginManager;
|
||||
import com.pukka.jiecheng.login.viewmodel.IsLoginViewModel;
|
||||
import com.pukka.jiecheng.newtv.NewTvListner;
|
||||
import com.pukka.jiecheng.newtv.NewTvUtils;
|
||||
@@ -54,7 +57,7 @@ import com.pukka.jiecheng.widget.HomeNavLinearLayoutManagerTv;
|
||||
import com.pukka.jiecheng.widget.TopView;
|
||||
import com.pukka.jiecheng.widget.VideoContainer;
|
||||
import com.pukka.jiecheng.widget.banner.Banner;
|
||||
import com.pukka.libcore.base.BuildConfig;
|
||||
import com.pukka.jiecheng.BuildConfig;
|
||||
import com.pukka.libcore.boxShared.BoxSharedParameters;
|
||||
import com.pukka.libcore.boxShared.BoxSharedParamsUtils;
|
||||
import com.pukka.libcore.threading.DispatchQueue;
|
||||
@@ -93,6 +96,8 @@ public class IndexActivity extends BaseActivity {
|
||||
|
||||
private MyFragment mMyFragment;
|
||||
private HotelMyFragment mHotelMyFragment;
|
||||
// 家用版(home flavor)专用"我的"Fragment:未登录态、隐藏 MAC
|
||||
private HomeMyFragment mHomeMyFragment;
|
||||
private ShortVideoFragment mShortVideoFragment;
|
||||
|
||||
private FilterFragment filterFragment;
|
||||
@@ -310,20 +315,80 @@ public class IndexActivity extends BaseActivity {
|
||||
return isContentLayoutReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为扫码版变体(as8Scan / asAScan)。
|
||||
* <p>判断依据:{@code BuildConfig.SCAN_VARIANT} 由 build.gradle 的 flavor 维度注入,
|
||||
* 扫码版自动置为 true。扫码版默认 tab 落「电影」+ 切 contentFragment;行业版默认 tab
|
||||
* 按 navName 包含"推荐"定位(接口 type 字段不可靠) + 动态切换对应 Fragment。</p>
|
||||
*/
|
||||
private boolean isScanVariant() {
|
||||
return BuildConfig.SCAN_VARIANT;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行业版定位默认「推荐」tab:接口下发的 type 字段不可靠(如 navList[0]=「我的」type=2),
|
||||
* 直接遍历 navList 按 navName 包含"推荐"来定位(与扫码版找「电影」的方式一致)。
|
||||
*
|
||||
* <p>找不到时兜底回退到第 0 个 tab(与之前行为兼容)。</p>
|
||||
*/
|
||||
private NavFocusBean findDefaultNavIndex(List<GetLayoutNavBean.DataBean> navList) {
|
||||
NavFocusBean result = new NavFocusBean();
|
||||
if (ListUtils.isEmpty(navList)) {
|
||||
result.setIndex(0);
|
||||
result.setIndexSub(-1);
|
||||
return result;
|
||||
}
|
||||
for (int i = 0; i < navList.size(); i++) {
|
||||
GetLayoutNavBean.DataBean group = navList.get(i);
|
||||
List<GetLayoutNavBean.DataBean.SubNavDataBean> subList = group.getNavList();
|
||||
if (ListUtils.isEmpty(subList)) continue;
|
||||
boolean isSingle = TextUtils.isEmpty(group.getNavColor())
|
||||
&& TextUtils.isEmpty(group.getCornerUrl())
|
||||
&& subList.size() == 1;
|
||||
if (isSingle) {
|
||||
String navName = subList.get(0).getNavname();
|
||||
if (navName != null && navName.contains("推荐")) {
|
||||
result.setIndex(i);
|
||||
result.setIndexSub(-1);
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
for (int j = 0; j < subList.size(); j++) {
|
||||
String navName = subList.get(j).getNavname();
|
||||
if (navName != null && navName.contains("推荐")) {
|
||||
result.setIndex(i);
|
||||
result.setIndexSub(j);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 兜底:找不到「筛选」回退到 0
|
||||
result.setIndex(0);
|
||||
result.setIndexSub(-1);
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean applyInitialNavFocus() {
|
||||
if (mRvNav.getChildCount() <= 0 || mRvNav.getLayoutManager() == null) {
|
||||
return false;
|
||||
}
|
||||
NavFocusBean firstFocusBean = mIndexNavAdapter.getFistFocusIndex(mediaId);
|
||||
View focusView = mRvNav.getLayoutManager().findViewByPosition(firstFocusBean.getIndex());
|
||||
// 默认落焦位置由 registerTopNavDataObserver 按渠道设置:扫码版→「电影」,行业版→「推荐」
|
||||
DefaultNavFocusBean defaultBean = mNavViewModel.defaultFocusIndex.getValue();
|
||||
if (defaultBean == null || defaultBean.getDefaultIndex() < 0) {
|
||||
return false;
|
||||
}
|
||||
int focusIndex = defaultBean.getDefaultIndex();
|
||||
int focusIndexSub = defaultBean.getDefaultIndexSub();
|
||||
View focusView = mRvNav.getLayoutManager().findViewByPosition(focusIndex);
|
||||
if (focusView == null) {
|
||||
mRvNav.getLayoutManager().scrollToPosition(firstFocusBean.getIndex());
|
||||
mRvNav.getLayoutManager().scrollToPosition(focusIndex);
|
||||
mRvNav.post(mFocusDispatcher::attemptFocus);
|
||||
return false;
|
||||
}
|
||||
RecyclerView recyclerView = focusView.findViewById(R.id.id_rv_sub_nav);
|
||||
if (recyclerView != null && recyclerView.getLayoutManager() != null) {
|
||||
View focusViewSub = recyclerView.getLayoutManager().findViewByPosition(firstFocusBean.getIndexSub());
|
||||
if (recyclerView != null && recyclerView.getLayoutManager() != null && focusIndexSub >= 0) {
|
||||
View focusViewSub = recyclerView.getLayoutManager().findViewByPosition(focusIndexSub);
|
||||
if (focusViewSub != null) {
|
||||
focusViewSub.requestFocus();
|
||||
mTopView.mIvLogo.setFocusable(false);
|
||||
@@ -437,6 +502,43 @@ public class IndexActivity extends BaseActivity {
|
||||
return false;
|
||||
}
|
||||
|
||||
private View findNavResideView() {
|
||||
if (mRvNav.getLayoutManager() == null || mNavViewModel.resideIndex.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
int resideIndex = mNavViewModel.resideIndex.getValue().getResideIndex();
|
||||
int resideSubIndex = mNavViewModel.resideIndex.getValue().getResideIndexSub();
|
||||
View focusView = mRvNav.getLayoutManager().findViewByPosition(resideIndex);
|
||||
if (focusView == null) {
|
||||
return null;
|
||||
}
|
||||
if (resideSubIndex != -1) {
|
||||
RecyclerView subNavRv = focusView.findViewById(R.id.id_rv_sub_nav);
|
||||
if (subNavRv != null && subNavRv.getLayoutManager() != null) {
|
||||
View sub = subNavRv.getLayoutManager().findViewByPosition(resideSubIndex);
|
||||
if (sub != null) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
return focusView;
|
||||
}
|
||||
|
||||
private boolean isViewInside(View child, View ancestor) {
|
||||
if (child == null || ancestor == null) {
|
||||
return false;
|
||||
}
|
||||
View current = child;
|
||||
while (current != null) {
|
||||
if (current == ancestor) {
|
||||
return true;
|
||||
}
|
||||
android.view.ViewParent vp = current.getParent();
|
||||
current = vp instanceof View ? (View) vp : null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -571,28 +673,62 @@ public class IndexActivity extends BaseActivity {
|
||||
LogUtils.d("mNavList.size() == " + mNavList.size());
|
||||
mIndexNavAdapter.setLayoutNavList(mNavList);
|
||||
isNavDataReady = true;
|
||||
NavFocusBean firstFocusBean = mIndexNavAdapter.getFistFocusIndex(mediaId);
|
||||
// 扫码版:默认选中「电影」;行业版:默认选中「推荐」(type 字段不清晰,按 navName 兜底)
|
||||
NavFocusBean defaultFocus;
|
||||
if (isScanVariant()) {
|
||||
defaultFocus = mIndexNavAdapter.getFistFocusIndex(mediaId);
|
||||
} else {
|
||||
defaultFocus = findDefaultNavIndex(mNavList);
|
||||
}
|
||||
DefaultNavFocusBean defaultBean = mNavViewModel.defaultFocusIndex.getValue();
|
||||
if (defaultBean != null && defaultBean.getDefaultIndex() < 0) {
|
||||
defaultBean.setDefaultIndex(firstFocusBean.getIndex());
|
||||
defaultBean.setDefaultIndexSub(firstFocusBean.getIndexSub());
|
||||
defaultBean.setDefaultIndex(defaultFocus.getIndex());
|
||||
defaultBean.setDefaultIndexSub(defaultFocus.getIndexSub());
|
||||
}
|
||||
if (mNavList.size() == 1 && mNavList.get(0).getNavList().size() == 1) {
|
||||
dealOnlyOneTabUi(mNavList);
|
||||
} else {
|
||||
mRvNav.setVisibility(View.VISIBLE);
|
||||
// 根据默认 Tab 的 navType 和 navId 切换到对应 Fragment 并加载内容
|
||||
// (优先「电影」tab,找不到回退到第 0 个 tab)
|
||||
switchToDefaultFragmentByNavList(mNavList, firstFocusBean);
|
||||
if (isScanVariant()) {
|
||||
// 扫码版:按 defaultFocus(指向「电影」)切到对应 Fragment
|
||||
switchToDefaultFragmentByNavList(mNavList, defaultFocus);
|
||||
} else {
|
||||
// 行业版:按 defaultFocus(指向「推荐」)切到对应 Fragment
|
||||
switchToDefaultFragmentByNavList(mNavList, defaultFocus);
|
||||
}
|
||||
}
|
||||
mRvNav.postDelayed(() -> mFocusDispatcher.request(FOCUS_REQUEST_INITIAL), 500);
|
||||
if (isFirstLoad == 1) {
|
||||
isFirstLoad = 0;
|
||||
replayColdStartVoiceCommands();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 冷启动语音命令回放:导航就绪、首帧渲染完成后触发。
|
||||
* <p>
|
||||
* 设计要点(DIP 解耦):IndexActivity 属于 UI 层,voice 模块属于业务层,
|
||||
* 为避免相互 import 产生循环依赖,这里用反射调用
|
||||
* {@code VoiceCommandService.getInstance(context).replayColdStartQueue()},
|
||||
* 方法签名固定、零性能影响(仅首帧触发一次)。
|
||||
* </p>
|
||||
*/
|
||||
private void replayColdStartVoiceCommands() {
|
||||
try {
|
||||
Class<?> svcClz = Class.forName(
|
||||
"com.pukka.jiecheng.voice.service.VoiceCommandService");
|
||||
java.lang.reflect.Method getInstance = svcClz.getMethod(
|
||||
"getInstance", Context.class);
|
||||
Object svc = getInstance.invoke(null, IndexActivity.this);
|
||||
java.lang.reflect.Method replay = svcClz.getMethod("replayColdStartQueue");
|
||||
replay.invoke(svc);
|
||||
} catch (Throwable t) {
|
||||
LogUtils.w("replayColdStartQueue failed: " + t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void dealOnlyOneTabUi(List<GetLayoutNavBean.DataBean> mNavList) {
|
||||
mRvNav.setVisibility(View.GONE);
|
||||
GetLayoutNavBean.DataBean.SubNavDataBean tempNavObj = mNavList.get(0).getNavList().get(0);
|
||||
@@ -653,9 +789,21 @@ public class IndexActivity extends BaseActivity {
|
||||
IndexAmberManager.navName = navName;
|
||||
LogUtils.d("default nav: name=" + navName + " navId=" + navId + " navType=" + navType);
|
||||
|
||||
if (navType == 6) {
|
||||
onNavTypeChanged(navType, i);
|
||||
} else {
|
||||
onTabSelected(i);
|
||||
}
|
||||
mFocusDispatcher.requestPosition(i, j);
|
||||
|
||||
if (navType == 2) {
|
||||
// 我的页面(电影不可能是该类型,保留以兼容后端特殊配置)
|
||||
if (BoxSharedParamsUtils.isJcHotel()) {
|
||||
// 家用版优先:home flavor 走专用 HomeMyFragment
|
||||
if (AppConfiguration.getInstance().isHomeEdition()) {
|
||||
if (null != mHomeMyFragment) {
|
||||
switchFragment(mHomeMyFragment);
|
||||
}
|
||||
} else if (BoxSharedParamsUtils.isJcHotel()) {
|
||||
switchFragment(mHotelMyFragment);
|
||||
} else if (mMyFragment != null) {
|
||||
mMyFragment.navId = mCurrentNavId;
|
||||
@@ -663,7 +811,6 @@ public class IndexActivity extends BaseActivity {
|
||||
}
|
||||
} else if (navType == 6) {
|
||||
// 筛选页
|
||||
onNavTypeChanged(navType, i);
|
||||
switchFragment(filterFragment);
|
||||
filterFragment.initPos();
|
||||
} else if (navType == 5 || navType == 7) {
|
||||
@@ -723,11 +870,22 @@ public class IndexActivity extends BaseActivity {
|
||||
ft.commit();
|
||||
filterFragment = FilterFragment.newInstance(true);
|
||||
currentFragment = mContentFragment;
|
||||
if (BoxSharedParamsUtils.isJcHotel()) {
|
||||
// 家用版优先:home flavor 走专用 HomeMyFragment(未登录态、隐藏 MAC)
|
||||
if (AppConfiguration.getInstance().isHomeEdition()) {
|
||||
mHomeMyFragment = HomeMyFragment.newInstance();
|
||||
} else if (BoxSharedParamsUtils.isJcHotel()) {
|
||||
mHotelMyFragment = HotelMyFragment.newInstance();
|
||||
} else {
|
||||
mMyFragment = MyFragment.newInstance();
|
||||
}
|
||||
// 家用版:监听扫码登录成功事件,刷新"我的"Tab 真实状态
|
||||
if (AppConfiguration.getInstance().isHomeEdition()) {
|
||||
WxMpLoginManager.INSTANCE.getLoginSuccessEvent().observe(this, success -> {
|
||||
if (Boolean.TRUE.equals(success) && null != mHomeMyFragment) {
|
||||
mHomeMyFragment.refreshUserState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mShortVideoFragment = ShortVideoFragment.newInstance();
|
||||
mBrowseFrameLayout = findViewById(R.id.id_nav_bfl);
|
||||
@@ -854,7 +1012,12 @@ public class IndexActivity extends BaseActivity {
|
||||
mediaId = mCurrentNavId;
|
||||
// 如果是我的页面,则切换到我的fragment上
|
||||
if (navType == 2) {
|
||||
if (BoxSharedParamsUtils.isJcHotel()) {
|
||||
// 家用版优先:home flavor 走专用 HomeMyFragment
|
||||
if (AppConfiguration.getInstance().isHomeEdition()) {
|
||||
if (null != mHomeMyFragment) {
|
||||
switchFragment(mHomeMyFragment);
|
||||
}
|
||||
} else if (BoxSharedParamsUtils.isJcHotel()) {
|
||||
switchFragment(mHotelMyFragment);
|
||||
} else {
|
||||
if (null != mMyFragment) {
|
||||
@@ -1038,24 +1201,34 @@ public class IndexActivity extends BaseActivity {
|
||||
mShortVideoFragment.setAdapterDirection(4);
|
||||
}
|
||||
}
|
||||
// BOTTOM 模板列表失去焦点,导航列表获取焦点
|
||||
// 模板的RecyclerView
|
||||
RecyclerView recyclerView = mContentFragment.getRvContent();
|
||||
if (recyclerView.getFocusedChild() != null && mRvNav.getFocusedChild() == null
|
||||
&& null != mRvNav.getLayoutManager() && null != mNavViewModel.resideIndex.getValue()) {
|
||||
if (mNavViewModel.resideIndex.getValue().getResideIndexSub() != -1) {
|
||||
View focusView = mRvNav.getLayoutManager()
|
||||
.findViewByPosition(mNavViewModel.resideIndex.getValue().getResideIndex());
|
||||
if (null != focusView) {
|
||||
RecyclerView subNavRv = focusView.findViewById(R.id.id_rv_sub_nav);
|
||||
if (null != subNavRv && null != subNavRv.getLayoutManager()) {
|
||||
return subNavRv.getLayoutManager()
|
||||
.findViewByPosition(mNavViewModel.resideIndex.getValue().getResideIndexSub());
|
||||
if (mRvNav.getFocusedChild() == null) {
|
||||
View navReside = findNavResideView();
|
||||
if (navReside != null) {
|
||||
ScrollView mySv = null;
|
||||
if (currentFragment == mMyFragment && mMyFragment != null) {
|
||||
mySv = mMyFragment.getScrollView();
|
||||
} else if (currentFragment == mHotelMyFragment && mHotelMyFragment != null) {
|
||||
mySv = mHotelMyFragment.getScrollView();
|
||||
}
|
||||
if (mySv != null) {
|
||||
if (!mySv.canScrollVertically(-1) && isViewInside(focused, mySv)) {
|
||||
return navReside;
|
||||
}
|
||||
}
|
||||
if (currentFragment == mHomeMyFragment && mHomeMyFragment != null) {
|
||||
View root = mHomeMyFragment.getView();
|
||||
if (root != null && !root.canScrollVertically(-1) && isViewInside(focused, root)) {
|
||||
return navReside;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mRvNav.getLayoutManager()
|
||||
.findViewByPosition(mNavViewModel.resideIndex.getValue().getResideIndex());
|
||||
}
|
||||
RecyclerView recyclerView = mContentFragment.getRvContent();
|
||||
if (recyclerView.getFocusedChild() != null && mRvNav.getFocusedChild() == null) {
|
||||
View navReside = findNavResideView();
|
||||
if (navReside != null) {
|
||||
return navReside;
|
||||
}
|
||||
}
|
||||
|
||||
if ((null != mNavViewModel.resideIndex.getValue() && null != mNavViewModel.myIndex.getValue())
|
||||
@@ -1067,20 +1240,11 @@ public class IndexActivity extends BaseActivity {
|
||||
.getMyIndexSub()
|
||||
|| mNavViewModel.resideIndex.getValue().getResideIndexSub() == mNavViewModel.myIndex.getValue()
|
||||
.getMyIndex())
|
||||
&& mRvNav.getFocusedChild() == null && null != mRvNav.getLayoutManager()) {
|
||||
if (mNavViewModel.resideIndex.getValue().getResideIndexSub() != -1) {
|
||||
View focusView = mRvNav.getLayoutManager()
|
||||
.findViewByPosition(mNavViewModel.resideIndex.getValue().getResideIndex());
|
||||
if (null != focusView) {
|
||||
RecyclerView subNavRv = focusView.findViewById(R.id.id_rv_sub_nav);
|
||||
if (null != subNavRv && null != subNavRv.getLayoutManager()) {
|
||||
return subNavRv.getLayoutManager()
|
||||
.findViewByPosition(mNavViewModel.resideIndex.getValue().getResideIndexSub());
|
||||
}
|
||||
}
|
||||
&& mRvNav.getFocusedChild() == null) {
|
||||
View navReside = findNavResideView();
|
||||
if (navReside != null) {
|
||||
return navReside;
|
||||
}
|
||||
return mRvNav.getLayoutManager()
|
||||
.findViewByPosition(mNavViewModel.resideIndex.getValue().getResideIndex());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -1105,6 +1269,14 @@ public class IndexActivity extends BaseActivity {
|
||||
transaction.hide(currentFragment).show(targetFragment).commit();
|
||||
}
|
||||
currentFragment = targetFragment;
|
||||
// 家用版:首页 Tab 切换走 hide/show,Fragment.onResume 不会被触发。
|
||||
// 切到"我的"时主动调一次 refreshUserState(),确保扫码登录/退出登录等异步态变更
|
||||
// 落到 UI 上。
|
||||
if (AppConfiguration.getInstance().isHomeEdition()
|
||||
&& targetFragment == mHomeMyFragment
|
||||
&& mHomeMyFragment != null) {
|
||||
mHomeMyFragment.refreshUserState();
|
||||
}
|
||||
}
|
||||
|
||||
private static void releasePlayer() {
|
||||
@@ -1149,6 +1321,15 @@ public class IndexActivity extends BaseActivity {
|
||||
if (currentFragment == filterFragment) {
|
||||
filterFragment.requestFocusFirst();
|
||||
return super.onKeyDown(keyCode, event);
|
||||
} else if (currentFragment == mMyFragment && mMyFragment != null) {
|
||||
mMyFragment.requestFocusFirst();
|
||||
return super.onKeyDown(keyCode, event);
|
||||
} else if (currentFragment == mHotelMyFragment && mHotelMyFragment != null) {
|
||||
mHotelMyFragment.requestFocusFirst();
|
||||
return super.onKeyDown(keyCode, event);
|
||||
} else if (currentFragment == mHomeMyFragment && mHomeMyFragment != null) {
|
||||
mHomeMyFragment.requestFocusFirst();
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
}
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == android.view.KeyEvent.KEYCODE_ENTER) {
|
||||
|
||||
0
app/src/main/java/com/pukka/jiecheng/index/IndexAmberManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/IndexAmberManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/IndexConstant.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/IndexConstant.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/D1ContentAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/D1ContentAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/D3ContentAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/D3ContentAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/D3NavAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/D3NavAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/D4ContentAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/D4ContentAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexContentAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexContentAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexGroupNavAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexGroupNavAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMyCollectAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMyCollectAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMyHistoryAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMyHistoryAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMySetMoreAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMySetMoreAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexNavAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/IndexNavAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/MatchListAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/MatchListAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/MatchListHorAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/MatchListHorAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/ShortVideoAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/ShortVideoAdapter.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/interfaces/OnNavidListener.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/interfaces/OnNavidListener.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/viewholder/GroupNavViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/viewholder/GroupNavViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/viewholder/NavViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/adapter/viewholder/NavViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/CornerMarkBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/CornerMarkBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/D1TypeBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/D1TypeBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/DefaultNavFocusBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/DefaultNavFocusBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/ItemTypeBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/ItemTypeBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/LogOutBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/LogOutBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/MyIndexBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/MyIndexBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/NavBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/NavBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/NavFocusBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/NavFocusBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/NavResideBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/bean/NavResideBean.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/dialog/BackImgDialog.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/dialog/BackImgDialog.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/dialog/BackRetentionDialog.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/dialog/BackRetentionDialog.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/dialog/FullimgDialog.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/dialog/FullimgDialog.java
Executable file → Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @file HomeLogoutConfirmDialog.kt
|
||||
* @description 家用版退出登录二次确认对话框:提示"是否退出登录?",确认/取消双按钮。
|
||||
* 样式对齐家用版 ActivitedDialog / BenefitRevokedDialog(FullScreenDialog
|
||||
* 全屏透明窗口 + 白底圆角卡片 + 焦点渐变按钮)。确认走 onConfirm 回调;
|
||||
* 取消、返回键、点击外部均只关闭对话框。默认焦点落在"取消",防止误触退出。
|
||||
* @author ainetv
|
||||
* @date 2026-09-17
|
||||
*/
|
||||
package com.pukka.jiecheng.index.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import com.pukka.jiecheng.R
|
||||
|
||||
class HomeLogoutConfirmDialog(
|
||||
activity: Activity,
|
||||
private val onConfirm: () -> Unit
|
||||
) : Dialog(activity, R.style.FullScreenDialog) {
|
||||
|
||||
init {
|
||||
setContentView(R.layout.dialog_home_logout_confirm)
|
||||
window?.apply {
|
||||
setLayout(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
setGravity(Gravity.CENTER)
|
||||
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
findViewById<View>(R.id.btnConfirm).setOnClickListener {
|
||||
dismiss()
|
||||
onConfirm()
|
||||
}
|
||||
findViewById<View>(R.id.btnCancel).setOnClickListener { dismiss() }
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
super.show()
|
||||
// 破坏性操作:默认焦点落在"取消",用户需主动右移确认
|
||||
findViewById<View>(R.id.btnCancel)?.requestFocus()
|
||||
}
|
||||
}
|
||||
0
app/src/main/java/com/pukka/jiecheng/index/dialog/UserExitDialog.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/dialog/UserExitDialog.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/fragment/ContentFragment.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/fragment/ContentFragment.java
Executable file → Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* @file HomeMyFragment.kt
|
||||
* @description 家用版(home flavor)"我的"Tab Fragment。第一阶段仅实现未登录态:
|
||||
* 显示头像占位图 + "微信扫码登录"文字,不显示 MAC 地址,点击账号区域启动 WxMpLoginActivity。
|
||||
* 第二阶段(不在本期范围):接入登录后刷新、退出登录、头像/昵称/会员到期日。
|
||||
* 下方按钮区(设置/设备信息/检测更新/关于我们)复用 include_hotel_my_buttons.xml,
|
||||
* 事件逻辑沿用 HotelMyFragment:跳转 MgRoute 对应页面 / Toast 提示 / 焦点态 TopStyleUtils。
|
||||
* @author ainetv
|
||||
* @date 2026-09-12
|
||||
*/
|
||||
package com.pukka.jiecheng.index.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchy
|
||||
import com.facebook.drawee.drawable.ScalingUtils
|
||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder
|
||||
import com.facebook.drawee.generic.RoundingParams
|
||||
import com.facebook.drawee.view.SimpleDraweeView
|
||||
import com.pukka.jiecheng.R
|
||||
import com.pukka.jiecheng.downapk.StoreVersionUpgrader.checkUpdate
|
||||
import com.pukka.jiecheng.index.IndexActivity
|
||||
import com.pukka.jiecheng.index.dialog.HomeLogoutConfirmDialog
|
||||
import com.pukka.jiecheng.login.HomeLogoutHandler
|
||||
import com.pukka.jiecheng.login.WxMpLoginActivity
|
||||
import com.pukka.jiecheng.login.WxMpLoginManager
|
||||
import com.pukka.jiecheng.login.store.HomeUserInfo
|
||||
import com.pukka.jiecheng.login.store.HomeUserStore
|
||||
import com.pukka.jiecheng.utils.MgRoute
|
||||
import com.pukka.jiecheng.utils.TopStyleUtils
|
||||
import com.pukka.libcore.utils.LogUtils
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 家用版"我的"Tab Fragment(未登录态)
|
||||
*
|
||||
* 与 HotelMyFragment 区别:
|
||||
* - 顶部账号区域整块作为点击热区,点击后跳转 WxMpLoginActivity(占位登录页)
|
||||
* - 第一阶段不显示 MAC 地址,第二阶段接入登录态后展示头像/昵称/会员到期日
|
||||
*
|
||||
* 视图约束:顶部账号区域 id 与酒店版对齐为 R.id.id_cl_user_left,
|
||||
* 让 fragment_home_my.xml 与 include_hotel_my_buttons.xml 共享同一锚点资源,
|
||||
* 下方按钮区(设置/设备信息/检测更新/关于我们)按预期布局。
|
||||
*/
|
||||
class HomeMyFragment : Fragment(), View.OnClickListener, View.OnFocusChangeListener {
|
||||
|
||||
// 顶部账号区域(整块作为点击热区;同时是 include_hotel_my_buttons.xml 内兄弟节点的左侧锚点)
|
||||
private var mClUserArea: ConstraintLayout? = null
|
||||
// 头像占位图
|
||||
private var mIvAvatar: SimpleDraweeView? = null
|
||||
// 登录提示文字
|
||||
private var mTvLoginHint: TextView? = null
|
||||
// 已登录态 UI
|
||||
private var mTvNickname: TextView? = null
|
||||
private var mTvExpire: TextView? = null
|
||||
private var mBtnLogout: Button? = null
|
||||
// 下方按钮区(与 HotelMyFragment 共用 ID)
|
||||
private var mIvSetting: RelativeLayout? = null // id_iv_3 设置
|
||||
private var mIvDevice: RelativeLayout? = null // id_iv_device_4 设备信息
|
||||
private var mIvUpdate: RelativeLayout? = null // id_iv_5 检测更新
|
||||
private var mIvAbout: RelativeLayout? = null // id_iv_6 关于我们
|
||||
// toTop 区(与 HotelMyFragment 共用 ID,来自 include index_floor_totop)
|
||||
private var mClToTop: ConstraintLayout? = null // id_cl_to_top 回到顶部
|
||||
private var mClSearch: ConstraintLayout? = null // id_bottom_cl_search 搜索
|
||||
|
||||
private var mContext: Context? = null
|
||||
|
||||
/**
|
||||
* 静态工厂方法,与 HotelMyFragment.newInstance() 保持一致。
|
||||
*/
|
||||
companion object {
|
||||
private const val TAG = "HomeMyFragment"
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(): HomeMyFragment = HomeMyFragment()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = inflater.inflate(R.layout.fragment_home_my, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
mContext = context
|
||||
initView(view)
|
||||
userBind()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化视图引用。顶部账号区域点击启动登录页;
|
||||
* 下方 4 个按钮(设置/设备信息/检测更新/关于我们)+ toTop 区(回到顶部/搜索)
|
||||
* 事件逻辑沿用 HotelMyFragment。
|
||||
*/
|
||||
private fun initView(view: View) {
|
||||
mClUserArea = view.findViewById(R.id.id_cl_user_left)
|
||||
mIvAvatar = view.findViewById(R.id.id_iv_home_avatar)
|
||||
applyAvatarHierarchy()
|
||||
mTvLoginHint = view.findViewById(R.id.id_tv_home_login_hint)
|
||||
userBind()
|
||||
mIvSetting = view.findViewById(R.id.id_iv_3)
|
||||
mIvDevice = view.findViewById(R.id.id_iv_device_4)
|
||||
mIvUpdate = view.findViewById(R.id.id_iv_5)
|
||||
mIvAbout = view.findViewById(R.id.id_iv_6)
|
||||
mClToTop = view.findViewById(R.id.id_cl_to_top)
|
||||
mClSearch = view.findViewById(R.id.id_bottom_cl_search)
|
||||
mIvSetting?.setOnClickListener(this)
|
||||
mIvDevice?.setOnClickListener(this)
|
||||
mIvUpdate?.setOnClickListener(this)
|
||||
mIvAbout?.setOnClickListener(this)
|
||||
mClToTop?.setOnClickListener(this)
|
||||
mClSearch?.setOnClickListener(this)
|
||||
mClUserArea?.setOnFocusChangeListener(this)
|
||||
mIvSetting?.setOnFocusChangeListener(this)
|
||||
mIvDevice?.setOnFocusChangeListener(this)
|
||||
mIvUpdate?.setOnFocusChangeListener(this)
|
||||
mIvAbout?.setOnFocusChangeListener(this)
|
||||
mClToTop?.setOnFocusChangeListener(this)
|
||||
mClSearch?.setOnFocusChangeListener(this)
|
||||
mClToTop?.visibility = View.GONE
|
||||
view.findViewById<View>(R.id.view_space)?.visibility = View.GONE
|
||||
// 已登录态 UI 绑定
|
||||
mTvNickname = view.findViewById(R.id.id_tv_home_nickname)
|
||||
mTvExpire = view.findViewById(R.id.id_tv_home_expire)
|
||||
mBtnLogout = view.findViewById(R.id.id_btn_home_logout)
|
||||
mBtnLogout?.setOnClickListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* 第一阶段 userBind:仅设置登录提示文字。
|
||||
* 第二阶段改名为 applyUserState:根据 HomeUserInfo 切换未登录/已登录态 UI。
|
||||
*/
|
||||
private fun userBind() {
|
||||
applyUserState(HomeUserStore.getUser())
|
||||
}
|
||||
|
||||
/**
|
||||
* 为头像 SimpleDraweeView 注入圆形剪裁的 GenericDraweeHierarchy。
|
||||
* placeholder 与网络图均用 CENTER_CROP 铺满控件(不自适应留白);
|
||||
* 外框白色描边由 layout XML 的 android:background=@drawable/bg_avatar_white_border 提供。
|
||||
* 注意:此函数会整体替换 hierarchy,XML 上的 app:scaleType 会被覆盖,缩放类型以此处为准。 */
|
||||
private fun applyAvatarHierarchy() {
|
||||
val avatar = mIvAvatar ?: return
|
||||
val builder = GenericDraweeHierarchyBuilder(resources)
|
||||
.setPlaceholderImage(R.mipmap.my_avatar_default)
|
||||
.setPlaceholderImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
|
||||
.setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP)
|
||||
.setRoundingParams(RoundingParams.asCircle())
|
||||
avatar.hierarchy = builder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 HomeUserInfo 切换 UI:
|
||||
* - 未登录:头像占位 + 显示登录提示 + 隐藏 nickname/expire/logout + 启动扫码 Activity
|
||||
* - 已登录:头像用 Fresco 加载 + 显示昵称 + 显示到期日 + 显示退出按钮
|
||||
*/
|
||||
private fun applyUserState(info: HomeUserInfo) {
|
||||
if (info.isLoggedIn) {
|
||||
mIvAvatar?.setImageURI(info.avatarUrl)
|
||||
mTvLoginHint?.visibility = View.GONE
|
||||
mTvNickname?.visibility = View.VISIBLE
|
||||
mTvNickname?.text = info.nickname ?: ""
|
||||
mTvExpire?.visibility = View.VISIBLE
|
||||
if (info.expireTime.isNullOrBlank()) {
|
||||
mTvExpire?.setText(R.string.home_my_expire_empty)
|
||||
} else {
|
||||
mTvExpire?.text = getString(R.string.home_my_expire_format, info.expireTime)
|
||||
}
|
||||
mBtnLogout?.visibility = View.VISIBLE
|
||||
// 已登录态:点击头像区不再启动扫码
|
||||
mClUserArea?.setOnClickListener(null)
|
||||
} else {
|
||||
// 清空 URI:由 hierarchy 中的 placeholder(R.mipmap.my_avatar_default)渲染,
|
||||
// 经 Fresco 的圆角+描边 pipeline 输出。未走 setImageResource 是为了避免绕过 hierarchy。
|
||||
mIvAvatar?.setImageURI(null as String?)
|
||||
mTvLoginHint?.visibility = View.VISIBLE
|
||||
mTvNickname?.visibility = View.GONE
|
||||
mTvExpire?.visibility = View.GONE
|
||||
mBtnLogout?.visibility = View.GONE
|
||||
// 未登录态:点击启动扫码
|
||||
mClUserArea?.setOnClickListener {
|
||||
val ctx = context ?: return@setOnClickListener
|
||||
startActivity(Intent(ctx, WxMpLoginActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击事件统一分发:
|
||||
* - 设置 / 设备信息 / 关于我们:MgRoute 跳转对应页面(与 HotelMyFragment 一致)
|
||||
* - 检测更新:第一阶段沿用酒店版占逻辑(DownApkDialog 已注释),仅 Toast 提示
|
||||
* - 回到顶部:仅在宿主为 IndexActivity 时生效(与酒店版 cast 思路一致)
|
||||
* - 搜索:MgRoute.go2Search(2)
|
||||
* - 退出登录:先弹 HomeLogoutConfirmDialog 二次确认,确认后委托
|
||||
* HomeLogoutHandler.performDeviceLogout()(与心跳 revokeCardSecret 共用路径)
|
||||
*/
|
||||
override fun onClick(view: View) {
|
||||
val ctx = mContext ?: return
|
||||
when (view.id) {
|
||||
R.id.id_iv_3 -> MgRoute.go2Setting(ctx)
|
||||
R.id.id_iv_device_4 -> MgRoute.go2Device(ctx)
|
||||
R.id.id_iv_5 ->
|
||||
// 手动触发 App 升级检查(新网络栈 POST /api/jst/device/store-version)
|
||||
// 复用 UpgradeHelper.upgrade 弹框 + 下载 + 安装链路,无新弹框/无下载器改动
|
||||
checkUpdate(getActivity())
|
||||
R.id.id_iv_6 -> MgRoute.go2AboutMy(ctx)
|
||||
R.id.id_cl_to_top -> {
|
||||
// 回到顶部:仅当宿主是 IndexActivity 时调用,否则 no-op(与酒店版逻辑一致)
|
||||
(ctx as? IndexActivity)?.scrollToTop()
|
||||
}
|
||||
R.id.id_bottom_cl_search -> MgRoute.go2Search(ctx, 2)
|
||||
R.id.id_btn_home_logout -> {
|
||||
// 先弹二次确认框(样式对齐家用版 ActivitedDialog/BenefitRevokedDialog),
|
||||
// 确认后才执行退出;取消/返回键仅关闭弹框
|
||||
val act = activity ?: return
|
||||
HomeLogoutConfirmDialog(act) { performLogout() }.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行退出登录:委托给 [HomeLogoutHandler.performDeviceLogout],
|
||||
* 与心跳收到 revokeCardSecret 时走同一条清理路径(通知后端登出 + 清 HomeUserStore + 清 SDK 卡密缓存)。
|
||||
* 完成后通过 lifecycleScope 在主线程刷新 UI 状态。
|
||||
*/
|
||||
private fun performLogout() {
|
||||
HomeLogoutHandler.performDeviceLogout()
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
// 给协程一点时间启动网络请求,但不阻塞 UI:本地状态在 HomeLogoutHandler
|
||||
// 内部异步清理完成,这里只需刷新一次 UI 即可
|
||||
try {
|
||||
kotlinx.coroutines.delay(300)
|
||||
applyUserState(HomeUserStore.getUser())
|
||||
} catch (ce: CancellationException) {
|
||||
// 必须 rethrow:否则 view 销毁触发的 lifecycleScope 取消会被吞,
|
||||
// applyUserState 仍在 detached Fragment 上跑 getString / setText,必崩。
|
||||
throw ce
|
||||
} catch (e: Throwable) {
|
||||
LogUtils.e(TAG, "refresh user state after logout failed: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 焦点态变更:复用酒店版的 TopStyleUtils.topImageChange,保持 4 个按钮的焦点样式一致。
|
||||
*/
|
||||
override fun onFocusChange(view: View, hasFocus: Boolean) {
|
||||
TopStyleUtils.topImageChange(view, hasFocus)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
applyUserState(HomeUserStore.getUser())
|
||||
WxMpLoginManager.refreshUserState(viewLifecycleOwner.lifecycleScope) { info, hasUpdate ->
|
||||
if (hasUpdate) applyUserState(info)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activity 调用入口:触发一次完整 refresh(供 WxMpLoginActivity finish 后 / 首页 Tab 切换使用)。
|
||||
*
|
||||
* 注意:hide/show 模式下 view 一旦创建就常驻,但仍存在 view 销毁的窗口期——
|
||||
* Activity 因后台回收/重启后,fragment 实例被重新 attach 但 view 重建尚未完成,
|
||||
* 此时访问 viewLifecycleOwner 会抛 IllegalStateException(见 IndexActivity:1237 崩溃栈)。
|
||||
* 因此 view 未就绪时直接 return,onResume() 会自然跑一次同步回显+异步刷新,无 UI 滞后。
|
||||
*/
|
||||
fun refreshUserState() {
|
||||
// view 未创建/已销毁:跳过;下次 onResume() 会自然执行同样的刷新逻辑
|
||||
if (view == null || !isAdded) return
|
||||
applyUserState(HomeUserStore.getUser())
|
||||
WxMpLoginManager.refreshUserState(viewLifecycleOwner.lifecycleScope) { info, hasUpdate ->
|
||||
if (hasUpdate) applyUserState(info)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestFocusFirst() {
|
||||
LogUtils.d(TAG, "requestFocusFirst")
|
||||
val target = mClUserArea ?: return
|
||||
target.post {
|
||||
if (isAdded && view != null) {
|
||||
target.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
app/src/main/java/com/pukka/jiecheng/index/fragment/HotelMyFragment.java
Executable file → Normal file
24
app/src/main/java/com/pukka/jiecheng/index/fragment/HotelMyFragment.java
Executable file → Normal file
@@ -16,12 +16,11 @@ import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.pukka.jiecheng.R;
|
||||
import com.pukka.jiecheng.downapk.DownApkDialog;
|
||||
import com.pukka.jiecheng.downapk.StoreVersionUpgrader;
|
||||
import com.pukka.jiecheng.index.HotelMyActivity;
|
||||
import com.pukka.jiecheng.index.IndexActivity;
|
||||
import com.pukka.jiecheng.index.viewmodel.MyViewModel;
|
||||
import com.pukka.jiecheng.utils.MgRoute;
|
||||
import com.pukka.jiecheng.utils.ToastUtils;
|
||||
import com.pukka.jiecheng.utils.TopStyleUtils;
|
||||
import com.pukka.libcore.boxShared.MacUtils;
|
||||
import com.pukka.libcore.utils.LogUtils;
|
||||
@@ -128,14 +127,13 @@ public class HotelMyFragment extends Fragment implements View.OnClickListener ,
|
||||
if (view.getId() == R.id.id_bottom_cl_search) {
|
||||
MgRoute.go2Search(mContext, 2);
|
||||
} else if (view.getId() == R.id.id_iv_3) {
|
||||
ToastUtils.showLongToast("功能开发中");
|
||||
MgRoute.go2Setting(mContext);
|
||||
} else if (view.getId() == R.id.id_iv_device_4) {
|
||||
MgRoute.go2Device(mContext);
|
||||
} else if (view.getId() == R.id.id_iv_5) {
|
||||
//检测更新
|
||||
// TODO: 2025/6/10 APK更新
|
||||
// new DownApkDialog(getActivity()).initDown2();
|
||||
ToastUtils.showLongBottomToast("暂无新版本");
|
||||
// 手动触发 App 升级检查(新网络栈 POST /api/jst/device/store-version)
|
||||
// 复用 UpgradeHelper.upgrade 弹框 + 下载 + 安装链路,无新弹框/无下载器改动
|
||||
StoreVersionUpgrader.checkUpdate(getActivity());
|
||||
} else if (view.getId() == R.id.id_cl_to_top) {
|
||||
IndexActivity indexActivity = (IndexActivity) mContext;
|
||||
indexActivity.scrollToTop();
|
||||
@@ -148,6 +146,18 @@ public class HotelMyFragment extends Fragment implements View.OnClickListener ,
|
||||
return mSvContent;
|
||||
}
|
||||
|
||||
public void requestFocusFirst() {
|
||||
LogUtils.d("HotelMyFragment requestFocusFirst");
|
||||
if (mSvContent == null) {
|
||||
return;
|
||||
}
|
||||
mSvContent.post(() -> {
|
||||
if (mClLeftUser != null) {
|
||||
mClLeftUser.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
16
app/src/main/java/com/pukka/jiecheng/index/fragment/MyFragment.java
Executable file → Normal file
16
app/src/main/java/com/pukka/jiecheng/index/fragment/MyFragment.java
Executable file → Normal file
@@ -313,6 +313,22 @@ public class MyFragment extends Fragment implements View.OnClickListener, View.O
|
||||
return mSvContent;
|
||||
}
|
||||
|
||||
public void requestFocusFirst() {
|
||||
LogUtils.d("MyFragment requestFocusFirst");
|
||||
if (mSvContent == null) {
|
||||
return;
|
||||
}
|
||||
mSvContent.post(() -> {
|
||||
if (getView() == null) {
|
||||
return;
|
||||
}
|
||||
View firstTarget = getView().findViewById(R.id.id_iv_1);
|
||||
if (firstTarget != null) {
|
||||
firstTarget.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static HorizontalRecyclerViewTv getRvHistory() {
|
||||
return mRvHistory;
|
||||
}
|
||||
|
||||
0
app/src/main/java/com/pukka/jiecheng/index/fragment/ShortVideoFragment.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/fragment/ShortVideoFragment.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/manager/FloorOnBindViewHolderManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/manager/FloorOnBindViewHolderManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/manager/FloorOnCreateViewHolderManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/manager/FloorOnCreateViewHolderManager.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/A49ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/A49ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/A50ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/A50ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B10ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B10ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B11ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B11ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B12ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B12ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B13ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B13ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B14ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B14ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B15ViewHolder.java
Executable file → Normal file
0
app/src/main/java/com/pukka/jiecheng/index/viewholder/B15ViewHolder.java
Executable file → Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user