From b7b5518834501e063c672ba7f16cc6eff8e98b8d Mon Sep 17 00:00:00 2001 From: freeboss Date: Mon, 2 Nov 2026 19:25:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20dev-5.1=20=E5=88=86=E6=94=AF=E5=9F=BA?= =?UTF-8?q?=E7=BA=BF=EF=BC=88v1.3.35=20=E5=8F=91=E7=89=88=E5=90=8E?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=20+=205.1=20=E5=A3=B0=E9=81=93=E6=94=B9?= =?UTF-8?q?=E9=80=A0=E6=96=87=E6=A1=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 以 main(ccc65af) 为基线提交当前工作区代码 - 新增 5.1 声道改造文档两份 - .gitignore 补充 .vscode/、.screenshots/ --- .gitignore | 6 + 5.1声道接入成果总结.md | 146 ++++ 5.1声道改造-代码变更清单与复用指南.md | 585 +++++++++++++++ README.md | 0 app/.gitignore | 0 app/build.gradle | 147 ++-- app/key/jiecheng.jks | Bin app/key/keystore.properties | 0 .../JetsenxSdk-api-future-release-v1.23.aar | Bin app/libs/JetsenxSdk-api-release_v1.14.0.aar | Bin ...tsenxSdk-external-future-release-v1.23.aar | Bin .../JetsenxSdk-external-release_v1.14.0.aar | Bin app/libs/NewTVSDK-v4.0.3-app-lite.jar | Bin app/libs/arm64-v8a/libnewtvsdk.so | Bin app/libs/armeabi-v7a/libnewtvsdk.so | Bin .../exoplayer-ffmpeg-extension-2.19.1.aar | Bin app/proguard-rules.pro | 0 app/src/androidTest/androidmanifest.xml | 0 .../jiecheng/ExampleInstrumentedTest.java | 0 .../jiecheng/IndexNavType6TestActivity.java | 0 .../migu/jiecheng/IndexNavType6UiTest.java | 0 .../res/values/strings.xml | 0 .../res/values/strings.xml | 0 app/src/ashome/res/values/strings.xml | 4 + app/src/fxScan/res/values/strings.xml | 4 + app/src/home/res/values/strings.xml | 4 + app/src/main/AndroidManifest.xml | 96 ++- .../java/com/pukka/jiecheng/JcConfig.java | 0 .../java/com/pukka/jiecheng/MainActivity.java | 335 ++++++++- .../com/pukka/jiecheng/MyApplication.java | 64 +- .../java/com/pukka/jiecheng/TestActivity.java | 69 +- .../jiecheng/ThirdPartyEntryActivity.java | 500 ------------- .../jiecheng/aboutmy/AboutMyActivity.java | 0 .../jiecheng/aboutmy/LongPressSpanHelper.kt | 0 .../com/pukka/jiecheng/base/BaseActivity.java | 0 .../base/BaseOnItemClickListener.java | 0 .../base/BaseOnItemFocusChangeListener.java | 0 .../pukka/jiecheng/base/BasePresenter.java | 0 .../base/BasePresenterViewHolder.java | 0 .../jiecheng/base/BaseRecyclerAdapter.java | 0 .../jiecheng/base/BaseRecyclerViewHolder.java | 0 .../pukka/jiecheng/base/BaseViewHolder.java | 0 .../jiecheng/base/SystemConfigManager.java | 0 .../jiecheng/config/AppConfiguration.java | 25 +- .../pukka/jiecheng/device/DeviceActivity.kt | 45 +- .../device/DeviceIdentityInitManager.java | 0 .../device/DeviceIdentityProvider.java | 6 +- .../jiecheng/device/DeviceIdentityStore.java | 0 .../pukka/jiecheng/device/OaidProvider.java | 0 .../pukka/jiecheng/downapk/DownApkDialog.java | 0 .../jiecheng/downapk/DownloadApkManager.java | 0 .../jiecheng/downapk/StoreVersionUpgrader.kt | 181 +++++ .../pukka/jiecheng/downapk/UpgradeHelper.java | 0 .../downapk/amber/DownApkAmberManager.java | 0 .../pukka/jiecheng/filter/FilterActivity.java | 0 .../pukka/jiecheng/filter/FilterFragment.java | 0 .../filter/HeaderVisibilityHelper.java | 0 .../filter/adapter/ContentAdapter.java | 2 +- .../filter/adapter/HeaderAdapter.java | 0 .../jiecheng/filter/adapter/TopAdapter.java | 0 .../jiecheng/filter/adapter/TopAdapter2.java | 0 .../pukka/jiecheng/filter/bean/TopBean.java | 0 .../jiecheng/filter/bean/TopListBean.java | 0 .../history/CenterLinearLayoutManager.java | 0 .../jiecheng/history/HistoryActivity.java | 0 .../jiecheng/history/HistoryItemAdapter.java | 0 .../jiecheng/history/HistoryTextBean.java | 0 .../history/HistoryTextPresenter.java | 0 .../pukka/jiecheng/index/HotelMyActivity.java | 0 .../pukka/jiecheng/index/IndexActivity.java | 269 +++++-- .../jiecheng/index/IndexAmberManager.java | 0 .../pukka/jiecheng/index/IndexConstant.java | 0 .../index/adapter/D1ContentAdapter.java | 0 .../index/adapter/D3ContentAdapter.java | 0 .../jiecheng/index/adapter/D3NavAdapter.java | 0 .../index/adapter/D4ContentAdapter.java | 0 .../index/adapter/IndexContentAdapter.java | 0 .../index/adapter/IndexGroupNavAdapter.java | 0 .../index/adapter/IndexMyCollectAdapter.java | 0 .../index/adapter/IndexMyHistoryAdapter.java | 0 .../index/adapter/IndexMySetMoreAdapter.java | 0 .../index/adapter/IndexNavAdapter.java | 0 .../index/adapter/MatchListAdapter.java | 0 .../index/adapter/MatchListHorAdapter.java | 0 .../index/adapter/ShortVideoAdapter.java | 0 .../adapter/interfaces/OnNavidListener.java | 0 .../viewholder/GroupNavViewHolder.java | 0 .../adapter/viewholder/NavViewHolder.java | 0 .../jiecheng/index/bean/CornerMarkBean.java | 0 .../pukka/jiecheng/index/bean/D1TypeBean.java | 0 .../index/bean/DefaultNavFocusBean.java | 0 .../jiecheng/index/bean/ItemTypeBean.java | 0 .../pukka/jiecheng/index/bean/LogOutBean.java | 0 .../jiecheng/index/bean/MyIndexBean.java | 0 .../pukka/jiecheng/index/bean/NavBean.java | 0 .../jiecheng/index/bean/NavFocusBean.java | 0 .../jiecheng/index/bean/NavResideBean.java | 0 .../jiecheng/index/dialog/BackImgDialog.java | 0 .../index/dialog/BackRetentionDialog.java | 0 .../jiecheng/index/dialog/FullimgDialog.java | 0 .../index/dialog/HomeLogoutConfirmDialog.kt | 48 ++ .../jiecheng/index/dialog/UserExitDialog.java | 0 .../index/fragment/ContentFragment.java | 0 .../jiecheng/index/fragment/HomeMyFragment.kt | 297 ++++++++ .../index/fragment/HotelMyFragment.java | 24 +- .../jiecheng/index/fragment/MyFragment.java | 16 + .../index/fragment/ShortVideoFragment.java | 0 .../manager/FloorOnBindViewHolderManager.java | 0 .../FloorOnCreateViewHolderManager.java | 0 .../index/viewholder/A49ViewHolder.java | 0 .../index/viewholder/A50ViewHolder.java | 0 .../index/viewholder/B10ViewHolder.java | 0 .../index/viewholder/B11ViewHolder.java | 0 .../index/viewholder/B12ViewHolder.java | 0 .../index/viewholder/B13ViewHolder.java | 0 .../index/viewholder/B14ViewHolder.java | 0 .../index/viewholder/B15ViewHolder.java | 0 .../index/viewholder/B1ViewHolder.java | 0 .../index/viewholder/B2ViewHolder.java | 0 .../index/viewholder/B2_2ViewHolder.java | 0 .../index/viewholder/B2_3ViewHolder.java | 0 .../index/viewholder/B3ViewHolder.java | 0 .../index/viewholder/B4ViewHolder.java | 0 .../index/viewholder/B5ViewHolder.java | 0 .../index/viewholder/B5_2ViewHolder.java | 0 .../index/viewholder/B6ViewHolder.java | 0 .../index/viewholder/B7ViewHolder.java | 0 .../index/viewholder/B8ViewHolder.java | 0 .../index/viewholder/B9ViewHolder.java | 0 .../viewholder/BaseBannerViewHolder.java | 0 .../viewholder/BaseExVideoViewHolder.java | 0 .../index/viewholder/BaseTitleViewHolder.java | 0 .../index/viewholder/BaseViewHolder.java | 0 .../index/viewholder/C1ViewHolder.java | 0 .../index/viewholder/C2ViewHolder.java | 0 .../index/viewholder/C3ViewHolder.java | 0 .../index/viewholder/C4ViewHolder.java | 0 .../index/viewholder/C5ViewHolder.java | 0 .../index/viewholder/C6ViewHolder.java | 0 .../index/viewholder/C6_1ViewHolder.java | 0 .../index/viewholder/C7ViewHolder.java | 0 .../index/viewholder/D1ViewHolder.java | 0 .../index/viewholder/D2ViewHolder.java | 0 .../index/viewholder/D2_1ViewHolder.java | 0 .../index/viewholder/D3ViewHolder.java | 0 .../index/viewholder/D4ViewHolder.java | 0 .../index/viewholder/JC1ViewHolder.java | 0 .../index/viewholder/JC32ViewHolder.java | 0 .../index/viewholder/ToTopViewHolder.java | 0 .../index/viewmodel/ContentViewModel.java | 0 .../index/viewmodel/IndexViewModel.java | 0 .../jiecheng/index/viewmodel/MyViewModel.java | 0 .../index/viewmodel/NavViewModel.java | 0 .../index/viewmodel/ShortVideoViewModel.java | 0 .../index/viewmodel/TopViewModel.java | 0 .../com/pukka/jiecheng/list/ListActivity.java | 11 + .../pukka/jiecheng/list/ListAmberManager.java | 0 .../pukka/jiecheng/list/ListStyleUtils.java | 0 .../list/adapter/ListContentAdapter.java | 0 .../jiecheng/list/adapter/ListNavAdapter.java | 0 .../list/viewmodel/ListViewModel.java | 0 .../pukka/jiecheng/login/HomeLogoutHandler.kt | 73 ++ .../pukka/jiecheng/login/KeyboardAdapter.java | 0 .../pukka/jiecheng/login/LoginActivity.java | 0 .../jiecheng/login/WxMpLoginActivity.java | 120 +++ .../pukka/jiecheng/login/WxMpLoginManager.kt | 351 +++++++++ .../login/bean/GetCloudLoginBean.java | 0 .../login/bean/GetQueryUsableBean.java | 0 .../jiecheng/login/bean/LoginCodeBean.java | 0 .../jiecheng/login/bean/LoginLogonBean.java | 0 .../jiecheng/login/bean/QueryVipInfoBean.java | 0 .../jiecheng/login/store/HomeUserStore.kt | 103 +++ .../login/viewmodel/IsLoginViewModel.java | 0 .../pukka/jiecheng/monitor/HomeMonitor.java | 0 .../jiecheng/monitor/NetworkMonitor.java | 0 .../pukka/jiecheng/network/ApiRequesters.kt | 23 +- .../network/ApiRequesters_MapHelper.kt | 6 +- .../com/pukka/jiecheng/network/api/AsApi.kt | 50 +- .../jiecheng/network/api/BenefitOrderApi.kt | 42 ++ .../jiecheng/network/api/StoreVersionApi.kt | 28 + .../network/auth/HeaderInterceptor.kt | 3 +- .../network/auth/SignAuthenticator.kt | 160 +--- .../jiecheng/network/auth/SignRefresher.kt | 149 ++++ .../network/auth/SignRequestPolicy.kt | 37 +- .../pukka/jiecheng/network/auth/TokenStore.kt | 0 .../pukka/jiecheng/network/dto/ActivateDto.kt | 0 .../pukka/jiecheng/network/dto/ActivateReq.kt | 0 .../network/dto/BenefitOrderJsapiDto.kt | 96 +++ .../jiecheng/network/dto/BenefitOrderReq.kt | 0 .../network/dto/BenefitOrderRespDto.kt | 0 .../network/dto/BenefitOrderStatusDto.kt | 0 .../jiecheng/network/dto/CheckBenefitsDto.kt | 4 - .../jiecheng/network/dto/CheckBenefitsReq.kt | 0 .../pukka/jiecheng/network/dto/CheckDto.kt | 0 .../pukka/jiecheng/network/dto/CheckReq.kt | 0 .../jiecheng/network/dto/DeviceBenefitsDto.kt | 49 ++ .../pukka/jiecheng/network/dto/DeviceMeDto.kt | 24 + .../network/dto/HeartBeatActionDto.kt | 34 + .../jiecheng/network/dto/HeartBeatDto.kt | 12 +- .../jiecheng/network/dto/HeartBeatReq.kt | 0 .../com/pukka/jiecheng/network/dto/InitDto.kt | 0 .../com/pukka/jiecheng/network/dto/InitReq.kt | 6 - .../pukka/jiecheng/network/dto/LoginUrlDto.kt | 24 + .../pukka/jiecheng/network/dto/LogoutDto.kt | 13 + .../jiecheng/network/dto/RefreshSignDto.kt | 0 .../jiecheng/network/dto/RefreshSignReq.kt | 0 .../jiecheng/network/dto/StoreVersionDto.kt | 36 + .../jiecheng/network/dto/StoreVersionReq.kt | 24 + .../pukka/jiecheng/network/dto/TrackingDto.kt | 0 .../pukka/jiecheng/network/dto/TrackingReq.kt | 0 .../pukka/jiecheng/network/infra/ApiResult.kt | 0 .../jiecheng/network/infra/EnvProvider.kt | 0 .../jiecheng/network/infra/MoshiProvider.kt | 0 .../jiecheng/network/infra/NetResponse.kt | 0 .../pukka/jiecheng/network/infra/Network.kt | 0 .../jiecheng/network/infra/NetworkConfig.kt | 0 .../jiecheng/network/infra/NetworkEnv.kt | 0 .../jiecheng/network/infra/OkHttpProvider.kt | 7 +- .../jiecheng/network/infra/RetrofitFactory.kt | 0 .../pukka/jiecheng/newtv/NewTvListner.java | 0 .../pukka/jiecheng/newtv/NewTvManager.java | 0 .../com/pukka/jiecheng/newtv/NewTvUtils.java | 0 .../com/pukka/jiecheng/newtv/NewTvView.java | 0 .../pukka/jiecheng/newtv/SensorsPlayBean.java | 0 .../pukka/jiecheng/order/JsapiOrderHelper.kt | 160 ++++ .../pukka/jiecheng/order/JsapiOrderMonitor.kt | 511 +++++++++++++ .../order/JsapiOrderMonitorCallback.kt | 59 ++ .../pukka/jiecheng/order/OrderActivity.java | 643 ---------------- .../com/pukka/jiecheng/order/OrderActivity.kt | 228 ++++++ .../jiecheng/order/OrderAmberManager.java | 0 .../pukka/jiecheng/order/OrderStyleUtils.java | 0 .../order/adapter/OrderCenterAdapter.java | 312 ++++++-- .../order/adapter/OrderListAdapter.java | 179 ++--- .../adapter/viewholder/CenterViewHolder.java | 304 +++++--- .../viewholder/CenterVipViewHolder.java | 0 .../jiecheng/order/bean/ActivateBean.java | 0 .../order/bean/GetPayResultCycle.java | 0 .../jiecheng/order/bean/OrderListBean.java | 0 .../jiecheng/order/bean/PricingBean.java | 0 .../pukka/jiecheng/order/bean/QrCodeBean.java | 0 .../order/dialog/OrderCancelDialog.java | 0 .../jiecheng/order/dialog/OrderDialog.java | 0 .../order/fragment/OrderCenterFragment.java | 677 ++++++++++++----- .../order/fragment/OrderDuihuanFragment.java | 29 - .../order/fragment/OrderListFragment.java | 406 ++--------- .../order/fragment/OrderNetworkBridge.kt | 65 ++ .../order/fragment/OrderXufeiFragment.java | 0 .../order/viewmodel/OrderViewModel.java | 0 .../AgreementStoragePermissionGateway.java | 0 .../jiecheng/request/AsApiErrorCode.java | 6 +- .../jiecheng/request/AsApiErrorMapper.kt | 53 ++ .../pukka/jiecheng/request/AsApiRequester.kt | 373 +++++----- .../jiecheng/request/AsHeartBeatManager.java | 106 ++- .../jiecheng/search/Bean/HistoryBean.java | 0 .../pukka/jiecheng/search/SearchActivity.java | 26 + .../jiecheng/search/SearchAmberManger.java | 0 .../pukka/jiecheng/search/SearchConstant.java | 0 .../jiecheng/search/SearchRecyclerView.java | 0 .../pukka/jiecheng/search/T9KeyboardPop.java | 0 .../jiecheng/search/UnFocusListener.java | 0 .../search/adapter/ContentInfoAdapter.java | 0 .../search/adapter/DisplayTypeAdapter.java | 0 .../search/adapter/HistoryAdapter.java | 0 .../search/adapter/HotContentAdapter.java | 0 .../search/adapter/KeyboardAdapter.java | 0 .../search/adapter/LiveWidgetInfoAdapter.java | 0 .../search/adapter/SuggestAdapter.java | 0 .../search/adapter/T9KeyboardAdapter.java | 0 .../widget/SearchResultRecyclerView.java | 0 .../pukka/jiecheng/setting/SettingActivity.kt | 205 ++++++ .../pukka/jiecheng/setting/SettingAdapter.kt | 49 ++ .../com/pukka/jiecheng/setting/SettingItem.kt | 24 + .../pukka/jiecheng/utils/ActivitedDialog.java | 0 .../pukka/jiecheng/utils/ActivityMonitor.java | 5 +- .../jiecheng/utils/BenefitRevokedDialog.java | 141 ++++ .../jiecheng/utils/BuglyInitManager.java | 0 .../jiecheng/utils/BuildConfigUtils.java | 0 .../pukka/jiecheng/utils/ChannelManger.java | 13 +- .../jiecheng/utils/CloudKeyCodeUtils.java | 0 .../pukka/jiecheng/utils/ColorSpanUtils.java | 0 .../com/pukka/jiecheng/utils/ColorUtil.java | 0 .../jiecheng/utils/CommonLoadingDialog.java | 0 .../pukka/jiecheng/utils/CrashHandler.java | 0 .../pukka/jiecheng/utils/DataValueUtils.java | 0 .../utils/GridSpacingItemDecoration.java | 0 .../jiecheng/utils/IsLoginTimerUtil.java | 0 .../jiecheng/utils/ItemOnClickUtils.java | 0 .../pukka/jiecheng/utils/JetsenxSdkUtil.java | 372 ++++++++-- .../com/pukka/jiecheng/utils/MgRoute.java | 12 +- .../jiecheng/utils/NewRecordCollManager.java | 0 .../pukka/jiecheng/utils/NextPageManger.java | 0 .../pukka/jiecheng/utils/PageNumberUtil.java | 0 .../pukka/jiecheng/utils/PaginationUtil.java | 0 .../jiecheng/utils/PayResultTimerUtil.java | 0 .../com/pukka/jiecheng/utils/QRCodeUtil.java | 0 .../pukka/jiecheng/utils/QRCodeWriter.java | 0 .../com/pukka/jiecheng/utils/ResUtil.java | 0 .../com/pukka/jiecheng/utils/RsaSignUtil.java | 0 .../jiecheng/utils/Surround51Manager.java | 121 +++ .../com/pukka/jiecheng/utils/ToastUtils.java | 0 .../pukka/jiecheng/utils/TopStyleUtils.java | 0 .../com/pukka/jiecheng/utils/UiUtils.java | 0 .../pukka/jiecheng/utils/UpdateDialog.java | 0 .../jiecheng/utils/UpdateOtherDialog.java | 0 .../pukka/jiecheng/utils/UrlParamBuilder.java | 12 +- .../jiecheng/utils/VideoContainerHelper.java | 0 .../jiecheng/utils/XCrashInitManager.java | 0 .../jiecheng/video/VideoDetailActivity.java | 687 ++++++++++++++---- .../video/adapter/HistoryAdapter.java | 0 .../video/adapter/ProgramTableAdapter.java | 0 .../adapter/ProgramTablePageAdapter.java | 0 .../video/adapter/RecommendAdapter.java | 0 .../video/adapter/vh/HistoryViewHolder.java | 0 .../vh/ProgramTableEmptyViewHolder.java | 0 .../ProgramTableVarietyEmptyViewHolder.java | 0 .../vh/ProgramTableVarietyViewHolder.java | 0 .../adapter/vh/ProgramTableViewHolder.java | 0 .../video/adapter/vh/RecommendViewHolder.java | 0 .../video/amber/DetailAmberManager.java | 0 .../jiecheng/video/bean/ErrInfoBean.java | 0 .../video/bean/FindPlayrecordBean.java | 0 .../video/bean/JetsenxEventReportBean.java | 0 .../jiecheng/video/bean/PlayCodeBean.java | 0 .../video/bean/SendAddPlayRecordBean.java | 0 .../jiecheng/video/bean/TablePageBean.java | 0 .../pukka/jiecheng/video/bean/TestBean.java | 0 .../video/benefit/BenefitOrderCallback.java | 14 +- .../video/benefit/BenefitOrderController.kt | 114 ++- .../jiecheng/video/face/DetailInterface.java | 0 .../video/face/NavigationInterface.java | 0 .../video/face/PlayTheNextVideoInterface.java | 0 .../video/face/SelectionsInterface.java | 0 .../video/fragment/RecommendFragment.java | 0 .../video/manger/ResolutionManger.java | 3 +- .../jiecheng/video/manger/SpeedManger.java | 0 .../video/other/MgDividerItemDecoration.java | 0 .../video/other/SpacesItemDecoration.java | 0 .../jiecheng/video/popup/BlackFullDialog.java | 0 .../jiecheng/video/popup/InfoPopupWindow.java | 0 .../jiecheng/video/popup/LoadFullDialog.java | 0 .../video/popup/OnDemandEpisodesView.java | 0 .../jiecheng/video/popup/QrCodeDialog.java | 0 .../jiecheng/video/popup/RetentionDialog.java | 0 .../jiecheng/video/popup/TipsDialog.java | 0 .../video/utils/ContentDetailUtils.java | 1 + .../video/utils/HandlerLifecycleManager.java | 0 .../video/utils/KeyThrottleVerifier.java | 0 .../video/utils/PlayRecordContentManager.java | 0 .../video/utils/PlayRecordManager.java | 34 +- .../video/utils/PlayRecordShortManager.java | 0 .../video/utils/SelectedProgramsManager.java | 0 .../jiecheng/video/utils/TimeVideoUtils.java | 0 .../jiecheng/video/utils/ToastUtils.java | 0 .../utils/VideoActivityStackManager.java | 0 .../video/utils/VideoDetailKeyHandler.java | 0 .../video/utils/VideoDetailStatusManager.java | 28 +- .../video/utils/VideoFocusStateMachine.java | 0 .../pukka/jiecheng/video/utils/ViewUtils.java | 0 .../VideoDetailRecommendRecyclerView.java | 0 .../voice/adapter/SkyworthVoiceAdapter.java | 306 ++++++++ .../jiecheng/voice/guard/AppStateGuard.java | 8 + .../voice/guard/DefaultAppStateGuard.java | 46 ++ .../voice/log/VoiceCommandLogger.java | 55 ++ .../voice/parser/SkyworthVodParser.java | 169 +++++ .../jiecheng/voice/parser/VendorAParser.java | 189 +++++ .../jiecheng/voice/parser/VendorBParser.java | 12 + .../voice/parser/VoicePlatformParser.java | 8 + .../voice/protocol/VoiceCallbackResult.java | 70 ++ .../jiecheng/voice/protocol/VoiceCommand.java | 73 ++ .../voice/protocol/VoiceCommandType.java | 24 + .../jiecheng/voice/protocol/VoiceExtras.java | 45 ++ .../protocol/VoiceProtocolConstants.java | 23 + .../jiecheng/voice/queue/CommandQueue.java | 49 ++ .../voice/receiver/BootCompletedReceiver.java | 78 ++ .../voice/receiver/KeepAlivePingReceiver.java | 146 ++++ .../voice/receiver/VoiceCommandReceiver.java | 104 +++ .../voice/receiver/VoiceInternalReceiver.java | 82 +++ .../jiecheng/voice/router/BridgeRegistry.java | 26 + .../jiecheng/voice/router/CommandRouter.java | 143 ++++ .../voice/router/DefaultPageNavigator.java | 165 +++++ .../jiecheng/voice/router/PageNavigator.java | 18 + .../voice/router/PlaybackControlBridge.java | 12 + .../service/VoiceCommandCoordinator.java | 90 +++ .../voice/service/VoiceCommandService.java | 117 +++ .../voice/service/VoiceKeepAliveService.java | 103 +++ .../voice/throttle/CommandThrottle.java | 82 +++ .../voice/throttle/CommandThrottleClock.java | 5 + .../voice/window/VoiceOverlayHelper.java | 141 ++++ .../pukka/jiecheng/web/SimpleWebActivity.java | 0 .../widget/ActivationCodeInputView.kt | 0 .../widget/ActivationFocusRecyclerView.java | 0 .../jiecheng/widget/ActivationKeyItem.kt | 0 .../widget/ActivationKeypadAdapter.kt | 0 .../jiecheng/widget/CornerBadgeView.java | 0 .../jiecheng/widget/CustomerCodeView.java | 0 .../widget/D3ContentLinearLayoutManager.java | 0 .../widget/D3NavLinerLayoutManagerTv.java | 0 .../pukka/jiecheng/widget/D3RecyclerView.java | 0 .../jiecheng/widget/DeviceNotActivatedView.kt | 0 .../widget/DeviceUnregisteredView.java | 117 --- .../jiecheng/widget/FocusLinearLayout.java | 0 .../jiecheng/widget/FocusRecyclerView.java | 0 .../pukka/jiecheng/widget/FocusTextView.java | 0 .../widget/HomeNavLinearLayoutManagerTv.java | 0 .../jiecheng/widget/HorizontalGridViewPk.java | 0 .../widget/LinearSpacingItemDecoration.java | 0 .../jiecheng/widget/LocationCodeInputView.kt | 0 .../jiecheng/widget/MarqueeTextView.java | 0 .../pukka/jiecheng/widget/MgRecyclerView.java | 0 .../com/pukka/jiecheng/widget/QrCodeView.java | 0 .../pukka/jiecheng/widget/SettingItemView.kt | 78 ++ .../jiecheng/widget/SpaceItemDecoration.java | 0 .../pukka/jiecheng/widget/SquareTextView.java | 0 .../com/pukka/jiecheng/widget/TopView.java | 7 +- .../jiecheng/widget/TvXRecyclerView.java | 0 .../jiecheng/widget/UserAgreementView.java | 0 .../widget/VerticalRecyclerViewEx.java | 0 .../pukka/jiecheng/widget/VideoContainer.java | 0 .../pukka/jiecheng/widget/banner/Banner.java | 0 .../jiecheng/widget/banner/BannerConfig.java | 0 .../widget/banner/BannerScroller.java | 0 .../widget/banner/BannerViewPager.java | 0 .../widget/banner/OnBannerListener.java | 0 .../jiecheng/widget/banner/WeakHandler.java | 0 .../widget/banner/loader/BaseImageLoader.java | 0 .../banner/loader/FrescoImageLoader.java | 0 .../banner/loader/ImageLoaderInterface.java | 0 .../main/res/anim/activation_code_shake.xml | 0 app/src/main/res/anim/rotate_anim.xml | 0 .../color/activation_action_text_selector.xml | 0 .../color/activation_key_text_selector.xml | 0 .../color/activation_view_text_selector.xml | 0 .../res/drawable-hdpi/bg_about_submit.png | Bin .../drawable-hdpi/bg_about_submit_focus.png | Bin .../main/res/drawable-hdpi/bg_about_yhxy.png | Bin .../res/drawable-hdpi/bg_about_yhxy_focus.png | Bin .../main/res/drawable-hdpi/bg_about_ysxy.png | Bin .../res/drawable-hdpi/bg_about_ysxy_focus.png | Bin .../main/res/drawable-hdpi/bg_back_search.png | Bin .../drawable-hdpi/bg_back_search_focus.png | Bin .../main/res/drawable-hdpi/bg_back_top.png | Bin .../res/drawable-hdpi/bg_back_top_focus.png | Bin .../drawable-hdpi/bg_check_vr_btn_focus.png | Bin .../res/drawable-hdpi/bg_check_vr_btn_nor.png | Bin .../main/res/drawable-hdpi/bg_hotel_about.png | Bin .../res/drawable-hdpi/bg_hotel_deviceinfo.png | Bin .../res/drawable-hdpi/bg_hotel_setting.png | Bin .../res/drawable-hdpi/bg_hotel_update.png | Bin .../main/res/drawable-hdpi/bg_hotel_user.png | Bin .../main/res/drawable-hdpi/bg_short_mark.png | Bin .../drawable-hdpi/bg_short_playing_check.png | Bin .../main/res/drawable-hdpi/bg_short_vod.png | Bin .../res/drawable-hdpi/bg_short_vod_focus.png | Bin .../main/res/drawable-hdpi/bg_user_equit.png | Bin .../res/drawable-hdpi/bg_useragreement.png | Bin .../drawable-hdpi/bg_useragreement_submit.png | Bin .../bg_useragreement_submit_focus.png | Bin .../bg_useragreenmen_quit_focus.png | Bin .../bg_useragreenment_quit_unfocus.png | Bin app/src/main/res/drawable-hdpi/btn_1_foc.png | Bin app/src/main/res/drawable-hdpi/btn_1_nor.png | Bin .../btn_device_unactivit_focus.png | Bin app/src/main/res/drawable-hdpi/btn_focus.png | Bin app/src/main/res/drawable-hdpi/btn_nor.png | Bin .../main/res/drawable-hdpi/btn_progress.png | Bin .../btn_short_playing_submit.png | Bin app/src/main/res/drawable-hdpi/device_bg.png | Bin .../res/drawable-hdpi/device_check_update.png | Bin .../device_check_update_focus.png | Bin .../main/res/drawable-hdpi/device_info_bg.png | Bin app/src/main/res/drawable-hdpi/gamepad.png | Bin app/src/main/res/drawable-hdpi/help_bg.png | Bin .../res/drawable-hdpi/home_btn_down_foc.png | Bin .../res/drawable-hdpi/home_btn_down_nor.png | Bin .../main/res/drawable-hdpi/ic_check_vr_no.png | Bin .../res/drawable-hdpi/ic_check_vr_yes.png | Bin .../main/res/drawable-hdpi/ic_full_vr_tip.png | Bin .../res/drawable-hdpi/ic_short_playing.gif | Bin app/src/main/res/drawable-hdpi/ic_weixin.png | Bin .../main/res/drawable-hdpi/ic_zhifubao.png | Bin .../drawable-hdpi/icon_detail_back_normal.png | Bin .../res/drawable-hdpi/icon_error_prompt.png | Bin app/src/main/res/drawable-hdpi/index_bg.png | Bin .../drawable-hdpi/index_my_user_bg_focus.png | Bin .../res/drawable-hdpi/jst_logo_right.webp | Bin .../main/res/drawable-hdpi/keyboard_clear.png | Bin .../res/drawable-hdpi/keyboard_delete.png | Bin app/src/main/res/drawable-hdpi/list_bg.png | Bin .../res/drawable-hdpi/list_bg_tag_focus.png | Bin .../res/drawable-hdpi/list_bg_tag_unfocus.png | Bin .../main/res/drawable-hdpi/list_search_ic.png | Bin .../main/res/drawable-hdpi/my_order_bg.png | Bin .../res/drawable-hdpi/my_order_jilu_bg.webp | Bin .../drawable-hdpi/my_order_page_select.png | Bin .../drawable-hdpi/my_order_page_unselect.png | Bin app/src/main/res/drawable-hdpi/order_bg1.webp | Bin .../res/drawable-hdpi/order_bg_qr_code.png | Bin .../res/drawable-hdpi/order_btn_focus_bg.png | Bin .../drawable-hdpi/order_btn_focused_bg.png | Bin .../drawable-hdpi/order_btn_unfocus_bg.png | Bin .../order_center_keyboard_btn_focus.png | Bin .../order_center_keyboard_btn_unfocus.png | Bin .../order_center_vip_text_bg.png | Bin .../main/res/drawable-hdpi/order_pre_tip.png | Bin .../ordercenter_item_focus_bg.png | Bin .../ordercenter_item_unfoces_bg.png | Bin app/src/main/res/drawable-hdpi/progress.png | Bin .../res/drawable-hdpi/progress_loading.png | Bin .../res/drawable-hdpi/short_ok_game_tip.png | Bin .../main/res/drawable-hdpi/short_ok_tip.png | Bin .../res/drawable-hdpi/short_up_game_tip.png | Bin .../main/res/drawable-hdpi/short_up_tip.png | Bin .../res/drawable-hdpi/try_see_pay_tip.webp | Bin .../res/drawable-hdpi/update_dialog_bg.png | Bin .../drawable-hdpi/weilai_btn_error_focus.png | Bin .../weilai_btn_error_nofocus.png | Bin .../res/drawable-hdpi/weilai_error_bg.webp | Bin .../drawable-v24/ic_launcher_foreground.xml | 0 .../res/drawable-xhdpi/progress_loading.png | Bin .../main/res/drawable-xxhdpi/loading00.png | Bin .../main/res/drawable-xxhdpi/loading000.png | Bin .../main/res/drawable-xxhdpi/loading01.png | Bin .../main/res/drawable-xxhdpi/loading02.png | Bin .../main/res/drawable-xxhdpi/loading03.png | Bin .../main/res/drawable-xxhdpi/loading04.png | Bin .../main/res/drawable-xxhdpi/loading05.png | Bin .../main/res/drawable-xxhdpi/loading06.png | Bin .../main/res/drawable-xxhdpi/loading07.png | Bin .../main/res/drawable-xxhdpi/loading08.png | Bin .../main/res/drawable-xxhdpi/loading09.png | Bin .../main/res/drawable-xxhdpi/loading10.png | Bin .../main/res/drawable-xxhdpi/loading11.png | Bin .../main/res/drawable-xxhdpi/loading12.png | Bin .../main/res/drawable-xxhdpi/loading13.png | Bin .../main/res/drawable-xxhdpi/loading14.png | Bin .../main/res/drawable-xxhdpi/loading15.png | Bin .../main/res/drawable-xxhdpi/loading16.png | Bin .../main/res/drawable-xxhdpi/loading17.png | Bin .../main/res/drawable-xxhdpi/loading18.png | Bin .../main/res/drawable-xxhdpi/loading19.png | Bin .../main/res/drawable-xxhdpi/loading20.png | Bin .../main/res/drawable-xxhdpi/loading21.png | Bin .../main/res/drawable-xxhdpi/loading22.png | Bin .../res/drawable-xxhdpi/progress_loading.png | Bin .../drawable-xxhdpi/switch_keyboard_bg.png | Bin .../res/drawable-xxxhdpi/progress_loading.png | Bin .../drawable-xxxhdpi/switch_keyboard_bg.png | Bin .../main/res/drawable/background_search.xml | 0 .../main/res/drawable/background_splash.webp | Bin app/src/main/res/drawable/bg_2.webp | Bin .../res/drawable/bg_about_submit_selector.xml | 0 .../res/drawable/bg_about_yhxy_selector.xml | 0 .../res/drawable/bg_about_ysxy_selector.xml | 0 .../bg_activation_action_selector.xml | 0 .../res/drawable/bg_activation_code_box.xml | 0 .../drawable/bg_activation_device_panel.xml | 0 .../res/drawable/bg_activation_gradient.xml | 0 .../drawable/bg_activation_input_preview.xml | 0 .../drawable/bg_activation_key_selector.xml | 0 .../drawable/bg_activation_loading_panel.xml | 0 .../res/drawable/bg_avatar_white_border.xml | 12 + .../res/drawable/bg_bottom_round_corner.xml | 0 .../drawable/bg_bottom_white_round_corner.xml | 0 app/src/main/res/drawable/bg_btn_confirm.xml | 0 .../main/res/drawable/bg_btn_err_prompt.xml | 0 .../res/drawable/bg_btn_retention_prompt.xml | 0 .../res/drawable/bg_btn_trysee_open_focus.xml | 0 .../drawable/bg_btn_useragreenment_focus.xml | 0 .../bg_btn_useragreenment_selector.xml | 0 .../bg_btn_useragreenment_unfocus.xml | 0 .../res/drawable/bg_check_8k_selector.xml | 0 .../res/drawable/bg_check_vr_selector.xml | 0 .../main/res/drawable/bg_content_c7_focus.xml | 0 .../res/drawable/bg_content_c7_selector.xml | 0 .../main/res/drawable/bg_content_focus.xml | 0 .../main/res/drawable/bg_content_selector.xml | 0 .../main/res/drawable/bg_content_t_focus.xml | 0 .../res/drawable/bg_content_t_selector.xml | 0 .../main/res/drawable/bg_content_unfocus.xml | 0 .../drawable/bg_deman_table_page_selector.xml | 0 .../bg_demand_table_page_selector.xml | 0 .../bg_demandbg_table_page_selector.xml | 0 .../res/drawable/bg_detail_back_selector.xml | 0 .../drawable/bg_detail_broadcast_label.xml | 0 .../drawable/bg_detail_btn_order_selector.xml | 0 .../res/drawable/bg_detail_btn_selector.xml | 0 .../drawable/bg_detail_btn_video_selector.xml | 0 .../drawable/bg_detail_btn_vip_selector.xml | 0 .../res/drawable/bg_detail_def_selector.xml | 0 .../res/drawable/bg_detail_episodes_mask.xml | 0 .../drawable/bg_detail_episodes_pp_line.xml | 0 .../res/drawable/bg_detail_more_selector.xml | 0 .../bg_detail_program_table_item_selector.xml | 0 .../bg_detail_table_page_selector.xml | 0 .../res/drawable/bg_detail_try_see_order.xml | 0 .../res/drawable/bg_detail_try_see_order2.xml | 0 .../bg_detail_try_see_order_bottom.xml | 0 .../res/drawable/bg_detail_video_selector.xml | 0 .../drawable/bg_detail_video_selector2.xml | 0 .../main/res/drawable/bg_detail_vip_label.xml | 0 .../res/drawable/bg_device_activate_btn.xml | 0 .../res/drawable/bg_device_activate_input.xml | 0 .../res/drawable/bg_device_activate_panel.xml | 0 .../res/drawable/bg_device_check_selector.xml | 0 .../res/drawable/bg_dialog_activation.xml | 0 .../bg_filter_table_page_selector.xml | 0 .../res/drawable/bg_full_vr_player_tip.xml | 0 .../drawable/bg_full_window_bottom_mask.xml | 0 .../res/drawable/bg_full_window_top_mask.xml | 0 .../main/res/drawable/bg_fulldialogclose.png | Bin app/src/main/res/drawable/bg_gd.webp | Bin app/src/main/res/drawable/bg_gx_no_circle.png | Bin app/src/main/res/drawable/bg_his_selector.xml | 0 app/src/main/res/drawable/bg_his_unfocus.xml | 0 .../drawable/bg_history_clear_selector.xml | 0 .../res/drawable/bg_history_clear_unfocus.xml | 0 .../main/res/drawable/bg_history_selector.xml | 0 .../res/drawable/bg_history_selector1.xml | 0 .../res/drawable/bg_history_selector2.xml | 0 .../res/drawable/bg_history_selector3.xml | 0 .../main/res/drawable/bg_index_sub_nav.xml | 0 .../main/res/drawable/bg_item_match_focus.xml | 0 .../res/drawable/bg_list_tag_selector.xml | 0 app/src/main/res/drawable/bg_login.webp | Bin app/src/main/res/drawable/bg_login_bt.webp | Bin .../main/res/drawable/bg_login_nber_no.xml | 0 app/src/main/res/drawable/bg_login_nber_w.xml | 0 app/src/main/res/drawable/bg_login_top.png | Bin .../res/drawable/bg_login_top_selector.xml | 0 .../res/drawable/bg_main_agree_selector.xml | 0 .../res/drawable/bg_match_item_selector.xml | 0 app/src/main/res/drawable/bg_media_focus.xml | 0 .../res/drawable/bg_media_list_selector.xml | 0 .../main/res/drawable/bg_media_selector.xml | 0 .../main/res/drawable/bg_media_unfocus.xml | 0 .../res/drawable/bg_order_btn_selector.xml | 0 .../res/drawable/bg_order_center_selector.xml | 0 app/src/main/res/drawable/bg_order_focus.xml | 0 .../main/res/drawable/bg_order_focus_down.xml | 0 .../main/res/drawable/bg_order_unfocus.xml | 0 .../res/drawable/bg_order_xufei_selector.xml | 0 .../res/drawable/bg_ordergame_selector.xml | 0 app/src/main/res/drawable/bg_orderlist.xml | 0 app/src/main/res/drawable/bg_qr_code_card.xml | 0 .../res/drawable/bg_qr_code_dialog_card.xml | 0 .../res/drawable/bg_qr_code_image_rounded.xml | 0 .../drawable/bg_search_back_btn_selector.xml | 0 .../drawable/bg_search_history_selector.xml | 0 .../res/drawable/bg_setting_item_selector.xml | 14 + .../main/res/drawable/bg_shap1_selector.xml | 0 .../main/res/drawable/bg_shap1_unfocus.xml | 0 app/src/main/res/drawable/bg_shap_focus.xml | 0 .../main/res/drawable/bg_shap_selector.xml | 0 app/src/main/res/drawable/bg_shap_unfocus.xml | 0 .../res/drawable/bg_shap_useragreenment.xml | 0 .../res/drawable/bg_short_video_selector.xml | 0 app/src/main/res/drawable/bg_splash.webp | Bin .../res/drawable/bg_suggest_item_selector.xml | 0 app/src/main/res/drawable/bg_trysee_open.xml | 0 .../res/drawable/bg_update_tv_selector.xml | 0 .../res/drawable/bg_useragreen_selector.xml | 0 app/src/main/res/drawable/bg_video_btn.xml | 0 .../res/drawable/bg_video_key_prompt_tips.xml | 0 .../res/drawable/bg_vr_check_bottom_mask.xml | 0 .../main/res/drawable/bgc_content_focus.xml | 0 .../res/drawable/bgc_content_selector.xml | 0 .../res/drawable/btn_common_top_selector.xml | 0 .../res/drawable/btn_home_logout_selector.xml | 18 + .../res/drawable/btn_my_back_top_selector.xml | 0 .../drawable/btn_my_order_page_selector.xml | 0 .../res/drawable/btn_my_search_selector.xml | 0 .../res/drawable/btn_my_user_selector.xml | 0 app/src/main/res/drawable/btn_orderlist.xml | 0 .../main/res/drawable/btn_to_top_selector.xml | 0 .../res/drawable/btn_user_exit_selector.xml | 0 app/src/main/res/drawable/code_bg.webp | Bin .../drawable/common_matchtext_bg_focus.xml | 0 .../drawable/common_matchtext_bg_unfocus.xml | 0 .../common_matchtextlive_bg_unfocus.xml | 0 .../main/res/drawable/common_notice_bg.xml | 0 .../res/drawable/common_set_btn_bg_focus.xml | 0 .../res/drawable/common_text_bg_cir_focus.xml | 0 .../res/drawable/common_text_bg_focus.xml | 0 .../drawable/common_text_bg_round_focus.xml | 0 .../drawable/common_text_color_selector.xml | 0 .../res/drawable/common_top_btn_bg_focus.xml | 0 app/src/main/res/drawable/cursor_color.xml | 0 .../drawable/detail_program_page_divider.xml | 0 app/src/main/res/drawable/dialog_order_bg.png | Bin app/src/main/res/drawable/downapkbg.png | Bin app/src/main/res/drawable/filter_mask_bg.xml | 0 app/src/main/res/drawable/gameorder.webp | Bin app/src/main/res/drawable/hisroty_bg.webp | Bin .../main/res/drawable/history_del_focus.webp | Bin app/src/main/res/drawable/history_del_nor.png | Bin app/src/main/res/drawable/history_delete.png | Bin app/src/main/res/drawable/history_no_data.png | Bin .../res/drawable/history_text_bg_selector.xml | 0 .../res/drawable/history_top_bg_selector.xml | 0 .../res/drawable/hotcontent_item_selector.xml | 0 .../res/drawable/ic_detail_item_playing.gif | Bin .../res/drawable/ic_launcher_background.xml | 0 .../icon_common_top_search_selector.xml | 0 .../res/drawable/icon_team_logo_default.xml | 0 .../res/drawable/icon_to_top_selector.xml | 0 app/src/main/res/drawable/icon_video_play.png | Bin app/src/main/res/drawable/icon_vip.png | Bin .../res/drawable/index_my_history_mask_bg.xml | 0 .../res/drawable/index_my_seekbar_style.xml | 0 .../main/res/drawable/index_my_user_bg.xml | 0 .../drawable/index_nav_item_bg_selector.xml | 0 .../drawable/index_set_item_bg_selector.xml | 0 .../res/drawable/indicator_demand_tab.xml | 0 .../res/drawable/indicator_detail_tab.xml | 0 .../res/drawable/keyboard_item_selector.xml | 0 .../res/drawable/loading_player_animation.xml | 0 app/src/main/res/drawable/loadingicon.png | Bin app/src/main/res/drawable/login_bg.webp | Bin app/src/main/res/drawable/login_cancle_bg.png | Bin app/src/main/res/drawable/login_code_bg.png | Bin .../main/res/drawable/login_code_bg_focus.png | Bin .../res/drawable/login_code_bg_selector.xml | 0 .../drawable/login_codebtn_bg_selector.xml | 0 .../main/res/drawable/login_codebtn_focus.png | Bin .../main/res/drawable/login_codebtn_nor.png | Bin .../res/drawable/login_getcode_btn_bg.webp | Bin app/src/main/res/drawable/login_icon_bg.webp | Bin .../main/res/drawable/login_item_bg_focus.png | Bin .../main/res/drawable/login_item_bg_nor.png | Bin app/src/main/res/drawable/login_line_1.webp | Bin app/src/main/res/drawable/login_phone_bg.png | Bin .../res/drawable/login_phone_bg_focus.png | Bin .../res/drawable/login_phone_bg_selector.xml | 0 app/src/main/res/drawable/login_show_bg.xml | 0 .../main/res/drawable/login_tolin_btn_bg.png | Bin .../res/drawable/login_tologin_bg_focus.png | Bin .../drawable/login_tologin_bg_selector.xml | 0 .../drawable/logout_text_color_selector.xml | 4 + .../res/drawable/match_all_tv_selector.xml | 0 .../res/drawable/match_list_all_selector.xml | 0 .../res/drawable/match_list_all_unfocus.xml | 0 app/src/main/res/drawable/match_list_bg.xml | 0 .../main/res/drawable/match_list_focus.xml | 0 .../res/drawable/match_list_focus_sport.xml | 0 .../main/res/drawable/match_list_selector.xml | 0 .../drawable/match_list_selector_sport.xml | 0 .../res/drawable/matchlist_no_data_focus.xml | 0 .../drawable/matchlist_no_data_selector.xml | 0 app/src/main/res/drawable/mb.png | Bin app/src/main/res/drawable/no_data_img.png | Bin app/src/main/res/drawable/order_bg.webp | Bin .../order_center_keyboader_bg_focus.xml | 0 .../order_center_keyboard_bg_selector.xml | 0 .../drawable/order_center_xufei_bg_focus.xml | 0 .../order_center_xufei_bg_selector.xml | 0 .../order_center_xufei_bg_unfocus.xml | 0 .../drawable/order_keyboard_item_selector.xml | 0 .../drawable/ordercenter_item_bigfoces_bg.png | Bin .../main/res/drawable/progress_bar_custom.xml | 0 app/src/main/res/drawable/progress_circle.xml | 0 app/src/main/res/drawable/progressbar_end.xml | 0 .../main/res/drawable/progressbar_start.xml | 0 .../res/drawable/progressbar_update_h.xml | 0 .../res/drawable/progressbar_update_other.xml | 0 app/src/main/res/drawable/rect_no.png | Bin app/src/main/res/drawable/rect_ok.png | Bin app/src/main/res/drawable/saomakuang.png | Bin app/src/main/res/drawable/saosao.png | Bin .../drawable/search_keyboard_bg_selector.xml | 0 .../selector_bg_activate_page_btn.xml | 0 .../res/drawable/selector_btn_update_now.xml | 0 .../res/drawable/selector_txt_update_now.xml | 0 .../drawable/switch_keyboard_bg_selector.xml | 0 .../drawable/switch_keyboard_bg_unfocus.xml | 0 app/src/main/res/drawable/sxtitle.png | Bin app/src/main/res/drawable/tryseeoverbg.xml | 0 .../main/res/drawable/update_tv_selector.xml | 0 app/src/main/res/drawable/video_bg_img.xml | 0 .../main/res/drawable/weilai_btn_error.xml | 0 .../drawable/weilai_btn_error_textcolor.xml | 0 .../layout-land/device_not_activated_view.xml | 0 app/src/main/res/layout/activity_about_my.xml | 0 app/src/main/res/layout/activity_device.xml | 58 +- app/src/main/res/layout/activity_filter.xml | 0 .../main/res/layout/activity_filtermain.xml | 0 app/src/main/res/layout/activity_history.xml | 0 app/src/main/res/layout/activity_hotel_my.xml | 0 app/src/main/res/layout/activity_index.xml | 0 app/src/main/res/layout/activity_list.xml | 0 app/src/main/res/layout/activity_login.xml | 0 .../main/res/layout/activity_login_new.xml | 0 app/src/main/res/layout/activity_main.xml | 0 app/src/main/res/layout/activity_order.xml | 23 +- app/src/main/res/layout/activity_search.xml | 0 app/src/main/res/layout/activity_setting.xml | 50 ++ app/src/main/res/layout/activity_test.xml | 0 .../res/layout/activity_thirdparty_entry.xml | 80 -- .../main/res/layout/activity_video_detail.xml | 0 .../main/res/layout/activity_wx_mp_login.xml | 126 ++++ app/src/main/res/layout/banner.xml | 0 .../res/layout/device_not_activated_view.xml | 0 .../res/layout/dialog_activation_success.xml | 0 .../res/layout/dialog_benefit_revoked.xml | 55 ++ .../res/layout/dialog_home_logout_confirm.xml | 92 +++ app/src/main/res/layout/dialog_qr_code.xml | 0 .../main/res/layout/filter_header_layout.xml | 0 app/src/main/res/layout/fragment_content.xml | 0 app/src/main/res/layout/fragment_home_my.xml | 102 +++ app/src/main/res/layout/fragment_hotel_my.xml | 77 +- app/src/main/res/layout/fragment_my.xml | 0 .../main/res/layout/fragment_order_center.xml | 55 +- .../res/layout/fragment_order_duihuan.xml | 2 +- .../main/res/layout/fragment_orderlist.xml | 4 +- .../main/res/layout/fragment_orderxufei.xml | 0 .../main/res/layout/fragment_recommend.xml | 0 .../main/res/layout/fragment_short_video.xml | 0 .../res/layout/include_hotel_my_buttons.xml | 90 +++ .../res/layout/index_floor_player_error.xml | 0 .../res/layout/index_floor_player_order.xml | 0 .../layout/index_floor_player_progress.xml | 0 .../res/layout/index_floor_title_layout.xml | 0 app/src/main/res/layout/index_floor_totop.xml | 41 +- .../main/res/layout/item_activation_key.xml | 0 .../main/res/layout/item_d1_list_content.xml | 0 app/src/main/res/layout/item_d3_nav.xml | 0 .../main/res/layout/item_detail_history.xml | 0 .../layout/item_detail_program_table_1.xml | 0 .../layout/item_detail_program_table_2.xml | 0 .../item_detail_program_table_empty_1.xml | 0 .../item_detail_program_table_empty_2.xml | 0 .../main/res/layout/item_detail_recommend.xml | 0 .../res/layout/item_detail_tab_page_text.xml | 0 app/src/main/res/layout/item_device_info.xml | 0 .../res/layout/item_device_info_footer.xml | 0 app/src/main/res/layout/item_diaplay_type.xml | 0 app/src/main/res/layout/item_filter.xml | 0 app/src/main/res/layout/item_history.xml | 0 .../main/res/layout/item_history_text_nav.xml | 0 app/src/main/res/layout/item_hot_hcontent.xml | 0 .../main/res/layout/item_image_txt_hor.xml | 0 .../main/res/layout/item_index_floor_a49.xml | 0 .../main/res/layout/item_index_floor_a50.xml | 0 .../main/res/layout/item_index_floor_b1.xml | 0 .../main/res/layout/item_index_floor_b10.xml | 0 .../main/res/layout/item_index_floor_b11.xml | 0 .../main/res/layout/item_index_floor_b12.xml | 0 .../main/res/layout/item_index_floor_b13.xml | 0 .../main/res/layout/item_index_floor_b14.xml | 0 .../main/res/layout/item_index_floor_b15.xml | 0 .../main/res/layout/item_index_floor_b2.xml | 0 .../main/res/layout/item_index_floor_b2_2.xml | 0 .../main/res/layout/item_index_floor_b2_3.xml | 0 .../main/res/layout/item_index_floor_b2_4.xml | 0 .../main/res/layout/item_index_floor_b3.xml | 0 .../main/res/layout/item_index_floor_b4.xml | 0 .../main/res/layout/item_index_floor_b5.xml | 0 .../main/res/layout/item_index_floor_b5_2.xml | 0 .../main/res/layout/item_index_floor_b6.xml | 0 .../main/res/layout/item_index_floor_b7.xml | 0 .../main/res/layout/item_index_floor_b8.xml | 0 .../main/res/layout/item_index_floor_b9.xml | 0 .../main/res/layout/item_index_floor_c1.xml | 0 .../main/res/layout/item_index_floor_c2.xml | 0 .../main/res/layout/item_index_floor_c3.xml | 0 .../main/res/layout/item_index_floor_c4.xml | 0 .../main/res/layout/item_index_floor_c5.xml | 0 .../main/res/layout/item_index_floor_c6.xml | 0 .../main/res/layout/item_index_floor_c6_1.xml | 0 .../main/res/layout/item_index_floor_c7.xml | 0 .../main/res/layout/item_index_floor_d1.xml | 0 .../main/res/layout/item_index_floor_d2.xml | 0 .../main/res/layout/item_index_floor_d2_1.xml | 0 .../main/res/layout/item_index_floor_d3.xml | 0 .../main/res/layout/item_index_floor_d4.xml | 0 .../main/res/layout/item_index_floor_jc1.xml | 0 .../main/res/layout/item_index_floor_jc32.xml | 0 .../layout/item_index_floor_openorclose.xml | 0 .../res/layout/item_index_floor_totop.xml | 0 .../res/layout/item_index_group_item_nav.xml | 0 .../main/res/layout/item_index_my_collect.xml | 0 .../main/res/layout/item_index_my_history.xml | 0 .../res/layout/item_index_my_set_more.xml | 0 app/src/main/res/layout/item_index_nav.xml | 0 .../main/res/layout/item_index_nav_group.xml | 0 app/src/main/res/layout/item_keyboard.xml | 0 .../main/res/layout/item_keyboard_button.xml | 0 app/src/main/res/layout/item_list_content.xml | 0 app/src/main/res/layout/item_list_nav.xml | 0 app/src/main/res/layout/item_live_info.xml | 0 app/src/main/res/layout/item_match_list.xml | 0 .../main/res/layout/item_match_list_hor.xml | 0 app/src/main/res/layout/item_media_list.xml | 0 app/src/main/res/layout/item_order_center.xml | 11 +- .../main/res/layout/item_order_center_vip.xml | 4 +- .../main/res/layout/item_order_keyboard.xml | 0 app/src/main/res/layout/item_orderlist.xml | 0 .../main/res/layout/item_player_history.xml | 0 .../main/res/layout/item_setting_switch.xml | 23 + .../res/layout/item_setting_switch_inner.xml | 54 ++ app/src/main/res/layout/item_short_vod.xml | 0 app/src/main/res/layout/item_suggest.xml | 0 app/src/main/res/layout/item_t9keyboard.xml | 0 .../layout_activation_loading_small.xml | 0 app/src/main/res/layout/layout_badge_view.xml | 0 .../main/res/layout/layout_common_loading.xml | 0 .../layout/layout_detail_info_popupwindow.xml | 0 ...ut_detail_ondemandepisodes_popupwindow.xml | 0 .../layout/layout_detail_video_full_black.xml | 0 .../layout_detail_video_full_loading.xml | 0 .../layout/layout_detail_video_status_err.xml | 8 +- .../layout_detail_video_status_loading.xml | 0 ...ayout_detail_video_status_noorder_host.xml | 0 .../layout_detail_video_status_try_see.xml | 0 ...layout_detail_video_status_try_see_end.xml | 0 .../main/res/layout/layout_dialog_backimg.xml | 0 .../main/res/layout/layout_dialog_downapk.xml | 0 .../main/res/layout/layout_dialog_fullimg.xml | 0 .../main/res/layout/layout_dialog_order.xml | 0 .../res/layout/layout_dialog_order_cancel.xml | 0 .../res/layout/layout_dialog_tryseeover.xml | 0 .../res/layout/layout_dialog_user_exit.xml | 0 .../main/res/layout/layout_error_prompt.xml | 0 .../main/res/layout/layout_login_new_qr.xml | 0 .../main/res/layout/layout_retention_back.xml | 0 .../res/layout/layout_retention_prompt.xml | 0 .../layout_video_play_key_prompt_tips.xml | 0 app/src/main/res/layout/newtv_view.xml | 0 app/src/main/res/layout/pop_t9keyboard.xml | 0 app/src/main/res/layout/top_view.xml | 0 app/src/main/res/layout/update_dialog.xml | 0 .../main/res/layout/update_other_dialog.xml | 0 .../main/res/layout/useragreenment_view.xml | 0 .../res/layout/view_activation_code_input.xml | 0 .../main/res/layout/view_customer_code.xml | 0 .../res/layout/view_device_unregistered.xml | 39 - .../res/layout/view_location_code_input.xml | 0 app/src/main/res/layout/view_no_data.xml | 0 app/src/main/res/layout/view_qr_code.xml | 0 .../main/res/layout/view_user_agreement.xml | 0 .../main/res/layout/web_activity_simple.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../main/res/mipmap-hdpi/all_game_foc.webp | Bin app/src/main/res/mipmap-hdpi/all_game_nor.png | Bin .../mipmap-hdpi/bg_detail_video_try_see.png | Bin .../main/res/mipmap-hdpi/bg_history_foc1.webp | Bin .../main/res/mipmap-hdpi/bg_history_foc2.webp | Bin .../main/res/mipmap-hdpi/bg_history_foc3.webp | Bin .../main/res/mipmap-hdpi/bg_history_foc4.png | Bin .../main/res/mipmap-hdpi/bg_history_nor1.png | Bin .../main/res/mipmap-hdpi/bg_history_nor2.png | Bin .../main/res/mipmap-hdpi/bg_history_nor3.png | Bin .../main/res/mipmap-hdpi/bg_history_nor4.png | Bin .../res/mipmap-hdpi/bg_index_my_collect.png | Bin .../res/mipmap-hdpi/bg_index_my_history.png | Bin .../res/mipmap-hdpi/bg_item_match_list.png | Bin .../main/res/mipmap-hdpi/bg_match_list.png | Bin app/src/main/res/mipmap-hdpi/bg_suggest.png | Bin .../main/res/mipmap-hdpi/btn_episode_sec.png | Bin .../mipmap-hdpi/btn_episode_sec_variety.png | Bin app/src/main/res/mipmap-hdpi/calendar1.png | Bin app/src/main/res/mipmap-hdpi/calendar_nor.png | Bin .../main/res/mipmap-hdpi/common_notice.png | Bin app/src/main/res/mipmap-hdpi/head_clear.png | Bin .../main/res/mipmap-hdpi/head_clear_focus.png | Bin app/src/main/res/mipmap-hdpi/head_delete.png | Bin .../res/mipmap-hdpi/head_delete_focus.png | Bin .../res/mipmap-hdpi/home_icon_up_down_foc.png | Bin .../res/mipmap-hdpi/home_icon_up_down_nor.png | Bin app/src/main/res/mipmap-hdpi/ic_app.png | Bin .../res/mipmap-hdpi/ic_detail_benefits.png | Bin .../res/mipmap-hdpi/ic_detail_btn_collect.png | Bin .../mipmap-hdpi/ic_detail_btn_collect_f.png | Bin .../mipmap-hdpi/ic_detail_btn_collect_s.png | Bin .../res/mipmap-hdpi/ic_detail_btn_order.png | Bin .../res/mipmap-hdpi/ic_detail_btn_order_s.png | Bin .../mipmap-hdpi/ic_detail_video_from_mg.png | Bin .../main/res/mipmap-hdpi/ic_key_prompt_1.png | Bin .../main/res/mipmap-hdpi/ic_key_prompt_2.png | Bin .../main/res/mipmap-hdpi/ic_key_prompt_3.png | Bin .../main/res/mipmap-hdpi/ic_key_prompt_4.png | Bin app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin .../res/mipmap-hdpi/ic_launcher_round.webp | Bin app/src/main/res/mipmap-hdpi/ic_migu_mini.png | Bin app/src/main/res/mipmap-hdpi/ic_more_foc.png | Bin .../main/res/mipmap-hdpi/icon_backward.png | Bin app/src/main/res/mipmap-hdpi/icon_clear.png | Bin app/src/main/res/mipmap-hdpi/icon_forward.png | Bin .../main/res/mipmap-hdpi/icon_nonetwork.png | Bin .../main/res/mipmap-hdpi/icon_noresult.png | Bin .../res/mipmap-hdpi/icon_player_error.png | Bin .../res/mipmap-hdpi/index_my_collect_more.png | Bin .../res/mipmap-hdpi/index_my_history_more.png | Bin .../res/mipmap-hdpi/index_my_setting_bg.png | Bin app/src/main/res/mipmap-hdpi/keyboard_bg.png | Bin app/src/main/res/mipmap-hdpi/label_vip.png | Bin .../main/res/mipmap-hdpi/my_account_bg.png | Bin .../res/mipmap-hdpi/my_avatar_default.png | Bin app/src/main/res/mipmap-hdpi/no1.png | Bin app/src/main/res/mipmap-hdpi/no10.png | Bin app/src/main/res/mipmap-hdpi/no2.png | Bin app/src/main/res/mipmap-hdpi/no3.png | Bin app/src/main/res/mipmap-hdpi/no4.png | Bin app/src/main/res/mipmap-hdpi/no5.png | Bin app/src/main/res/mipmap-hdpi/no6.png | Bin app/src/main/res/mipmap-hdpi/no7.png | Bin app/src/main/res/mipmap-hdpi/no8.png | Bin app/src/main/res/mipmap-hdpi/no9.png | Bin app/src/main/res/mipmap-hdpi/nodata.png | Bin .../main/res/mipmap-hdpi/player_bg_vip.webp | Bin .../main/res/mipmap-hdpi/player_load_bg.png | Bin .../main/res/mipmap-hdpi/player_load_logo.png | Bin .../main/res/mipmap-hdpi/search_tv_line.png | Bin .../mipmap-hdpi/status_icon_search_foc.png | Bin .../mipmap-hdpi/status_icon_search_nor.png | Bin app/src/main/res/mipmap-hdpi/t9_five_bg.png | Bin app/src/main/res/mipmap-hdpi/t9_four_bg.png | Bin app/src/main/res/mipmap-hdpi/t9_two_bg.png | Bin app/src/main/res/mipmap-hdpi/videocolled.png | Bin app/src/main/res/mipmap-hdpi/videofs.png | Bin app/src/main/res/mipmap-hdpi/videohotel.png | Bin app/src/main/res/mipmap-hdpi/videonocoll.png | Bin app/src/main/res/mipmap-hdpi/videoorder.png | Bin app/src/main/res/mipmap-hdpi/videoordered.png | Bin app/src/main/res/mipmap-hdpi/vip.png | Bin app/src/main/res/mipmap-hdpi/vs.png | Bin app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin .../res/mipmap-mdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xhdpi/jst_order_bg.webp | Bin 0 -> 7550 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin app/src/main/res/values-night/themes.xml | 0 .../main/res/values-sw1080dp-land/dimens.xml | 0 .../main/res/values-sw360dp-land/dimens.xml | 0 .../main/res/values-sw480dp-land/dimens.xml | 0 .../main/res/values-sw540dp-land/dimens.xml | 0 .../main/res/values-sw617dp-land/dimens.xml | 0 .../main/res/values-sw720dp-land/dimens.xml | 0 .../main/res/values-sw768dp-land/dimens.xml | 0 .../main/res/values-sw864dp-land/dimens.xml | 0 app/src/main/res/values/attrs.xml | 0 app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/dimens_sw.xml | 0 app/src/main/res/values/ids.xml | 0 app/src/main/res/values/strings.xml | 40 + app/src/main/res/values/styles.xml | 0 app/src/main/res/values/themes.xml | 0 .../res/xml/device_not_activated_scene.xml | 0 app/src/main/res/xml/file_paths.xml | 0 .../res/xml/network_permission_config.xml | 0 app/src/ruigeer/res/values/strings.xml | 0 .../com/migu/jiecheng/ExampleUnitTest.java | 0 .../downapk/StoreVersionUpgraderTest.kt | 98 +++ .../IndexActivityNavSelectionStateTest.java | 0 .../adapter/viewholder/NavViewHolderTest.java | 0 .../OrderCenterAdapterStructureTest.java | 24 + .../jiecheng/request/AsApiErrorMapperTest.kt | 85 +++ .../request/AsApiRequesterMacCacheTest.kt | 27 + .../JetsenxSdkUtilFallbackAdapterTest.java | 76 ++ .../JetsenxSdkUtilFallbackThrottleTest.java | 51 ++ .../voice/parser/SkyworthVodParserTest.java | 282 +++++++ .../voice/parser/VendorAParserTest.java | 161 ++++ .../protocol/VoiceCallbackResultTest.java | 58 ++ .../voice/protocol/VoiceCommandTest.java | 91 +++ .../protocol/VoiceProtocolConstantsTest.java | 66 ++ .../voice/queue/CommandQueueTest.java | 92 +++ .../voice/router/CommandRouterTest.java | 272 +++++++ .../voice/throttle/CommandThrottleTest.java | 128 ++++ app/src/thirdparty/res/values/strings.xml | 4 - build.gradle | 0 build_channels.bat | 0 config.gradle | 2 +- day.code-workspace | 11 - gradle.properties | 0 gradle/wrapper/gradle-wrapper.jar | Bin gradle/wrapper/gradle-wrapper.properties | 0 gradlew | 0 gradlew.bat | 0 key/jiecheng.jks | Bin key/keystore.properties | 0 libcore/.gitignore | 0 libcore/build.gradle | 0 libcore/consumer-rules.pro | 0 libcore/proguard-rules.pro | 0 .../libcore/ExampleInstrumentedTest.java | 0 libcore/src/main/AndroidManifest.xml | 0 .../com/pukka/libcore/base/BaseConst.java | 10 +- .../java/com/pukka/libcore/base/BaseUrl.java | 0 .../com/pukka/libcore/base/BuildConfig.java | 0 .../com/pukka/libcore/base/DifferenceApi.java | 0 .../com/pukka/libcore/base/DifferenceUrl.java | 0 .../boxShared/BoxSharedParameters.java | 0 .../boxShared/BoxSharedParamsUtils.java | 0 .../com/pukka/libcore/boxShared/IpUtils.java | 0 .../com/pukka/libcore/boxShared/MacUtils.java | 264 ++----- .../pukka/libcore/boxShared/MacUtils2.java | 73 -- .../com/pukka/libcore/cipher/AesUtils.java | 0 .../pukka/libcore/cipher/HMacShaUtils.java | 6 +- .../com/pukka/libcore/cipher/Md5Utils.java | 4 +- .../libcore/fresco/BaseSimpleDraweeView.java | 0 .../com/pukka/libcore/fresco/FrescoInit.java | 0 .../libcore/fresco/IPkSimpleDraweeView.java | 0 .../libcore/fresco/PkControllerListener.java | 0 .../fresco/PkSimpleDraweeListener.java | 0 .../libcore/fresco/PkSimpleDraweeView.java | 0 .../libcore/fresco/Tls12SocketFactory.java | 0 .../pukka/libcore/fresco/TrustAllManager.java | 0 .../libcore/retrofit/DefaultObserver.java | 0 .../libcore/retrofit/ExceptionReason.java | 0 .../libcore/retrofit/OkHttpClientUtil.java | 0 .../pukka/libcore/retrofit/PkApiService.java | 0 .../pukka/libcore/retrofit/RetrofitApi.java | 0 .../lowplatform/AndroidPlatform9.java | 0 .../lowplatform/LowPlatformUtils.java | 0 .../retrofit/lowplatform/OptionalMethod.java | 0 .../threading/ConcurrentDispatchQueue.java | 0 .../libcore/threading/DispatchQueue.java | 0 .../libcore/threading/DispatchQueueType.java | 0 .../libcore/threading/GuardedRunnable.java | 0 .../threading/SerialDispatchQueue.java | 0 .../com/pukka/libcore/threading/Timer.java | 0 .../com/pukka/libcore/utils/AppContext.java | 0 .../libcore/utils/AppLifecycleObserver.java | 0 .../com/pukka/libcore/utils/AppManager.java | 0 .../com/pukka/libcore/utils/AppUtils.java | 0 .../pukka/libcore/utils/Base64Decoder.java | 0 .../com/pukka/libcore/utils/Base64Util.java | 0 .../libcore/utils/ByteConversionUtils.java | 0 .../com/pukka/libcore/utils/ContextUtils.java | 0 .../com/pukka/libcore/utils/DateUtils.java | 0 .../pukka/libcore/utils/GreyingManager.java | 0 .../com/pukka/libcore/utils/JsonUtils.java | 0 .../com/pukka/libcore/utils/ListUtils.java | 0 .../com/pukka/libcore/utils/LogUtils.java | 0 .../com/pukka/libcore/utils/MD5Utils.java | 0 .../pukka/libcore/utils/MacAddressUtil.java | 53 +- .../com/pukka/libcore/utils/PackageUtils.java | 0 .../com/pukka/libcore/utils/ProcessUtil.java | 0 .../com/pukka/libcore/utils/ScreenUtil.java | 0 .../libcore/utils/SharedPreferenceUtils.java | 0 .../com/pukka/libcore/utils/SpSaveModel.java | 0 .../com/pukka/libcore/utils/SpTimeUtils.java | 0 .../com/pukka/libcore/utils/StringUtil.java | 81 ++- .../com/pukka/libcore/utils/StringUtils.java | 0 .../com/pukka/libcore/viewmodel/BaseVM.java | 0 .../libcore/widget/AlwaysMarqueeTextView.java | 0 .../pukka/libcore/widget/BrowserWebView.java | 0 .../libcore/widget/ContentRecyclerViewTv.java | 0 .../HorizontalLinearLayoutManagerTv.java | 0 .../widget/HorizontalRecyclerViewTv.java | 0 .../libcore/widget/TVLinearLayoutManager.java | 0 .../widget/VerticalGridLayoutManagerTv.java | 0 .../widget/VerticalLinearLayoutManagerTv.java | 0 .../widget/VerticalRecyclerViewTv.java | 0 .../com/pukka/libcore/ExampleUnitTest.java | 0 libdata/.gitignore | 0 libdata/build.gradle | 0 libdata/consumer-rules.pro | 0 libdata/proguard-rules.pro | 0 .../migu/libdata/ExampleInstrumentedTest.java | 0 libdata/src/main/AndroidManifest.xml | 0 .../src/main/java/com/migu/DataActivity.java | 0 .../java/com/migu/amber/AmberHeaderBean.java | 0 .../main/java/com/migu/amber/AmberUtil.java | 0 .../src/main/java/com/migu/bean/AdBean.java | 0 .../com/migu/bean/ApkVersionResponse.java | 0 .../java/com/migu/bean/AspectRatioBean.java | 0 .../src/main/java/com/migu/bean/AuthBean.java | 0 .../com/migu/bean/AuthOrizeBean_shanghai.java | 0 .../java/com/migu/bean/AuthorizeNewBean.java | 0 .../main/java/com/migu/bean/BesAuthBean.java | 0 .../main/java/com/migu/bean/BesOrderBean.java | 0 .../java/com/migu/bean/BesPlayUrlBean.java | 0 .../main/java/com/migu/bean/CollectBean.java | 0 .../migu/bean/CollectReportBean_shanghai.java | 0 .../java/com/migu/bean/ContentListBean.java | 0 .../com/migu/bean/DisplayTypeListBean.java | 0 .../java/com/migu/bean/EventBusMessage.java | 0 .../com/migu/bean/GetCalenderMatchDate.java | 0 .../java/com/migu/bean/GetCollectBean.java | 0 .../java/com/migu/bean/GetEpgNavBean.java | 0 .../com/migu/bean/GetFindPlayrecordBean.java | 0 .../java/com/migu/bean/GetGameDetailBean.java | 0 .../bean/GetGamePlayurlAndPlayStatusBean.java | 0 .../migu/bean/GetGameSeasonInfoListBean.java | 0 .../bean/GetGameSeasonInfoRecommendBean.java | 0 .../java/com/migu/bean/GetLayoutBean.java | 0 .../java/com/migu/bean/GetLayoutNavBean.java | 0 .../java/com/migu/bean/GetMatchListBean.java | 0 .../com/migu/bean/GetServiceListBean.java | 0 .../java/com/migu/bean/GetShortVideoBean.java | 0 .../java/com/migu/bean/GetTopButtonBean.java | 0 .../com/migu/bean/GetcontentlistBean.java | 0 .../java/com/migu/bean/GetlistcollBean.java | 0 .../java/com/migu/bean/GetsystemBean.java | 0 .../java/com/migu/bean/GettagslistBean.java | 0 .../com/migu/bean/GoOrderBean_shanghai.java | 0 .../main/java/com/migu/bean/GoldMainBean.java | 0 .../java/com/migu/bean/HeartBeatEntity.java | 0 .../migu/bean/HistoryReportBean_shanghai.java | 0 .../com/migu/bean/HljAddCollectionBean.java | 0 .../java/com/migu/bean/HljAddHistoryBean.java | 0 .../com/migu/bean/IntentBean_Shanghai.java | 0 .../java/com/migu/bean/KYSDKPlayInfoUser.java | 0 .../main/java/com/migu/bean/KeyboardBean.java | 0 .../com/migu/bean/MediaPlayerHeaderBean.java | 0 .../main/java/com/migu/bean/NewtvBean.java | 0 .../main/java/com/migu/bean/OTTSSOEntity.java | 0 .../main/java/com/migu/bean/PassDataBean.java | 0 .../java/com/migu/bean/PhoneDataBean.java | 0 .../java/com/migu/bean/PlayHistoryBean.java | 0 .../src/main/java/com/migu/bean/Province.java | 0 .../src/main/java/com/migu/bean/RateBean.java | 0 .../java/com/migu/bean/ReleaseVideoBean.java | 0 .../java/com/migu/bean/ResumeVideoBean.java | 0 .../com/migu/bean/SearchRecommendBean.java | 0 .../java/com/migu/bean/SearchResultBean.java | 0 .../main/java/com/migu/bean/ShanghaiBean.java | 0 .../java/com/migu/bean/ShortVideoBean.java | 0 .../com/migu/bean/ShortVideoInfoListBean.java | 0 .../com/migu/bean/ShortVideoLazyBean.java | 0 .../java/com/migu/bean/ShowOrGoneNavBean.java | 0 .../java/com/migu/bean/SilenceFullPlay.java | 0 .../java/com/migu/bean/StarInfoIndexBean.java | 0 .../com/migu/bean/StarPlayerInfoBean.java | 0 .../com/migu/bean/SubmitPointsLikesBean.java | 0 .../main/java/com/migu/bean/SuggestBean.java | 0 .../java/com/migu/bean/SupportVoteBean.java | 0 .../java/com/migu/bean/T9KeyboardBean.java | 0 .../java/com/migu/bean/TopicVoteInfoBean.java | 0 .../main/java/com/migu/bean/UpApkBean.java | 0 .../java/com/migu/bean/UpApkRequestBean.java | 0 .../java/com/migu/bean/VideoResultBean.java | 0 .../java/com/migu/callback/AijiaCallback.java | 0 .../com/migu/callback/CommonCallback.java | 0 .../callback/GoOrderCallbackShanghai.java | 0 .../com/migu/httputils/AndroidPlatform9.java | 0 .../com/migu/httputils/BaseHttpUtils.java | 0 .../com/migu/httputils/ContentHttpUtils.java | 0 .../com/migu/httputils/DataValueUtils.java | 0 .../com/migu/httputils/HomeHttpUtils.java | 0 .../com/migu/httputils/LowPlatformUtils.java | 0 .../com/migu/httputils/MatchHttpUtils.java | 0 .../com/migu/httputils/OptionalMethod.java | 0 .../java/com/migu/libnetwork/AnHuiApi.java | 0 .../java/com/migu/libnetwork/ApiService.java | 0 .../com/migu/libnetwork/GuangDongApi.java | 0 .../java/com/migu/libnetwork/HaiNanApi.java | 0 .../java/com/migu/libnetwork/HeNanApi.java | 0 .../com/migu/libnetwork/HeiLongJiangApi.java | 0 .../migu/libnetwork/HttpRequestCallback.java | 0 .../migu/libnetwork/HttpRequestConstants.java | 0 .../libnetwork/HttpRequestFileCallback.java | 0 .../libnetwork/HttpRequestInterceptor.java | 0 .../com/migu/libnetwork/HttpRetrofit.java | 0 .../migu/libnetwork/HttpRetrofitRequest.java | 0 .../java/com/migu/libnetwork/HttpTool.java | 0 .../java/com/migu/libnetwork/HttpUtils.java | 0 .../com/migu/libnetwork/HttpsTrustManager.kt | 0 .../com/migu/libnetwork/IHttpRequest.java | 0 .../java/com/migu/libnetwork/NetUtil.java | 0 .../com/migu/libnetwork/OkHttpClientUtil.java | 0 .../java/com/migu/libnetwork/OkHttpSsl.java | 0 .../migu/libnetwork/RetrofitClientUtil.java | 0 .../com/migu/libnetwork/RetryFunction.java | 0 .../migu/libnetwork/RxJavaFileObserver.java | 0 .../com/migu/libnetwork/RxJavaObserver.java | 0 .../migu/libnetwork/SSLSocketFactoryCompat.kt | 0 .../java/com/migu/libnetwork/SearchApi.java | 0 .../libnetwork/TrustAllHostnameVerifier.kt | 0 .../main/res/layout/libdata_activity_main.xml | 0 libdata/src/main/res/values/colors.xml | 0 libdata/src/main/res/values/dimens.xml | 0 libdata/src/main/res/values/strings.xml | 0 .../com/migu/libdata/ExampleUnitTest.java | 0 mediaplayertv/.gitignore | 0 mediaplayertv/build.gradle | 0 mediaplayertv/consumer-rules.pro | 0 mediaplayertv/libs/HyMediaPlayer-v1.2.15.aar | Bin .../exoplayer-ffmpeg-extension-2.19.1.aar | Bin mediaplayertv/proguard-rules.pro | 0 .../ExampleInstrumentedTest.java | 0 mediaplayertv/src/main/AndroidManifest.xml | 0 .../videoplayertv/AbstractMediaPlayer.java | 0 .../videoplayertv/AndroidMediaFormat.java | 0 .../videoplayertv/AndroidMediaPlayer.java | 0 .../pukka/videoplayertv/AndroidTrackInfo.java | 0 .../com/pukka/videoplayertv/ConfigPlayer.java | 0 .../pukka/videoplayertv/IMediaController.java | 0 .../pukka/videoplayertv/IMediaDataSource.java | 0 .../com/pukka/videoplayertv/IMediaFormat.java | 0 .../com/pukka/videoplayertv/IMediaPlayer.java | 0 .../com/pukka/videoplayertv/IRenderView.java | 0 .../videoplayertv/ISurfaceTextureHolder.java | 0 .../videoplayertv/ISurfaceTextureHost.java | 0 .../com/pukka/videoplayertv/ITrackInfo.java | 0 .../pukka/videoplayertv/MeasureHelper.java | 0 .../com/pukka/videoplayertv/MediaInfo.java | 0 .../videoplayertv/MediaPlayerControl.java | 0 .../pukka/videoplayertv/SoftMediaPlayer.java | 0 .../videoplayertv/SurfaceRenderView.java | 0 .../pukka/videoplayertv/TvBaseController.java | 0 .../videoplayertv/TvMediaController.java | 0 .../com/pukka/videoplayertv/VideoViewTv.java | 12 +- .../exoplayer/ExoMediaSourceHelper.java | 8 + .../exoplayer/PkExoMediaPlayer.java | 113 ++- .../exoplayer/PkHighExoMediaPlayer.java | 0 .../main/res/drawable-hdpi/ic_media_pause.png | Bin .../main/res/drawable-hdpi/player_load_bg.png | Bin .../res/drawable-hdpi/player_load_logo.png | Bin .../main/res/drawable-xxhdpi/loading00.png | Bin .../main/res/drawable-xxhdpi/loading000.png | Bin .../main/res/drawable-xxhdpi/loading01.png | Bin .../main/res/drawable-xxhdpi/loading02.png | Bin .../main/res/drawable-xxhdpi/loading03.png | Bin .../main/res/drawable-xxhdpi/loading04.png | Bin .../main/res/drawable-xxhdpi/loading05.png | Bin .../main/res/drawable-xxhdpi/loading06.png | Bin .../main/res/drawable-xxhdpi/loading07.png | Bin .../main/res/drawable-xxhdpi/loading08.png | Bin .../main/res/drawable-xxhdpi/loading09.png | Bin .../main/res/drawable-xxhdpi/loading10.png | Bin .../main/res/drawable-xxhdpi/loading11.png | Bin .../main/res/drawable-xxhdpi/loading12.png | Bin .../main/res/drawable-xxhdpi/loading13.png | Bin .../main/res/drawable-xxhdpi/loading14.png | Bin .../main/res/drawable-xxhdpi/loading15.png | Bin .../main/res/drawable-xxhdpi/loading16.png | Bin .../main/res/drawable-xxhdpi/loading17.png | Bin .../main/res/drawable-xxhdpi/loading18.png | Bin .../main/res/drawable-xxhdpi/loading19.png | Bin .../main/res/drawable-xxhdpi/loading20.png | Bin .../main/res/drawable-xxhdpi/loading21.png | Bin .../main/res/drawable-xxhdpi/loading22.png | Bin .../res/drawable-xxhdpi/progress_loading.png | Bin .../drawable-xxhdpi/switch_keyboard_bg.png | Bin .../res/drawable/bg_btn_video_selector.xml | 0 .../main/res/drawable/bg_control_bottom.xml | 0 .../src/main/res/drawable/bg_control_top.xml | 0 .../res/drawable/bg_pause_video_selector.xml | 0 .../drawable/bg_white_round_border_normal.xml | 0 .../bg_white_round_border_pressed.xml | 0 .../bg_white_round_border_selector.xml | 0 .../src/main/res/drawable/loading_icon.png | Bin .../res/drawable/loading_player_animation.xml | 0 .../res/drawable/ondemand_seekbar_style.xml | 0 .../main/res/drawable/player_seek_thumb.xml | 0 .../drawable/seekbar_focused_background.xml | 0 .../res/drawable/svg_media_pause_icon.xml | 0 .../main/res/drawable/svg_media_play_icon.xml | 0 .../src/main/res/drawable/videotip1.png | Bin .../src/main/res/drawable/videotip2.png | Bin .../res/drawable/volume_seek_progress.xml | 0 .../main/res/layout/media_controller_tv.xml | 0 .../res/layout/media_vod_menu_controller.xml | 0 .../src/main/res/mipmap-hdpi/dot.png | Bin .../main/res/mipmap-hdpi/ic_media_play.png | Bin .../main/res/mipmap-hdpi/icon_backward.png | Bin .../src/main/res/mipmap-hdpi/icon_forward.png | Bin .../main/res/values-sw1080dp-land/dimens.xml | 0 .../main/res/values-sw360dp-land/dimens.xml | 0 .../main/res/values-sw480dp-land/dimens.xml | 0 .../main/res/values-sw540dp-land/dimens.xml | 0 .../main/res/values-sw617dp-land/dimens.xml | 0 .../main/res/values-sw720dp-land/dimens.xml | 0 mediaplayertv/src/main/res/values/colors.xml | 0 mediaplayertv/src/main/res/values/dimens.xml | 0 .../src/main/res/values/dimens_sw.xml | 0 .../src/main/res/values/h_dimens.xml | 0 mediaplayertv/src/main/res/values/strings.xml | 0 .../src/main/res/values/w_dimens.xml | 0 .../pukka/mediaplayertv/ExampleUnitTest.java | 0 settings.gradle | 0 test/voice_helper/README.md | 167 +++++ test/voice_helper/cold_start.sh | 47 ++ test/voice_helper/control.sh | 35 + test/voice_helper/device.sh | 14 + test/voice_helper/jump_category.sh | 22 + test/voice_helper/jump_film.sh | 28 + test/voice_helper/lib/common.sh | 342 +++++++++ test/voice_helper/listen_callback.sh | 31 + test/voice_helper/run_all.sh | 88 +++ test/voice_helper/search.sh | 39 + test/voice_helper/security_test.sh | 22 + test/voice_helper/skyworth_cold_start.sh | 38 + test/voice_helper/skyworth_empty_test.sh | 52 ++ test/voice_helper/skyworth_security_test.sh | 21 + test/voice_helper/skyworth_vod.sh | 25 + test/voice_helper/throttle_test.sh | 30 + 盛阳等三方支付EPG传参App开发需求.md | 243 ------- 1394 files changed, 13694 insertions(+), 3710 deletions(-) mode change 100755 => 100644 .gitignore create mode 100644 5.1声道接入成果总结.md create mode 100644 5.1声道改造-代码变更清单与复用指南.md mode change 100755 => 100644 README.md mode change 100755 => 100644 app/.gitignore mode change 100755 => 100644 app/build.gradle mode change 100755 => 100644 app/key/jiecheng.jks mode change 100755 => 100644 app/key/keystore.properties mode change 100755 => 100644 app/libs/JetsenxSdk-api-future-release-v1.23.aar mode change 100755 => 100644 app/libs/JetsenxSdk-api-release_v1.14.0.aar mode change 100755 => 100644 app/libs/JetsenxSdk-external-future-release-v1.23.aar mode change 100755 => 100644 app/libs/JetsenxSdk-external-release_v1.14.0.aar mode change 100755 => 100644 app/libs/NewTVSDK-v4.0.3-app-lite.jar mode change 100755 => 100644 app/libs/arm64-v8a/libnewtvsdk.so mode change 100755 => 100644 app/libs/armeabi-v7a/libnewtvsdk.so mode change 100755 => 100644 app/libs/exoplayer-ffmpeg-extension-2.19.1.aar mode change 100755 => 100644 app/proguard-rules.pro mode change 100755 => 100644 app/src/androidTest/androidmanifest.xml mode change 100755 => 100644 app/src/androidTest/java/com/migu/jiecheng/ExampleInstrumentedTest.java mode change 100755 => 100644 app/src/androidTest/java/com/migu/jiecheng/IndexNavType6TestActivity.java mode change 100755 => 100644 app/src/androidTest/java/com/migu/jiecheng/IndexNavType6UiTest.java rename app/src/{aishiActivation => as8Scan}/res/values/strings.xml (100%) mode change 100755 => 100644 rename app/src/{aishiScan => asAScan}/res/values/strings.xml (100%) mode change 100755 => 100644 create mode 100644 app/src/ashome/res/values/strings.xml create mode 100644 app/src/fxScan/res/values/strings.xml create mode 100644 app/src/home/res/values/strings.xml mode change 100755 => 100644 app/src/main/AndroidManifest.xml mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/JcConfig.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/MainActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/MyApplication.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/TestActivity.java delete mode 100644 app/src/main/java/com/pukka/jiecheng/ThirdPartyEntryActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/aboutmy/AboutMyActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/aboutmy/LongPressSpanHelper.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/base/BaseActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/base/BaseOnItemClickListener.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/base/BaseOnItemFocusChangeListener.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/base/BasePresenter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/base/BasePresenterViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/base/BaseRecyclerAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/base/BaseRecyclerViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/base/BaseViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/base/SystemConfigManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/config/AppConfiguration.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/device/DeviceActivity.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityInitManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityProvider.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityStore.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/device/OaidProvider.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/downapk/DownApkDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/downapk/DownloadApkManager.java create mode 100644 app/src/main/java/com/pukka/jiecheng/downapk/StoreVersionUpgrader.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/downapk/UpgradeHelper.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/downapk/amber/DownApkAmberManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/filter/FilterActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/filter/FilterFragment.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/filter/HeaderVisibilityHelper.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/filter/adapter/ContentAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/filter/adapter/HeaderAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/filter/adapter/TopAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/filter/adapter/TopAdapter2.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/filter/bean/TopBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/filter/bean/TopListBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/history/CenterLinearLayoutManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/history/HistoryActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/history/HistoryItemAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/history/HistoryTextBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/history/HistoryTextPresenter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/HotelMyActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/IndexActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/IndexAmberManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/IndexConstant.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/D1ContentAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/D3ContentAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/D3NavAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/D4ContentAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/IndexContentAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/IndexGroupNavAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMyCollectAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMyHistoryAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMySetMoreAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/IndexNavAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/MatchListAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/MatchListHorAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/ShortVideoAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/interfaces/OnNavidListener.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/viewholder/GroupNavViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/adapter/viewholder/NavViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/bean/CornerMarkBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/bean/D1TypeBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/bean/DefaultNavFocusBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/bean/ItemTypeBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/bean/LogOutBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/bean/MyIndexBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/bean/NavBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/bean/NavFocusBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/bean/NavResideBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/dialog/BackImgDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/dialog/BackRetentionDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/dialog/FullimgDialog.java create mode 100644 app/src/main/java/com/pukka/jiecheng/index/dialog/HomeLogoutConfirmDialog.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/dialog/UserExitDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/fragment/ContentFragment.java create mode 100644 app/src/main/java/com/pukka/jiecheng/index/fragment/HomeMyFragment.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/fragment/HotelMyFragment.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/fragment/MyFragment.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/fragment/ShortVideoFragment.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/manager/FloorOnBindViewHolderManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/manager/FloorOnCreateViewHolderManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/A49ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/A50ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B10ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B11ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B12ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B13ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B14ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B15ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B1ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B2ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B2_2ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B2_3ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B3ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B4ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B5ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B5_2ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B6ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B7ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B8ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/B9ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseBannerViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseExVideoViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseTitleViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/C1ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/C2ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/C3ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/C4ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/C5ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/C6ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/C6_1ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/C7ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/D1ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/D2ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/D2_1ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/D3ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/D4ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/JC1ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/JC32ViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewholder/ToTopViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewmodel/ContentViewModel.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewmodel/IndexViewModel.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewmodel/MyViewModel.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewmodel/NavViewModel.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewmodel/ShortVideoViewModel.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/index/viewmodel/TopViewModel.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/list/ListActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/list/ListAmberManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/list/ListStyleUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/list/adapter/ListContentAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/list/adapter/ListNavAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/list/viewmodel/ListViewModel.java create mode 100644 app/src/main/java/com/pukka/jiecheng/login/HomeLogoutHandler.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/login/KeyboardAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/login/LoginActivity.java create mode 100644 app/src/main/java/com/pukka/jiecheng/login/WxMpLoginActivity.java create mode 100644 app/src/main/java/com/pukka/jiecheng/login/WxMpLoginManager.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/login/bean/GetCloudLoginBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/login/bean/GetQueryUsableBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/login/bean/LoginCodeBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/login/bean/LoginLogonBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/login/bean/QueryVipInfoBean.java create mode 100644 app/src/main/java/com/pukka/jiecheng/login/store/HomeUserStore.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/login/viewmodel/IsLoginViewModel.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/monitor/HomeMonitor.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/monitor/NetworkMonitor.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/ApiRequesters.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/ApiRequesters_MapHelper.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/api/AsApi.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/api/BenefitOrderApi.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/network/api/StoreVersionApi.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/auth/HeaderInterceptor.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/auth/SignAuthenticator.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/network/auth/SignRefresher.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/auth/SignRequestPolicy.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/auth/TokenStore.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/ActivateDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/ActivateReq.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderJsapiDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderReq.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderRespDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderStatusDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/CheckBenefitsDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/CheckBenefitsReq.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/CheckDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/CheckReq.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/network/dto/DeviceBenefitsDto.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/network/dto/DeviceMeDto.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatActionDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatReq.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/InitDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/InitReq.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/network/dto/LoginUrlDto.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/network/dto/LogoutDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/RefreshSignDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/RefreshSignReq.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/network/dto/StoreVersionDto.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/network/dto/StoreVersionReq.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/TrackingDto.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/dto/TrackingReq.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/infra/ApiResult.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/infra/EnvProvider.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/infra/MoshiProvider.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/infra/NetResponse.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/infra/Network.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/infra/NetworkConfig.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/infra/NetworkEnv.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/infra/OkHttpProvider.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/network/infra/RetrofitFactory.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/newtv/NewTvListner.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/newtv/NewTvManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/newtv/NewTvUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/newtv/NewTvView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/newtv/SensorsPlayBean.java create mode 100644 app/src/main/java/com/pukka/jiecheng/order/JsapiOrderHelper.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/order/JsapiOrderMonitor.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/order/JsapiOrderMonitorCallback.kt delete mode 100755 app/src/main/java/com/pukka/jiecheng/order/OrderActivity.java create mode 100644 app/src/main/java/com/pukka/jiecheng/order/OrderActivity.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/OrderAmberManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/OrderStyleUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/adapter/OrderCenterAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/adapter/OrderListAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/adapter/viewholder/CenterViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/adapter/viewholder/CenterVipViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/bean/ActivateBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/bean/GetPayResultCycle.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/bean/OrderListBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/bean/PricingBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/bean/QrCodeBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/dialog/OrderCancelDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/dialog/OrderDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/fragment/OrderCenterFragment.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/fragment/OrderDuihuanFragment.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/fragment/OrderListFragment.java create mode 100644 app/src/main/java/com/pukka/jiecheng/order/fragment/OrderNetworkBridge.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/fragment/OrderXufeiFragment.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/order/viewmodel/OrderViewModel.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/permission/AgreementStoragePermissionGateway.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/request/AsApiErrorCode.java create mode 100644 app/src/main/java/com/pukka/jiecheng/request/AsApiErrorMapper.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/request/AsApiRequester.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/request/AsHeartBeatManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/Bean/HistoryBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/SearchActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/SearchAmberManger.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/SearchConstant.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/SearchRecyclerView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/T9KeyboardPop.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/UnFocusListener.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/adapter/ContentInfoAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/adapter/DisplayTypeAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/adapter/HistoryAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/adapter/HotContentAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/adapter/KeyboardAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/adapter/LiveWidgetInfoAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/adapter/SuggestAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/adapter/T9KeyboardAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/search/widget/SearchResultRecyclerView.java create mode 100644 app/src/main/java/com/pukka/jiecheng/setting/SettingActivity.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/setting/SettingAdapter.kt create mode 100644 app/src/main/java/com/pukka/jiecheng/setting/SettingItem.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/ActivitedDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/ActivityMonitor.java create mode 100644 app/src/main/java/com/pukka/jiecheng/utils/BenefitRevokedDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/BuglyInitManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/BuildConfigUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/ChannelManger.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/CloudKeyCodeUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/ColorSpanUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/ColorUtil.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/CommonLoadingDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/CrashHandler.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/DataValueUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/GridSpacingItemDecoration.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/IsLoginTimerUtil.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/ItemOnClickUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/JetsenxSdkUtil.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/MgRoute.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/NewRecordCollManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/NextPageManger.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/PageNumberUtil.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/PaginationUtil.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/PayResultTimerUtil.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/QRCodeUtil.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/QRCodeWriter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/ResUtil.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/RsaSignUtil.java create mode 100644 app/src/main/java/com/pukka/jiecheng/utils/Surround51Manager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/ToastUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/TopStyleUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/UiUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/UpdateDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/UpdateOtherDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/UrlParamBuilder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/VideoContainerHelper.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/utils/XCrashInitManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/VideoDetailActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/adapter/HistoryAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/adapter/ProgramTableAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/adapter/ProgramTablePageAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/adapter/RecommendAdapter.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/adapter/vh/HistoryViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableEmptyViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableVarietyEmptyViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableVarietyViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/adapter/vh/RecommendViewHolder.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/amber/DetailAmberManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/bean/ErrInfoBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/bean/FindPlayrecordBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/bean/JetsenxEventReportBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/bean/PlayCodeBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/bean/SendAddPlayRecordBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/bean/TablePageBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/bean/TestBean.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/benefit/BenefitOrderCallback.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/benefit/BenefitOrderController.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/face/DetailInterface.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/face/NavigationInterface.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/face/PlayTheNextVideoInterface.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/face/SelectionsInterface.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/fragment/RecommendFragment.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/manger/ResolutionManger.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/manger/SpeedManger.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/other/MgDividerItemDecoration.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/other/SpacesItemDecoration.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/popup/BlackFullDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/popup/InfoPopupWindow.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/popup/LoadFullDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/popup/OnDemandEpisodesView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/popup/QrCodeDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/popup/RetentionDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/popup/TipsDialog.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/ContentDetailUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/HandlerLifecycleManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/KeyThrottleVerifier.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordContentManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordShortManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/SelectedProgramsManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/TimeVideoUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/ToastUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/VideoActivityStackManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/VideoDetailKeyHandler.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/VideoDetailStatusManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/VideoFocusStateMachine.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/utils/ViewUtils.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/video/widget/VideoDetailRecommendRecyclerView.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/adapter/SkyworthVoiceAdapter.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/guard/AppStateGuard.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/guard/DefaultAppStateGuard.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/log/VoiceCommandLogger.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/parser/SkyworthVodParser.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/parser/VendorAParser.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/parser/VendorBParser.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/parser/VoicePlatformParser.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCallbackResult.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCommand.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCommandType.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceExtras.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceProtocolConstants.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/queue/CommandQueue.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/receiver/BootCompletedReceiver.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/receiver/KeepAlivePingReceiver.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceCommandReceiver.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceInternalReceiver.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/router/BridgeRegistry.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/router/CommandRouter.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/router/DefaultPageNavigator.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/router/PageNavigator.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/router/PlaybackControlBridge.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandCoordinator.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandService.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/service/VoiceKeepAliveService.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottle.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottleClock.java create mode 100644 app/src/main/java/com/pukka/jiecheng/voice/window/VoiceOverlayHelper.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/web/SimpleWebActivity.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/ActivationCodeInputView.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/ActivationFocusRecyclerView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/ActivationKeyItem.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/ActivationKeypadAdapter.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/CornerBadgeView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/CustomerCodeView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/D3ContentLinearLayoutManager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/D3NavLinerLayoutManagerTv.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/D3RecyclerView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/DeviceNotActivatedView.kt delete mode 100644 app/src/main/java/com/pukka/jiecheng/widget/DeviceUnregisteredView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/FocusLinearLayout.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/FocusRecyclerView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/FocusTextView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/HomeNavLinearLayoutManagerTv.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/HorizontalGridViewPk.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/LinearSpacingItemDecoration.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/LocationCodeInputView.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/MarqueeTextView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/MgRecyclerView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/QrCodeView.java create mode 100644 app/src/main/java/com/pukka/jiecheng/widget/SettingItemView.kt mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/SpaceItemDecoration.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/SquareTextView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/TopView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/TvXRecyclerView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/UserAgreementView.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/VerticalRecyclerViewEx.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/VideoContainer.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/banner/Banner.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/banner/BannerConfig.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/banner/BannerScroller.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/banner/BannerViewPager.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/banner/OnBannerListener.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/banner/WeakHandler.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/banner/loader/BaseImageLoader.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/banner/loader/FrescoImageLoader.java mode change 100755 => 100644 app/src/main/java/com/pukka/jiecheng/widget/banner/loader/ImageLoaderInterface.java mode change 100755 => 100644 app/src/main/res/anim/activation_code_shake.xml mode change 100755 => 100644 app/src/main/res/anim/rotate_anim.xml mode change 100755 => 100644 app/src/main/res/color/activation_action_text_selector.xml mode change 100755 => 100644 app/src/main/res/color/activation_key_text_selector.xml mode change 100755 => 100644 app/src/main/res/color/activation_view_text_selector.xml mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_about_submit.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_about_submit_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_about_yhxy.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_about_yhxy_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_about_ysxy.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_about_ysxy_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_back_search.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_back_search_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_back_top.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_back_top_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_check_vr_btn_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_check_vr_btn_nor.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_hotel_about.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_hotel_deviceinfo.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_hotel_setting.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_hotel_update.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_hotel_user.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_short_mark.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_short_playing_check.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_short_vod.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_short_vod_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_user_equit.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_useragreement.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_useragreement_submit.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_useragreement_submit_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_useragreenmen_quit_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/bg_useragreenment_quit_unfocus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/btn_1_foc.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/btn_1_nor.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/btn_device_unactivit_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/btn_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/btn_nor.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/btn_progress.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/btn_short_playing_submit.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/device_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/device_check_update.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/device_check_update_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/device_info_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/gamepad.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/help_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/home_btn_down_foc.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/home_btn_down_nor.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/ic_check_vr_no.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/ic_check_vr_yes.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/ic_full_vr_tip.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/ic_short_playing.gif mode change 100755 => 100644 app/src/main/res/drawable-hdpi/ic_weixin.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/ic_zhifubao.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/icon_detail_back_normal.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/icon_error_prompt.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/index_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/index_my_user_bg_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/jst_logo_right.webp mode change 100755 => 100644 app/src/main/res/drawable-hdpi/keyboard_clear.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/keyboard_delete.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/list_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/list_bg_tag_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/list_bg_tag_unfocus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/list_search_ic.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/my_order_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/my_order_jilu_bg.webp mode change 100755 => 100644 app/src/main/res/drawable-hdpi/my_order_page_select.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/my_order_page_unselect.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/order_bg1.webp mode change 100755 => 100644 app/src/main/res/drawable-hdpi/order_bg_qr_code.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/order_btn_focus_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/order_btn_focused_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/order_btn_unfocus_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/order_center_keyboard_btn_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/order_center_keyboard_btn_unfocus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/order_center_vip_text_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/order_pre_tip.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/ordercenter_item_focus_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/ordercenter_item_unfoces_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/progress.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/progress_loading.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/short_ok_game_tip.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/short_ok_tip.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/short_up_game_tip.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/short_up_tip.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/try_see_pay_tip.webp mode change 100755 => 100644 app/src/main/res/drawable-hdpi/update_dialog_bg.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/weilai_btn_error_focus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/weilai_btn_error_nofocus.png mode change 100755 => 100644 app/src/main/res/drawable-hdpi/weilai_error_bg.webp mode change 100755 => 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml mode change 100755 => 100644 app/src/main/res/drawable-xhdpi/progress_loading.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading00.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading000.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading01.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading02.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading03.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading04.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading05.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading06.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading07.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading08.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading09.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading10.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading11.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading12.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading13.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading14.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading15.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading16.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading17.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading18.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading19.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading20.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading21.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/loading22.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/progress_loading.png mode change 100755 => 100644 app/src/main/res/drawable-xxhdpi/switch_keyboard_bg.png mode change 100755 => 100644 app/src/main/res/drawable-xxxhdpi/progress_loading.png mode change 100755 => 100644 app/src/main/res/drawable-xxxhdpi/switch_keyboard_bg.png mode change 100755 => 100644 app/src/main/res/drawable/background_search.xml mode change 100755 => 100644 app/src/main/res/drawable/background_splash.webp mode change 100755 => 100644 app/src/main/res/drawable/bg_2.webp mode change 100755 => 100644 app/src/main/res/drawable/bg_about_submit_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_about_yhxy_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_about_ysxy_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_activation_action_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_activation_code_box.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_activation_device_panel.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_activation_gradient.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_activation_input_preview.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_activation_key_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_activation_loading_panel.xml create mode 100644 app/src/main/res/drawable/bg_avatar_white_border.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_bottom_round_corner.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_bottom_white_round_corner.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_btn_confirm.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_btn_err_prompt.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_btn_retention_prompt.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_btn_trysee_open_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_btn_useragreenment_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_btn_useragreenment_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_btn_useragreenment_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_check_8k_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_check_vr_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_content_c7_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_content_c7_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_content_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_content_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_content_t_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_content_t_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_content_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_deman_table_page_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_demand_table_page_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_demandbg_table_page_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_back_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_broadcast_label.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_btn_order_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_btn_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_btn_video_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_btn_vip_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_def_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_episodes_mask.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_episodes_pp_line.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_more_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_program_table_item_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_table_page_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_try_see_order.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_try_see_order2.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_try_see_order_bottom.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_video_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_video_selector2.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_detail_vip_label.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_device_activate_btn.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_device_activate_input.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_device_activate_panel.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_device_check_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_dialog_activation.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_filter_table_page_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_full_vr_player_tip.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_full_window_bottom_mask.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_full_window_top_mask.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_fulldialogclose.png mode change 100755 => 100644 app/src/main/res/drawable/bg_gd.webp mode change 100755 => 100644 app/src/main/res/drawable/bg_gx_no_circle.png mode change 100755 => 100644 app/src/main/res/drawable/bg_his_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_his_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_history_clear_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_history_clear_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_history_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_history_selector1.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_history_selector2.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_history_selector3.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_index_sub_nav.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_item_match_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_list_tag_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_login.webp mode change 100755 => 100644 app/src/main/res/drawable/bg_login_bt.webp mode change 100755 => 100644 app/src/main/res/drawable/bg_login_nber_no.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_login_nber_w.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_login_top.png mode change 100755 => 100644 app/src/main/res/drawable/bg_login_top_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_main_agree_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_match_item_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_media_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_media_list_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_media_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_media_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_order_btn_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_order_center_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_order_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_order_focus_down.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_order_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_order_xufei_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_ordergame_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_orderlist.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_qr_code_card.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_qr_code_dialog_card.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_qr_code_image_rounded.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_search_back_btn_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_search_history_selector.xml create mode 100644 app/src/main/res/drawable/bg_setting_item_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_shap1_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_shap1_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_shap_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_shap_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_shap_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_shap_useragreenment.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_short_video_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_splash.webp mode change 100755 => 100644 app/src/main/res/drawable/bg_suggest_item_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_trysee_open.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_update_tv_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_useragreen_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_video_btn.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_video_key_prompt_tips.xml mode change 100755 => 100644 app/src/main/res/drawable/bg_vr_check_bottom_mask.xml mode change 100755 => 100644 app/src/main/res/drawable/bgc_content_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/bgc_content_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/btn_common_top_selector.xml create mode 100644 app/src/main/res/drawable/btn_home_logout_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/btn_my_back_top_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/btn_my_order_page_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/btn_my_search_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/btn_my_user_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/btn_orderlist.xml mode change 100755 => 100644 app/src/main/res/drawable/btn_to_top_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/btn_user_exit_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/code_bg.webp mode change 100755 => 100644 app/src/main/res/drawable/common_matchtext_bg_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/common_matchtext_bg_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/common_matchtextlive_bg_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/common_notice_bg.xml mode change 100755 => 100644 app/src/main/res/drawable/common_set_btn_bg_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/common_text_bg_cir_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/common_text_bg_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/common_text_bg_round_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/common_text_color_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/common_top_btn_bg_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/cursor_color.xml mode change 100755 => 100644 app/src/main/res/drawable/detail_program_page_divider.xml mode change 100755 => 100644 app/src/main/res/drawable/dialog_order_bg.png mode change 100755 => 100644 app/src/main/res/drawable/downapkbg.png mode change 100755 => 100644 app/src/main/res/drawable/filter_mask_bg.xml mode change 100755 => 100644 app/src/main/res/drawable/gameorder.webp mode change 100755 => 100644 app/src/main/res/drawable/hisroty_bg.webp mode change 100755 => 100644 app/src/main/res/drawable/history_del_focus.webp mode change 100755 => 100644 app/src/main/res/drawable/history_del_nor.png mode change 100755 => 100644 app/src/main/res/drawable/history_delete.png mode change 100755 => 100644 app/src/main/res/drawable/history_no_data.png mode change 100755 => 100644 app/src/main/res/drawable/history_text_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/history_top_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/hotcontent_item_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/ic_detail_item_playing.gif mode change 100755 => 100644 app/src/main/res/drawable/ic_launcher_background.xml mode change 100755 => 100644 app/src/main/res/drawable/icon_common_top_search_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/icon_team_logo_default.xml mode change 100755 => 100644 app/src/main/res/drawable/icon_to_top_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/icon_video_play.png mode change 100755 => 100644 app/src/main/res/drawable/icon_vip.png mode change 100755 => 100644 app/src/main/res/drawable/index_my_history_mask_bg.xml mode change 100755 => 100644 app/src/main/res/drawable/index_my_seekbar_style.xml mode change 100755 => 100644 app/src/main/res/drawable/index_my_user_bg.xml mode change 100755 => 100644 app/src/main/res/drawable/index_nav_item_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/index_set_item_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/indicator_demand_tab.xml mode change 100755 => 100644 app/src/main/res/drawable/indicator_detail_tab.xml mode change 100755 => 100644 app/src/main/res/drawable/keyboard_item_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/loading_player_animation.xml mode change 100755 => 100644 app/src/main/res/drawable/loadingicon.png mode change 100755 => 100644 app/src/main/res/drawable/login_bg.webp mode change 100755 => 100644 app/src/main/res/drawable/login_cancle_bg.png mode change 100755 => 100644 app/src/main/res/drawable/login_code_bg.png mode change 100755 => 100644 app/src/main/res/drawable/login_code_bg_focus.png mode change 100755 => 100644 app/src/main/res/drawable/login_code_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/login_codebtn_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/login_codebtn_focus.png mode change 100755 => 100644 app/src/main/res/drawable/login_codebtn_nor.png mode change 100755 => 100644 app/src/main/res/drawable/login_getcode_btn_bg.webp mode change 100755 => 100644 app/src/main/res/drawable/login_icon_bg.webp mode change 100755 => 100644 app/src/main/res/drawable/login_item_bg_focus.png mode change 100755 => 100644 app/src/main/res/drawable/login_item_bg_nor.png mode change 100755 => 100644 app/src/main/res/drawable/login_line_1.webp mode change 100755 => 100644 app/src/main/res/drawable/login_phone_bg.png mode change 100755 => 100644 app/src/main/res/drawable/login_phone_bg_focus.png mode change 100755 => 100644 app/src/main/res/drawable/login_phone_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/login_show_bg.xml mode change 100755 => 100644 app/src/main/res/drawable/login_tolin_btn_bg.png mode change 100755 => 100644 app/src/main/res/drawable/login_tologin_bg_focus.png mode change 100755 => 100644 app/src/main/res/drawable/login_tologin_bg_selector.xml create mode 100644 app/src/main/res/drawable/logout_text_color_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/match_all_tv_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/match_list_all_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/match_list_all_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/match_list_bg.xml mode change 100755 => 100644 app/src/main/res/drawable/match_list_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/match_list_focus_sport.xml mode change 100755 => 100644 app/src/main/res/drawable/match_list_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/match_list_selector_sport.xml mode change 100755 => 100644 app/src/main/res/drawable/matchlist_no_data_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/matchlist_no_data_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/mb.png mode change 100755 => 100644 app/src/main/res/drawable/no_data_img.png mode change 100755 => 100644 app/src/main/res/drawable/order_bg.webp mode change 100755 => 100644 app/src/main/res/drawable/order_center_keyboader_bg_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/order_center_keyboard_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/order_center_xufei_bg_focus.xml mode change 100755 => 100644 app/src/main/res/drawable/order_center_xufei_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/order_center_xufei_bg_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/order_keyboard_item_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/ordercenter_item_bigfoces_bg.png mode change 100755 => 100644 app/src/main/res/drawable/progress_bar_custom.xml mode change 100755 => 100644 app/src/main/res/drawable/progress_circle.xml mode change 100755 => 100644 app/src/main/res/drawable/progressbar_end.xml mode change 100755 => 100644 app/src/main/res/drawable/progressbar_start.xml mode change 100755 => 100644 app/src/main/res/drawable/progressbar_update_h.xml mode change 100755 => 100644 app/src/main/res/drawable/progressbar_update_other.xml mode change 100755 => 100644 app/src/main/res/drawable/rect_no.png mode change 100755 => 100644 app/src/main/res/drawable/rect_ok.png mode change 100755 => 100644 app/src/main/res/drawable/saomakuang.png mode change 100755 => 100644 app/src/main/res/drawable/saosao.png mode change 100755 => 100644 app/src/main/res/drawable/search_keyboard_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/selector_bg_activate_page_btn.xml mode change 100755 => 100644 app/src/main/res/drawable/selector_btn_update_now.xml mode change 100755 => 100644 app/src/main/res/drawable/selector_txt_update_now.xml mode change 100755 => 100644 app/src/main/res/drawable/switch_keyboard_bg_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/switch_keyboard_bg_unfocus.xml mode change 100755 => 100644 app/src/main/res/drawable/sxtitle.png mode change 100755 => 100644 app/src/main/res/drawable/tryseeoverbg.xml mode change 100755 => 100644 app/src/main/res/drawable/update_tv_selector.xml mode change 100755 => 100644 app/src/main/res/drawable/video_bg_img.xml mode change 100755 => 100644 app/src/main/res/drawable/weilai_btn_error.xml mode change 100755 => 100644 app/src/main/res/drawable/weilai_btn_error_textcolor.xml mode change 100755 => 100644 app/src/main/res/layout-land/device_not_activated_view.xml mode change 100755 => 100644 app/src/main/res/layout/activity_about_my.xml mode change 100755 => 100644 app/src/main/res/layout/activity_device.xml mode change 100755 => 100644 app/src/main/res/layout/activity_filter.xml mode change 100755 => 100644 app/src/main/res/layout/activity_filtermain.xml mode change 100755 => 100644 app/src/main/res/layout/activity_history.xml mode change 100755 => 100644 app/src/main/res/layout/activity_hotel_my.xml mode change 100755 => 100644 app/src/main/res/layout/activity_index.xml mode change 100755 => 100644 app/src/main/res/layout/activity_list.xml mode change 100755 => 100644 app/src/main/res/layout/activity_login.xml mode change 100755 => 100644 app/src/main/res/layout/activity_login_new.xml mode change 100755 => 100644 app/src/main/res/layout/activity_main.xml mode change 100755 => 100644 app/src/main/res/layout/activity_order.xml mode change 100755 => 100644 app/src/main/res/layout/activity_search.xml create mode 100644 app/src/main/res/layout/activity_setting.xml mode change 100755 => 100644 app/src/main/res/layout/activity_test.xml delete mode 100644 app/src/main/res/layout/activity_thirdparty_entry.xml mode change 100755 => 100644 app/src/main/res/layout/activity_video_detail.xml create mode 100644 app/src/main/res/layout/activity_wx_mp_login.xml mode change 100755 => 100644 app/src/main/res/layout/banner.xml mode change 100755 => 100644 app/src/main/res/layout/device_not_activated_view.xml mode change 100755 => 100644 app/src/main/res/layout/dialog_activation_success.xml create mode 100644 app/src/main/res/layout/dialog_benefit_revoked.xml create mode 100644 app/src/main/res/layout/dialog_home_logout_confirm.xml mode change 100755 => 100644 app/src/main/res/layout/dialog_qr_code.xml mode change 100755 => 100644 app/src/main/res/layout/filter_header_layout.xml mode change 100755 => 100644 app/src/main/res/layout/fragment_content.xml create mode 100644 app/src/main/res/layout/fragment_home_my.xml mode change 100755 => 100644 app/src/main/res/layout/fragment_hotel_my.xml mode change 100755 => 100644 app/src/main/res/layout/fragment_my.xml mode change 100755 => 100644 app/src/main/res/layout/fragment_order_center.xml mode change 100755 => 100644 app/src/main/res/layout/fragment_order_duihuan.xml mode change 100755 => 100644 app/src/main/res/layout/fragment_orderlist.xml mode change 100755 => 100644 app/src/main/res/layout/fragment_orderxufei.xml mode change 100755 => 100644 app/src/main/res/layout/fragment_recommend.xml mode change 100755 => 100644 app/src/main/res/layout/fragment_short_video.xml create mode 100644 app/src/main/res/layout/include_hotel_my_buttons.xml mode change 100755 => 100644 app/src/main/res/layout/index_floor_player_error.xml mode change 100755 => 100644 app/src/main/res/layout/index_floor_player_order.xml mode change 100755 => 100644 app/src/main/res/layout/index_floor_player_progress.xml mode change 100755 => 100644 app/src/main/res/layout/index_floor_title_layout.xml mode change 100755 => 100644 app/src/main/res/layout/index_floor_totop.xml mode change 100755 => 100644 app/src/main/res/layout/item_activation_key.xml mode change 100755 => 100644 app/src/main/res/layout/item_d1_list_content.xml mode change 100755 => 100644 app/src/main/res/layout/item_d3_nav.xml mode change 100755 => 100644 app/src/main/res/layout/item_detail_history.xml mode change 100755 => 100644 app/src/main/res/layout/item_detail_program_table_1.xml mode change 100755 => 100644 app/src/main/res/layout/item_detail_program_table_2.xml mode change 100755 => 100644 app/src/main/res/layout/item_detail_program_table_empty_1.xml mode change 100755 => 100644 app/src/main/res/layout/item_detail_program_table_empty_2.xml mode change 100755 => 100644 app/src/main/res/layout/item_detail_recommend.xml mode change 100755 => 100644 app/src/main/res/layout/item_detail_tab_page_text.xml mode change 100755 => 100644 app/src/main/res/layout/item_device_info.xml mode change 100755 => 100644 app/src/main/res/layout/item_device_info_footer.xml mode change 100755 => 100644 app/src/main/res/layout/item_diaplay_type.xml mode change 100755 => 100644 app/src/main/res/layout/item_filter.xml mode change 100755 => 100644 app/src/main/res/layout/item_history.xml mode change 100755 => 100644 app/src/main/res/layout/item_history_text_nav.xml mode change 100755 => 100644 app/src/main/res/layout/item_hot_hcontent.xml mode change 100755 => 100644 app/src/main/res/layout/item_image_txt_hor.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_a49.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_a50.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b1.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b10.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b11.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b12.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b13.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b14.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b15.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b2.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b2_2.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b2_3.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b2_4.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b3.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b4.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b5.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b5_2.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b6.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b7.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b8.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_b9.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_c1.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_c2.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_c3.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_c4.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_c5.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_c6.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_c6_1.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_c7.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_d1.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_d2.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_d2_1.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_d3.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_d4.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_jc1.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_jc32.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_openorclose.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_floor_totop.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_group_item_nav.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_my_collect.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_my_history.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_my_set_more.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_nav.xml mode change 100755 => 100644 app/src/main/res/layout/item_index_nav_group.xml mode change 100755 => 100644 app/src/main/res/layout/item_keyboard.xml mode change 100755 => 100644 app/src/main/res/layout/item_keyboard_button.xml mode change 100755 => 100644 app/src/main/res/layout/item_list_content.xml mode change 100755 => 100644 app/src/main/res/layout/item_list_nav.xml mode change 100755 => 100644 app/src/main/res/layout/item_live_info.xml mode change 100755 => 100644 app/src/main/res/layout/item_match_list.xml mode change 100755 => 100644 app/src/main/res/layout/item_match_list_hor.xml mode change 100755 => 100644 app/src/main/res/layout/item_media_list.xml mode change 100755 => 100644 app/src/main/res/layout/item_order_center.xml mode change 100755 => 100644 app/src/main/res/layout/item_order_center_vip.xml mode change 100755 => 100644 app/src/main/res/layout/item_order_keyboard.xml mode change 100755 => 100644 app/src/main/res/layout/item_orderlist.xml mode change 100755 => 100644 app/src/main/res/layout/item_player_history.xml create mode 100644 app/src/main/res/layout/item_setting_switch.xml create mode 100644 app/src/main/res/layout/item_setting_switch_inner.xml mode change 100755 => 100644 app/src/main/res/layout/item_short_vod.xml mode change 100755 => 100644 app/src/main/res/layout/item_suggest.xml mode change 100755 => 100644 app/src/main/res/layout/item_t9keyboard.xml mode change 100755 => 100644 app/src/main/res/layout/layout_activation_loading_small.xml mode change 100755 => 100644 app/src/main/res/layout/layout_badge_view.xml mode change 100755 => 100644 app/src/main/res/layout/layout_common_loading.xml mode change 100755 => 100644 app/src/main/res/layout/layout_detail_info_popupwindow.xml mode change 100755 => 100644 app/src/main/res/layout/layout_detail_ondemandepisodes_popupwindow.xml mode change 100755 => 100644 app/src/main/res/layout/layout_detail_video_full_black.xml mode change 100755 => 100644 app/src/main/res/layout/layout_detail_video_full_loading.xml mode change 100755 => 100644 app/src/main/res/layout/layout_detail_video_status_err.xml mode change 100755 => 100644 app/src/main/res/layout/layout_detail_video_status_loading.xml mode change 100755 => 100644 app/src/main/res/layout/layout_detail_video_status_noorder_host.xml mode change 100755 => 100644 app/src/main/res/layout/layout_detail_video_status_try_see.xml mode change 100755 => 100644 app/src/main/res/layout/layout_detail_video_status_try_see_end.xml mode change 100755 => 100644 app/src/main/res/layout/layout_dialog_backimg.xml mode change 100755 => 100644 app/src/main/res/layout/layout_dialog_downapk.xml mode change 100755 => 100644 app/src/main/res/layout/layout_dialog_fullimg.xml mode change 100755 => 100644 app/src/main/res/layout/layout_dialog_order.xml mode change 100755 => 100644 app/src/main/res/layout/layout_dialog_order_cancel.xml mode change 100755 => 100644 app/src/main/res/layout/layout_dialog_tryseeover.xml mode change 100755 => 100644 app/src/main/res/layout/layout_dialog_user_exit.xml mode change 100755 => 100644 app/src/main/res/layout/layout_error_prompt.xml mode change 100755 => 100644 app/src/main/res/layout/layout_login_new_qr.xml mode change 100755 => 100644 app/src/main/res/layout/layout_retention_back.xml mode change 100755 => 100644 app/src/main/res/layout/layout_retention_prompt.xml mode change 100755 => 100644 app/src/main/res/layout/layout_video_play_key_prompt_tips.xml mode change 100755 => 100644 app/src/main/res/layout/newtv_view.xml mode change 100755 => 100644 app/src/main/res/layout/pop_t9keyboard.xml mode change 100755 => 100644 app/src/main/res/layout/top_view.xml mode change 100755 => 100644 app/src/main/res/layout/update_dialog.xml mode change 100755 => 100644 app/src/main/res/layout/update_other_dialog.xml mode change 100755 => 100644 app/src/main/res/layout/useragreenment_view.xml mode change 100755 => 100644 app/src/main/res/layout/view_activation_code_input.xml mode change 100755 => 100644 app/src/main/res/layout/view_customer_code.xml delete mode 100644 app/src/main/res/layout/view_device_unregistered.xml mode change 100755 => 100644 app/src/main/res/layout/view_location_code_input.xml mode change 100755 => 100644 app/src/main/res/layout/view_no_data.xml mode change 100755 => 100644 app/src/main/res/layout/view_qr_code.xml mode change 100755 => 100644 app/src/main/res/layout/view_user_agreement.xml mode change 100755 => 100644 app/src/main/res/layout/web_activity_simple.xml mode change 100755 => 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml mode change 100755 => 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/all_game_foc.webp mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/all_game_nor.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_detail_video_try_see.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_history_foc1.webp mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_history_foc2.webp mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_history_foc3.webp mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_history_foc4.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_history_nor1.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_history_nor2.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_history_nor3.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_history_nor4.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_index_my_collect.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_index_my_history.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_item_match_list.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_match_list.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/bg_suggest.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/btn_episode_sec.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/btn_episode_sec_variety.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/calendar1.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/calendar_nor.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/common_notice.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/head_clear.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/head_clear_focus.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/head_delete.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/head_delete_focus.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/home_icon_up_down_foc.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/home_icon_up_down_nor.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_app.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_detail_benefits.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_detail_btn_collect.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_detail_btn_collect_f.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_detail_btn_collect_s.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_detail_btn_order.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_detail_btn_order_s.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_detail_video_from_mg.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_key_prompt_1.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_key_prompt_2.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_key_prompt_3.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_key_prompt_4.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_migu_mini.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/ic_more_foc.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/icon_backward.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/icon_clear.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/icon_forward.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/icon_nonetwork.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/icon_noresult.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/icon_player_error.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/index_my_collect_more.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/index_my_history_more.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/index_my_setting_bg.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/keyboard_bg.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/label_vip.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/my_account_bg.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/my_avatar_default.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/no1.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/no10.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/no2.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/no3.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/no4.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/no5.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/no6.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/no7.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/no8.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/no9.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/nodata.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/player_bg_vip.webp mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/player_load_bg.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/player_load_logo.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/search_tv_line.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/status_icon_search_foc.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/status_icon_search_nor.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/t9_five_bg.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/t9_four_bg.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/t9_two_bg.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/videocolled.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/videofs.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/videohotel.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/videonocoll.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/videoorder.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/videoordered.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/vip.png mode change 100755 => 100644 app/src/main/res/mipmap-hdpi/vs.png mode change 100755 => 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp mode change 100755 => 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp mode change 100755 => 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp mode change 100755 => 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/jst_order_bg.webp mode change 100755 => 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp mode change 100755 => 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp mode change 100755 => 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp mode change 100755 => 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp mode change 100755 => 100644 app/src/main/res/values-night/themes.xml mode change 100755 => 100644 app/src/main/res/values-sw1080dp-land/dimens.xml mode change 100755 => 100644 app/src/main/res/values-sw360dp-land/dimens.xml mode change 100755 => 100644 app/src/main/res/values-sw480dp-land/dimens.xml mode change 100755 => 100644 app/src/main/res/values-sw540dp-land/dimens.xml mode change 100755 => 100644 app/src/main/res/values-sw617dp-land/dimens.xml mode change 100755 => 100644 app/src/main/res/values-sw720dp-land/dimens.xml mode change 100755 => 100644 app/src/main/res/values-sw768dp-land/dimens.xml mode change 100755 => 100644 app/src/main/res/values-sw864dp-land/dimens.xml mode change 100755 => 100644 app/src/main/res/values/attrs.xml mode change 100755 => 100644 app/src/main/res/values/colors.xml mode change 100755 => 100644 app/src/main/res/values/dimens_sw.xml mode change 100755 => 100644 app/src/main/res/values/ids.xml mode change 100755 => 100644 app/src/main/res/values/strings.xml mode change 100755 => 100644 app/src/main/res/values/styles.xml mode change 100755 => 100644 app/src/main/res/values/themes.xml mode change 100755 => 100644 app/src/main/res/xml/device_not_activated_scene.xml mode change 100755 => 100644 app/src/main/res/xml/file_paths.xml mode change 100755 => 100644 app/src/main/res/xml/network_permission_config.xml mode change 100755 => 100644 app/src/ruigeer/res/values/strings.xml mode change 100755 => 100644 app/src/test/java/com/migu/jiecheng/ExampleUnitTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/downapk/StoreVersionUpgraderTest.kt mode change 100755 => 100644 app/src/test/java/com/pukka/jiecheng/index/IndexActivityNavSelectionStateTest.java mode change 100755 => 100644 app/src/test/java/com/pukka/jiecheng/index/adapter/viewholder/NavViewHolderTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/order/adapter/OrderCenterAdapterStructureTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/request/AsApiErrorMapperTest.kt create mode 100644 app/src/test/java/com/pukka/jiecheng/request/AsApiRequesterMacCacheTest.kt create mode 100644 app/src/test/java/com/pukka/jiecheng/utils/JetsenxSdkUtilFallbackAdapterTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/utils/JetsenxSdkUtilFallbackThrottleTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/voice/parser/SkyworthVodParserTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/voice/parser/VendorAParserTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceCallbackResultTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceCommandTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/voice/protocol/VoiceProtocolConstantsTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/voice/queue/CommandQueueTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/voice/router/CommandRouterTest.java create mode 100644 app/src/test/java/com/pukka/jiecheng/voice/throttle/CommandThrottleTest.java delete mode 100644 app/src/thirdparty/res/values/strings.xml mode change 100755 => 100644 build.gradle mode change 100755 => 100644 build_channels.bat mode change 100755 => 100644 config.gradle delete mode 100644 day.code-workspace mode change 100755 => 100644 gradle.properties mode change 100755 => 100644 gradle/wrapper/gradle-wrapper.jar mode change 100755 => 100644 gradle/wrapper/gradle-wrapper.properties mode change 100755 => 100644 gradlew mode change 100755 => 100644 gradlew.bat mode change 100755 => 100644 key/jiecheng.jks mode change 100755 => 100644 key/keystore.properties mode change 100755 => 100644 libcore/.gitignore mode change 100755 => 100644 libcore/build.gradle mode change 100755 => 100644 libcore/consumer-rules.pro mode change 100755 => 100644 libcore/proguard-rules.pro mode change 100755 => 100644 libcore/src/androidTest/java/com/pukka/libcore/ExampleInstrumentedTest.java mode change 100755 => 100644 libcore/src/main/AndroidManifest.xml mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/base/BaseConst.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/base/BaseUrl.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/base/BuildConfig.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/base/DifferenceApi.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/base/DifferenceUrl.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/boxShared/BoxSharedParameters.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/boxShared/BoxSharedParamsUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/boxShared/IpUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/boxShared/MacUtils.java delete mode 100755 libcore/src/main/java/com/pukka/libcore/boxShared/MacUtils2.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/cipher/AesUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/cipher/HMacShaUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/cipher/Md5Utils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/fresco/BaseSimpleDraweeView.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/fresco/FrescoInit.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/fresco/IPkSimpleDraweeView.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/fresco/PkControllerListener.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/fresco/PkSimpleDraweeListener.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/fresco/PkSimpleDraweeView.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/fresco/Tls12SocketFactory.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/fresco/TrustAllManager.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/retrofit/DefaultObserver.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/retrofit/ExceptionReason.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/retrofit/OkHttpClientUtil.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/retrofit/PkApiService.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/retrofit/RetrofitApi.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/retrofit/lowplatform/AndroidPlatform9.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/retrofit/lowplatform/LowPlatformUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/retrofit/lowplatform/OptionalMethod.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/threading/ConcurrentDispatchQueue.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/threading/DispatchQueue.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/threading/DispatchQueueType.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/threading/GuardedRunnable.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/threading/SerialDispatchQueue.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/threading/Timer.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/AppContext.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/AppLifecycleObserver.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/AppManager.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/AppUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/Base64Decoder.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/Base64Util.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/ByteConversionUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/ContextUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/DateUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/GreyingManager.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/JsonUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/ListUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/LogUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/MD5Utils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/MacAddressUtil.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/PackageUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/ProcessUtil.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/ScreenUtil.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/SharedPreferenceUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/SpSaveModel.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/SpTimeUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/StringUtil.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/utils/StringUtils.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/viewmodel/BaseVM.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/widget/AlwaysMarqueeTextView.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/widget/BrowserWebView.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/widget/ContentRecyclerViewTv.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/widget/HorizontalLinearLayoutManagerTv.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/widget/HorizontalRecyclerViewTv.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/widget/TVLinearLayoutManager.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/widget/VerticalGridLayoutManagerTv.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/widget/VerticalLinearLayoutManagerTv.java mode change 100755 => 100644 libcore/src/main/java/com/pukka/libcore/widget/VerticalRecyclerViewTv.java mode change 100755 => 100644 libcore/src/test/java/com/pukka/libcore/ExampleUnitTest.java mode change 100755 => 100644 libdata/.gitignore mode change 100755 => 100644 libdata/build.gradle mode change 100755 => 100644 libdata/consumer-rules.pro mode change 100755 => 100644 libdata/proguard-rules.pro mode change 100755 => 100644 libdata/src/androidTest/java/com/migu/libdata/ExampleInstrumentedTest.java mode change 100755 => 100644 libdata/src/main/AndroidManifest.xml mode change 100755 => 100644 libdata/src/main/java/com/migu/DataActivity.java mode change 100755 => 100644 libdata/src/main/java/com/migu/amber/AmberHeaderBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/amber/AmberUtil.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/AdBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/ApkVersionResponse.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/AspectRatioBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/AuthBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/AuthOrizeBean_shanghai.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/AuthorizeNewBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/BesAuthBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/BesOrderBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/BesPlayUrlBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/CollectBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/CollectReportBean_shanghai.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/ContentListBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/DisplayTypeListBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/EventBusMessage.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetCalenderMatchDate.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetCollectBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetEpgNavBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetFindPlayrecordBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetGameDetailBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetGamePlayurlAndPlayStatusBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetGameSeasonInfoListBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetGameSeasonInfoRecommendBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetLayoutBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetLayoutNavBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetMatchListBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetServiceListBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetShortVideoBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetTopButtonBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetcontentlistBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetlistcollBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GetsystemBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GettagslistBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GoOrderBean_shanghai.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/GoldMainBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/HeartBeatEntity.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/HistoryReportBean_shanghai.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/HljAddCollectionBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/HljAddHistoryBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/IntentBean_Shanghai.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/KYSDKPlayInfoUser.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/KeyboardBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/MediaPlayerHeaderBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/NewtvBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/OTTSSOEntity.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/PassDataBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/PhoneDataBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/PlayHistoryBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/Province.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/RateBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/ReleaseVideoBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/ResumeVideoBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/SearchRecommendBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/SearchResultBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/ShanghaiBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/ShortVideoBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/ShortVideoInfoListBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/ShortVideoLazyBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/ShowOrGoneNavBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/SilenceFullPlay.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/StarInfoIndexBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/StarPlayerInfoBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/SubmitPointsLikesBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/SuggestBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/SupportVoteBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/T9KeyboardBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/TopicVoteInfoBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/UpApkBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/UpApkRequestBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/bean/VideoResultBean.java mode change 100755 => 100644 libdata/src/main/java/com/migu/callback/AijiaCallback.java mode change 100755 => 100644 libdata/src/main/java/com/migu/callback/CommonCallback.java mode change 100755 => 100644 libdata/src/main/java/com/migu/callback/GoOrderCallbackShanghai.java mode change 100755 => 100644 libdata/src/main/java/com/migu/httputils/AndroidPlatform9.java mode change 100755 => 100644 libdata/src/main/java/com/migu/httputils/BaseHttpUtils.java mode change 100755 => 100644 libdata/src/main/java/com/migu/httputils/ContentHttpUtils.java mode change 100755 => 100644 libdata/src/main/java/com/migu/httputils/DataValueUtils.java mode change 100755 => 100644 libdata/src/main/java/com/migu/httputils/HomeHttpUtils.java mode change 100755 => 100644 libdata/src/main/java/com/migu/httputils/LowPlatformUtils.java mode change 100755 => 100644 libdata/src/main/java/com/migu/httputils/MatchHttpUtils.java mode change 100755 => 100644 libdata/src/main/java/com/migu/httputils/OptionalMethod.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/AnHuiApi.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/ApiService.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/GuangDongApi.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HaiNanApi.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HeNanApi.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HeiLongJiangApi.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HttpRequestCallback.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HttpRequestConstants.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HttpRequestFileCallback.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HttpRequestInterceptor.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HttpRetrofit.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HttpRetrofitRequest.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HttpTool.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HttpUtils.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/HttpsTrustManager.kt mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/IHttpRequest.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/NetUtil.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/OkHttpClientUtil.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/OkHttpSsl.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/RetrofitClientUtil.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/RetryFunction.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/RxJavaFileObserver.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/RxJavaObserver.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/SSLSocketFactoryCompat.kt mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/SearchApi.java mode change 100755 => 100644 libdata/src/main/java/com/migu/libnetwork/TrustAllHostnameVerifier.kt mode change 100755 => 100644 libdata/src/main/res/layout/libdata_activity_main.xml mode change 100755 => 100644 libdata/src/main/res/values/colors.xml mode change 100755 => 100644 libdata/src/main/res/values/dimens.xml mode change 100755 => 100644 libdata/src/main/res/values/strings.xml mode change 100755 => 100644 libdata/src/test/java/com/migu/libdata/ExampleUnitTest.java mode change 100755 => 100644 mediaplayertv/.gitignore mode change 100755 => 100644 mediaplayertv/build.gradle mode change 100755 => 100644 mediaplayertv/consumer-rules.pro mode change 100755 => 100644 mediaplayertv/libs/HyMediaPlayer-v1.2.15.aar mode change 100755 => 100644 mediaplayertv/libs/exoplayer-ffmpeg-extension-2.19.1.aar mode change 100755 => 100644 mediaplayertv/proguard-rules.pro mode change 100755 => 100644 mediaplayertv/src/androidTest/java/com/pukka/mediaplayertv/ExampleInstrumentedTest.java mode change 100755 => 100644 mediaplayertv/src/main/AndroidManifest.xml mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/AbstractMediaPlayer.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/AndroidMediaFormat.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/AndroidMediaPlayer.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/AndroidTrackInfo.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/ConfigPlayer.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaController.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaDataSource.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaFormat.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/IMediaPlayer.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/IRenderView.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/ISurfaceTextureHolder.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/ISurfaceTextureHost.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/ITrackInfo.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/MeasureHelper.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/MediaInfo.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/MediaPlayerControl.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/SoftMediaPlayer.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/SurfaceRenderView.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/TvBaseController.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/TvMediaController.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/VideoViewTv.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/ExoMediaSourceHelper.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkExoMediaPlayer.java mode change 100755 => 100644 mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkHighExoMediaPlayer.java mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-hdpi/ic_media_pause.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-hdpi/player_load_bg.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-hdpi/player_load_logo.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading00.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading000.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading01.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading02.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading03.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading04.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading05.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading06.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading07.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading08.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading09.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading10.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading11.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading12.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading13.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading14.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading15.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading16.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading17.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading18.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading19.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading20.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading21.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/loading22.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/progress_loading.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable-xxhdpi/switch_keyboard_bg.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/bg_btn_video_selector.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/bg_control_bottom.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/bg_control_top.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/bg_pause_video_selector.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/bg_white_round_border_normal.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/bg_white_round_border_pressed.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/bg_white_round_border_selector.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/loading_icon.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/loading_player_animation.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/ondemand_seekbar_style.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/player_seek_thumb.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/seekbar_focused_background.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/svg_media_pause_icon.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/svg_media_play_icon.xml mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/videotip1.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/videotip2.png mode change 100755 => 100644 mediaplayertv/src/main/res/drawable/volume_seek_progress.xml mode change 100755 => 100644 mediaplayertv/src/main/res/layout/media_controller_tv.xml mode change 100755 => 100644 mediaplayertv/src/main/res/layout/media_vod_menu_controller.xml mode change 100755 => 100644 mediaplayertv/src/main/res/mipmap-hdpi/dot.png mode change 100755 => 100644 mediaplayertv/src/main/res/mipmap-hdpi/ic_media_play.png mode change 100755 => 100644 mediaplayertv/src/main/res/mipmap-hdpi/icon_backward.png mode change 100755 => 100644 mediaplayertv/src/main/res/mipmap-hdpi/icon_forward.png mode change 100755 => 100644 mediaplayertv/src/main/res/values-sw1080dp-land/dimens.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values-sw360dp-land/dimens.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values-sw480dp-land/dimens.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values-sw540dp-land/dimens.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values-sw617dp-land/dimens.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values-sw720dp-land/dimens.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values/colors.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values/dimens.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values/dimens_sw.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values/h_dimens.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values/strings.xml mode change 100755 => 100644 mediaplayertv/src/main/res/values/w_dimens.xml mode change 100755 => 100644 mediaplayertv/src/test/java/com/pukka/mediaplayertv/ExampleUnitTest.java mode change 100755 => 100644 settings.gradle create mode 100644 test/voice_helper/README.md create mode 100644 test/voice_helper/cold_start.sh create mode 100644 test/voice_helper/control.sh create mode 100644 test/voice_helper/device.sh create mode 100644 test/voice_helper/jump_category.sh create mode 100644 test/voice_helper/jump_film.sh create mode 100644 test/voice_helper/lib/common.sh create mode 100644 test/voice_helper/listen_callback.sh create mode 100644 test/voice_helper/run_all.sh create mode 100644 test/voice_helper/search.sh create mode 100644 test/voice_helper/security_test.sh create mode 100644 test/voice_helper/skyworth_cold_start.sh create mode 100644 test/voice_helper/skyworth_empty_test.sh create mode 100644 test/voice_helper/skyworth_security_test.sh create mode 100644 test/voice_helper/skyworth_vod.sh create mode 100644 test/voice_helper/throttle_test.sh delete mode 100644 盛阳等三方支付EPG传参App开发需求.md diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index c63206c..b83f4fe --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,13 @@ local.properties docs/* .agents/* .trae/* +.worktrees/ skills-lock.json *.log /CLAUDE.md .claude/* +.superpowers/* +.gitattributes +graphify-out/* +.vscode/ +.screenshots/ diff --git a/5.1声道接入成果总结.md b/5.1声道接入成果总结.md new file mode 100644 index 0000000..0605b21 --- /dev/null +++ b/5.1声道接入成果总结.md @@ -0,0 +1,146 @@ +# 捷成 Android TV — 5.1 环绕声道接入成果总结 + +> 版本基线:v1.3.33(aishiRemoteRelease)|验证设备:Amlogic Android 14 盒子(172.16.100.175,曾用 184) +> 验证结论:**AC3 5.1 比特流经 HDMI 直通输出成功,解码器面板显示 AC3** + +--- + +## 一、目标与结果 + +| 目标 | 结果 | +|---|---| +| 接入橘视厅 5.1 环绕声业务渠道,拉取 5.1 片库内容 | 完成 | +| 播放器识别并优先播放 5.1 声道片源 | 完成 | +| 5.1 音频经 HDMI 输出到外接解码器/功放 | 完成(AC3 源码直通,非 LPCM 下混) | + +全链路形态: + +``` +EPG 点播 → SDK 鉴权(getVideoUrls) → 识别"1080P 5.1 环绕声"独立档 +→ H265(硬解) + AC3(平台直通/FFmpeg 软解兜底) +→ HDMI 输出:直通设备=AC3 bitstream(RAW_DIRECT);非直通设备=6ch LPCM(FFmpeg 软解) +``` + +## 二、渠道参数(橘视厅5.1环绕声业务) + +| 参数 | 值 | +|---|---| +| tenant | huashitv | +| accesser | JSTAS51 | +| secret | oam25ZqvGy3gVY0x | +| agcode | 1100101010200492358120169 | +| comboCode(业务组合) | 1100121044377892530980001 | +| serviceGroupCode | 1100122010201752209710182 | +| businessLine(业务线) | 4100450044057893707260002 | +| productCode(产品包) | 2100130010166202685000333 | +| 免登录公钥 | debug/release 共用同一把(见 ChannelManger) | + +## 三、App 端代码改动 + +### 3.1 渠道参数全局替换(运行时 + 默认值三处同步) + +- [ChannelManger.java L63-L73](app/src/main/java/com/pukka/jiecheng/utils/ChannelManger.java#L63-L73):`initChannel()` 运行时唯一生效入口(MainActivity.onCreate 调用),ACCESSER/SECRET/业务组合/业务线/公钥全部替换为 5.1 业务参数 +- [BaseConst.java L18-L24](libcore/src/main/java/com/pukka/libcore/base/BaseConst.java#L18-L24):四个码的静态默认值同步(MyApplication 阶段请求使用) +- [JetsenxSdkUtil.java L36-L39](app/src/main/java/com/pukka/jiecheng/utils/JetsenxSdkUtil.java#L36-L39):AGCODE/ACCESSER/SECRET 默认值同步 + +### 3.2 产品包直连(跳过动态取包) + +[JetsenxSdkUtil.java L49-L53](app/src/main/java/com/pukka/jiecheng/utils/JetsenxSdkUtil.java#L49-L53):`productCodeSp` 直接初始化为官方产品包 `2100130010166202685000333`,跳过 `getAllAssetPrices` 动态取包(避免多产品包场景取错包导致 -110 未订购)。 + +### 3.3 5.1 片源自动优先(核心逻辑) + +[JetsenxSdkUtil.java](app/src/main/java/com/pukka/jiecheng/utils/JetsenxSdkUtil.java) 的 `getVideoUrls2()`(L785-L804)和 `getFreeVideoUrls2()`(L850-L869)两处,在 720/1080 解析循环后追加 5.1 覆盖块: + +```java +// 5.1 环绕声独立档(format='1080P 5.1 环绕声',H265+AC3_51):自动优先策略下作为 1080 档播放地址。 +// Android 5.1(API 22)及以下解码能力有限,跳过 5.1 流,保留普通 1080P。 +if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { + for (VideoInfo info : videoInfos) { + String fmt = info.getFormat() == null ? "" : info.getFormat(); + if (fmt.contains("5.1") || fmt.contains("环绕声")) { + videoUrl_1080 = info.getVideoUrl(); + // PlayCodeBean 同步 movieCode/programCode/specCode → 播放记录上报正确 + ... + break; + } + } +} +``` + +设计要点: +- **最小侵入**:复用现有 1080 档通道,档位管理(VideoDetailStatusManager)/播放调用点零改动 +- 5.1 流的 movieCode/programCode/specCode 同步写入 PlayRecordManager,上报与播控正确 +- API 22 及以下自动降级普通 1080P(无软解能力兜底) + +### 3.4 播放器渲染器策略:PREFER → ON(关键决策) + +[PkExoMediaPlayer.java L110](mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkExoMediaPlayer.java#L110): + +```java +// v1.3.32: EXTENSION_RENDERER_MODE_PREFER → FFmpeg 渲染器全量优先(含视频) +// v1.3.33: EXTENSION_RENDERER_MODE_ON → 平台渲染器优先,FFmpeg 仅兜底 +``` + +原因: +1. **直通需求**:`PREFER` 下 FFmpeg 音频渲染器优先抢走 AC3 → 永远软解为 PCM,AudioTrack 永远不会以 `ENCODING_AC3` 输出,HDMI 源码直通不可达 +2. **`ON` 的行为**:平台 MediaCodec 渲染器优先。设备开编码环绕声(ALWAYS)时 ExoPlayer 检测到 AC3 输出能力 → 直通;未开启/不支持时走平台解码或回落 FFmpeg 软解 +3. **额外收益**:H265 视频从 FFmpeg 软解回到平台硬解 + +### 3.5 FFmpeg 扩展依赖(软解兜底) + +[app/build.gradle L314-L318](app/build.gradle#L314-L318):`exoplayer-ffmpeg-extension-2.19.1.aar`(定制编译,含 AC3/EAC3/DCA/TrueHD 解码器,官方默认版不含)。用于不支持直通的设备软解 AC3 → 6ch LPCM 输出。 + +### 3.6 其他 + +- versionName → v1.3.33;签名路径改为相对路径 `key/jiecheng.jks` +- 打包命令:`sh ./gradlew assembleAishiRemoteRelease`(遥控版)/ `assembleAishiTouchRelease`(触屏版) + +## 四、5.1 片源数据形态 + +- **独立 URL 档位**(非同流多音轨):同一影片多档清晰度中有一档 `format='1080P 5.1 环绕声'` +- 该档位:H265 视频 + AC3 5.1(AC3_51)音频 +- **EPG/SDK 均无 5.1 元数据字段**,唯一标识是 `VideoInfo.format` 字符串 → UI 角标需依赖此字段(待捷成侧加字段可优化) + +## 五、真机验证结论(Amlogic Android 14) + +| 版本 | 渲染策略 | audio_flinger 实测 | 效果 | +|---|---|---|---| +| v1.3.32 | FFmpeg PREFER | `HAL: PCM_16_BIT, 6ch, usecase: PCM_DIRECT` | 软解 6ch LPCM 输出 HDMI,有声 | +| v1.3.33 | 平台优先 ON | `HAL: AUDIO_FORMAT_AC3, usecase: RAW_DIRECT, DIRECT 线程` | **AC3 比特流直通,面板显示 AC3** | + +验证命令:`adb shell dumpsys media.audio_flinger`,关注 DIRECT 输出线程的 `HAL format` 与 `usecase`。 + +## 六、系统侧音频配置要点(测试设备部署须知) + +AC3 直通需系统侧两项配置(设备 root 后操作): + +```bash +# 1. 开启编码环绕声输出(Amlogic 固件实测:2 是陷阱值,正确部署值 0/3,见下方链路关系) +adb shell settings put global encoded_surround_output 0 +# 生效标志:dumpsys media.audio_flinger | grep digital_audio_mode → AUTO + +# 2. 重启 App(ExoPlayer 在播放器实例化时读取系统音频能力) +``` + +链路关系(Amlogic 固件实测修正): +- **部署用 `0 (AUTO)` 或 `3 (MANUAL)`** → HAL `digital_audio_mode` 为 **AUTO** → AC3 流走 IEC RAW 直通 +- `encoded_surround_output=1 (NEVER)` → HAL 被下发 **PCM** 模式 → 直通不可用(App 注入条件已排除 NEVER,安全回落软解 2.0,不无声) +- **`encoded_surround_output=2 (ALWAYS)` 在该 Amlogic 固件是陷阱值**:policy 照常向 App 上报 AC3 能力,但 HAL 被 SettingsObserver 切到 PCM,两层矛盾表现为无声/静默回退。本文档早前"`put ... 2`"的建议**作废**,详见《5.1声道改造-代码变更清单与复用指南.md》§2.3 + +设备伪装(换机测试不重新开通):root 写 `shared_prefs/config.xml` 的 `ETH_MAC` 键 → App 用伪装 MAC 请求沿用 uid/订购。 + +## 七、遗留问题与后续规划 + +### 7.1 已知问题 + +1. **开机重置**:设备重启后有系统组件将 `encoded_surround_output` 改回 1(NEVER),导致 HAL 变 PCM、直通失效,需重新设置并重启 App。待排查改写来源(疑似 Amlogic 设置应用/固件默认逻辑),部署设备需固化该配置 +2. **生效时序**:运行时改 `encoded_surround_output` 后,HAL 的 `digital_audio_mode` 由 audio policy 在下次输出重配置时才下发 AUTO(实测延迟数分钟/需触发),改完设置应重启 App 并重新进播放 + +### 7.2 后续待办 + +| # | 事项 | 说明 | +|---|---|---| +| 1 | **设备能力检测** | App 启动/播放前检测:AC3 直通能力(AudioCapabilities/编码环绕声设置)+ 解码能力,动态决策 直通/软解/降级,并可在 UI 提示当前音频输出模式 | +| 2 | **5.1 解码开关按钮** | 设置项或播放页开关:开启=优先 5.1 档,关闭=普通 1080P(给无环绕设备的用户退出手段) | +| 3 | **5.1 节目标识** | 详情页/海报角标"5.1",基于 `VideoInfo.format` 含 "5.1"/"环绕声" 判断;EPG 无字段,可推动捷成侧补元数据 | +| 4 | 清晰度菜单区分 | 档位名显示"1080P 环绕声"替代直接替换 1080 档(当前 5.1 档覆盖 1080 档位) | diff --git a/5.1声道改造-代码变更清单与复用指南.md b/5.1声道改造-代码变更清单与复用指南.md new file mode 100644 index 0000000..c01b207 --- /dev/null +++ b/5.1声道改造-代码变更清单与复用指南.md @@ -0,0 +1,585 @@ +# 5.1 声道改造 — 代码变更清单与复用指南 + +> 用途:当前验证代码直接改在 main 上(v1.3.32 → v1.3.35),后续需从 main 单独建 5.1 渠道分支重新套用本文档的变更。 +> 验证设备:Amlogic Android 14 盒子(172.16.100.166,曾用 184/175),外接功放 HDMI。 +> 验证版本:v1.3.35 aishiRemoteRelease,已确认 5.1/2.0 切换、AC3 直通正常。 + +--- + +## 0. 结论速览 + +| 项 | 结论 | +|---|---| +| 播放策略 | 三级:AC3 源码直通 > 平台硬解(6ch LPCM) > FFmpeg 软解(下混 2.0);不支持直通时回退普通 1080P AAC 档 | +| 渲染器模式 | `EXTENSION_RENDERER_MODE_ON`(平台优先,FFmpeg 仅兜底)——**不能用 PREFER**(会全量软解,直通不可达) | +| 系统设置 | 该固件 `encoded_surround_output` 必须设 **0 (AUTO)**,**不能设 2 (ALWAYS)**(详见 §3,这是本次最大的坑) | +| 切换生效 | 切 5.1 开关后无需杀 app,重新起播(换集/退出播放页重进)即生效;播放中不热切(5.1 档与 2.0 档是两条 CDN 流) | +| CDN 结构 | 档位因片而异。新片:720P×2 + 1080P AAC 2.0 + 1080P H265 AC3 5.1;老片可能只有 720P + 5.1 档 | +| 5.1 流形态 | 单 AC3 音轨、**无 2.0 兜底音轨**,不支持直通时必须整条换流(不可同流切音轨) | +| 注入原理 | **v1.3.36 关键修复**:`DefaultAudioSink.Builder()` 必须**无参构造**——传 context 时首次 `getFormatSupport()` 会懒注册 AudioCapabilitiesReceiver 用系统 caps **覆盖注入值**(反编译 2.19.1 字节码确认),v1.3.35 的注入从未生效。随 v1.3.35 版本号发布 | +| 直通降级 | 注入会话中音频初始化类错误(1000/4001/5001)自动触发:进程级禁用注入 + 重建播放器软解重试(保留进度,一次为限),播放不中断 | +| 音量边界 | 直通后 App 无法干预响度(raw 流不解码不混音,响度由接收端解码决定),与软解路径间存在感知音量差——直通固有特性,非缺陷 | + +--- + +## 1. 代码变更清单(10 个文件) + +### 1.1 [新增] `app/src/main/java/com/pukka/jiecheng/utils/Surround51Manager.java` + +5.1 能力检测与播放开关的唯一入口。 + +```java +package com.pukka.jiecheng.utils; + +import android.content.Context; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.os.Build; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.pukka.libcore.utils.LogUtils; +import com.pukka.libcore.utils.SharedPreferenceUtils; + +/** + * 5.1 环绕声能力检测与播放开关。 + *

开关(默认开启)关闭时不选 5.1 独立档(H265+AC3_51),播放普通 2.0 档; + * 开启时按设备能力自动选择输出路径:AC3直通(HDMI bitstream) > 平台硬解(6ch LPCM) > FFmpeg软解(下混 2.0)。

+ *

CDN 侧 5.1 档为单 AC3 音轨流(无 2.0 兜底音轨),故能力不足或开关关闭时必须回退普通档 URL。

+ */ +public final class Surround51Manager { + private static final String TAG = "Surround51Manager"; + /** SP 键:5.1 环绕声播放开关(设备信息页可切换) */ + private static final String SP_KEY_51_ENABLED = "Surround51Enabled"; + + private static volatile Boolean sAc3Passthrough; + private static volatile Boolean sPlatformAc3Decoder; + + private Surround51Manager() { + } + + /** 5.1 环绕声播放开关,默认开启 */ + public static boolean is51Enabled() { + return SharedPreferenceUtils.getBoolean(SP_KEY_51_ENABLED, true); + } + + public static void set51Enabled(boolean enabled) { + SharedPreferenceUtils.put(SP_KEY_51_ENABLED, enabled); + clearCache(); + } + + /** 清除静态缓存(开关切换后需重新检测) */ + public static void clearCache() { + sAc3Passthrough = null; + sPlatformAc3Decoder = null; + } + + /** + * 是否优先选用 5.1 独立档:开关开启 且 Android 5.1(API 22) 以上。 + * API 22 及以下设备解码能力有限,固定走普通 2.0 档。 + */ + public static boolean is51Preferred() { + return is51Enabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1; + } + + /** 判断 SDK 返回档位是否 5.1 环绕声档(format='1080P 5.1 环绕声') */ + public static boolean isSurroundFormat(String format) { + String fmt = format == null ? "" : format; + return fmt.contains("5.1") || fmt.contains("环绕声"); + } + + /** + * 设备是否支持 AC3 源码直通(HDMI/SPDIF bitstream)。 + * 结果受系统编码环绕声设置(encoded_surround_output)与当前接入的音频设备能力影响。 + */ + public static boolean supportsAc3Passthrough(Context context) { + if (sAc3Passthrough == null) { + try { + sAc3Passthrough = AudioCapabilities.getCapabilities(context) + .supportsEncoding(C.ENCODING_AC3); + } catch (Throwable e) { + LogUtils.d(TAG, "supportsAc3Passthrough err:" + e); + sAc3Passthrough = false; + } + } + return sAc3Passthrough; + } + + /** 平台是否自带 AC3 硬解码器(如带 Dolby/MS12 授权的芯片),硬解输出 6ch LPCM 由系统按输出能力下混 */ + public static boolean hasPlatformAc3Decoder() { + if (sPlatformAc3Decoder == null) { + boolean found = false; + try { + for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) { + // v1.3.36: isAlias() 是 API 29+ 方法,API 28 及以下调用抛 NoSuchMethodError + //(腾讯T5/Android 9 实测),会中断整个循环导致检测永远为"无解码器" + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && info.isAlias()) + || info.isEncoder()) { + continue; + } + for (String type : info.getSupportedTypes()) { + if ("audio/ac3".equalsIgnoreCase(type)) { + found = true; + break; + } + } + if (found) { + break; + } + } + } catch (Throwable e) { + LogUtils.d(TAG, "hasPlatformAc3Decoder err:" + e); + found = false; + } + sPlatformAc3Decoder = found; + } + return sPlatformAc3Decoder; + } + + /** 当前设备 5.1 音频输出路径描述(设备信息页展示用) */ + public static String getAudioPathDesc(Context context) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { + return "不支持(系统版本过低)"; + } + if (supportsAc3Passthrough(context)) { + return "AC3源码直通"; + } + if (hasPlatformAc3Decoder()) { + return "硬解码"; + } + return "软解"; + } +} +``` + +注意事项: +- `MediaCodecList` 遍历用 `new MediaCodecList(MediaCodecList.ALL_CODECS)` 构造(API 21+);`MediaCodecList.CodecInfo` 不是内部类,正确类型是独立的 `android.media.MediaCodecInfo`。 +- `mediaplayertv` 模块不能依赖此类的选流方法(app 模块),但能力检测逻辑被 §1.2 以直接读 SP + 内联方式复用。 + +### 1.2 [修改] `mediaplayertv/src/main/java/com/pukka/videoplayertv/exoplayer/PkExoMediaPlayer.java` + +**改动 1(v1.3.33)**:`initPlayer()` 中渲染器模式 `EXTENSION_RENDERER_MODE_PREFER` → `EXTENSION_RENDERER_MODE_ON`。原因:PREFER 让 FFmpeg 渲染器全量优先(含 AC3 软解为 PCM),直通永远不可达;ON 平台优先、FFmpeg 仅兜底。 + +**改动 2(v1.3.35)**:`initPlayer()` 中 `DefaultRenderersFactory` 改为匿名子类,重写 `buildAudioSink()`,在系统未报告 AC3 但 5.1 开关开启且系统未禁止环绕声时,强制注入含 AC3 的 `AudioCapabilities`。新增 import: + +```java +import android.provider.Settings; +import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.google.android.exoplayer2.audio.AudioSink; +import com.google.android.exoplayer2.audio.DefaultAudioSink; +import com.pukka.libcore.utils.SharedPreferenceUtils; +``` + +`initPlayer()` 中创建播放器的完整替换代码: + +```java +// 增加缓冲区大小 +DefaultLoadControl mLoadControl = new DefaultLoadControl.Builder() + .setBufferDurationsMs(50000, 50000, 1500, 2000) + .build(); +if (mRenderersFactory == null) { + mRenderersFactory = new DefaultRenderersFactory(mAppContext) { + @Override + protected AudioSink buildAudioSink(Context context, boolean enableFloatOutput, + boolean enableAudioTrackPlaybackParams, boolean enableOffload) { + AudioCapabilities caps = AudioCapabilities.getCapabilities(context); + boolean ac3Supported = caps.supportsEncoding(C.ENCODING_AC3); + // 5.1 开关开启且系统未禁止环绕声时,强制注入 AC3 直通能力。 + // 修复:HAL 从 PCM 切回 AUTO 有延迟,getDirectPlaybackSupportedEncodings() + // 可能返回过时结果,导致 ExoPlayer 不尝试 AC3 直通。 + if (!ac3Supported) { + boolean surroundEnabled = SharedPreferenceUtils.getBoolean("Surround51Enabled", true); + if (surroundEnabled) { + int encodedSurround = Settings.Global.getInt( + context.getContentResolver(), "encoded_surround_output", 0); + // encoded_surround_output: 0=AUTO, 1=NEVER, 2=ALWAYS + if (encodedSurround != 1) { + LogUtils.d(TAG, "buildAudioSink: 系统未报告AC3,强制注入。" + + " encodedSurround=" + encodedSurround); + caps = new AudioCapabilities( + new int[]{C.ENCODING_PCM_16BIT, C.ENCODING_AC3}, 8); + } + } + } else { + LogUtils.d(TAG, "buildAudioSink: 系统已报告AC3直通能力"); + } + return new DefaultAudioSink.Builder(context) + .setAudioCapabilities(caps) + .setEnableFloatOutput(enableFloatOutput) + .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) + .setOffloadMode(enableOffload + ? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED + : DefaultAudioSink.OFFLOAD_MODE_DISABLED) + .build(); + } + }.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON); +} +ExoPlayer mInternalPlayer = new ExoPlayer.Builder( + mAppContext, + mRenderersFactory, + new DefaultMediaSourceFactory(mAppContext), + mTrackSelector == null ? mTrackSelector = new DefaultTrackSelector(mAppContext) : mTrackSelector, + mLoadControl, + DefaultBandwidthMeter.getSingletonInstance(mAppContext), + new DefaultAnalyticsCollector(Clock.DEFAULT)) + .build(); +``` + +**改动 3(v1.3.36,随 v1.3.35 版本号发布:修复注入失效 + 自动降级重试)** + +字段(类顶部新增): + +```java +/** 进程级禁用 AC3 caps 注入:直通初始化失败自动降级后置位,本进程内不再注入(设备级失败,重试无意义) */ +private static volatile boolean sAc3InjectionDisabled = false; +/** 当前播放器实例的 AudioSink 是否注入了 AC3 caps(onPlayerError 降级判定用) */ +private volatile boolean mAc3Injected = false; +``` + +`buildAudioSink()` 相对改动 2 的变化:注入条件追加 `!sAc3InjectionDisabled`;注入成功置 `mAc3Injected = true`;三分支日志(已报告/注入/注入已禁用);**`new DefaultAudioSink.Builder(context)` 改为无参 `new DefaultAudioSink.Builder()`(关键修复)**: + +```java +if (!ac3Supported && !sAc3InjectionDisabled) { + boolean surroundEnabled = SharedPreferenceUtils.getBoolean("Surround51Enabled", true); + if (surroundEnabled) { + int encodedSurround = Settings.Global.getInt( + context.getContentResolver(), "encoded_surround_output", 0); + if (encodedSurround != 1) { + LogUtils.d(TAG, "buildAudioSink: 系统未报告AC3,强制注入。" + + " encodedSurround=" + encodedSurround); + caps = new AudioCapabilities( + new int[]{C.ENCODING_PCM_16BIT, C.ENCODING_AC3}, 8); + mAc3Injected = true; + } + } +} else if (ac3Supported) { + LogUtils.d(TAG, "buildAudioSink: 系统已报告AC3直通能力"); +} else { + LogUtils.d(TAG, "buildAudioSink: 注入已禁用(本进程曾直通失败),走系统 caps"); +} +return new DefaultAudioSink.Builder() // 无参!传 context 会导致注入被系统 caps 覆盖 + .setAudioCapabilities(caps) + .setEnableFloatOutput(enableFloatOutput) + .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) + .setOffloadMode(enableOffload + ? DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_NOT_REQUIRED + : DefaultAudioSink.OFFLOAD_MODE_DISABLED) + .build(); +``` + +`onPlayerError()` 拦截音频初始化类错误自动降级,`degradeAndRetry()` 重建播放器(核心步骤): + +```java +// onPlayerError 中(notifyOnError 之前): +if (mAc3Injected && !sAc3InjectionDisabled) { + int code = error.errorCode; + if (code == PlaybackException.ERROR_CODE_UNSPECIFIED // 1000 + || code == PlaybackException.ERROR_CODE_DECODER_INIT_FAILED // 4001 + || code == PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED) { // 5001 + LogUtils.d(TAG, "onPlayerError: 注入会话音频初始化失败(code=" + code + + "),自动降级为软解重试"); + sAc3InjectionDisabled = true; // 进程级一次降级,防循环 + degradeAndRetry(error); + return; + } +} + +// degradeAndRetry 核心:记录进度 → 主线程移除监听并 release → +// mRenderersFactory = null(下次 initPlayer 重建 sink,因 sAc3InjectionDisabled=true 不再注入)→ +// mAc3Injected = false → initPlayer()/setWakeMode/setOptions/initMediaListener → +// setMediaSource(mMediaSource)/prepare()/seekTo(resumePos);任意步骤异常才上抛原错误。 +``` + +技术依据(反编译 2.19.1 字节码确认): +- **注入失效根因**:`DefaultAudioSink` 私有 `getAudioCapabilities()` 在 `receiver==null && context!=null` 时懒创建 `AudioCapabilitiesReceiver` 并 `register()`,用**系统实时 caps 直接覆盖 `audioCapabilities` 字段**。`Builder(context)` 传 context → 注入值只在构造到首次 `getFormatSupport()` 之间短暂存在,从未参与格式判定;**无参 `Builder()`(context=null)注入才持续生效**。 +- `AudioCapabilities.getCapabilities()` **无静态缓存**,每次实时查询;API 29+ TV 走 `AudioManager.getDirectPlaybackSupportedEncodings()`,该查询反映 audio policy/HAL 当前运行时状态,在 HAL 模式切换期间会返回过时结果。 +- **`PlaybackException` 位置与命名坑**:类在 **exoplayer-common-2.19.1.aar**(2.18 起模块化拆分,不在 exoplayer-core 的 classes.jar 根包);错误码常量命名是 `ERROR_CODE_*`(**非 `ERRORCODE_*`,编译期找不到符号的坑**)。白名单只收音频初始化类错误(1000/4001/5001),防误降级。 +- `MediaCodecAudioRenderer` 直通准入 gate(字节码):`audioSink.supportsFormat(format) && MediaCodecUtil.getDecryptOnlyDecoderInfo() != null`(后者查 `audio/raw` decoder)——注入后有 raw decoder 但 policy 无 DIRECT 输出的设备(如部分 MTK)会在 `AudioTrack(ENCODING_AC3)` 初始化时硬失败,这正是降级重试要防的场景。 +- `DefaultRenderersFactory.buildAudioSink()` 是 protected,可被子类重写;`DefaultAudioSink.Builder.setAudioCapabilities()` 是官方注入点。 +- SP 键 `"Surround51Enabled"` 与 app 模块 `Surround51Manager.SP_KEY_51_ENABLED` 保持一致(mediaplayertv 依赖 libcore,可直接用 `SharedPreferenceUtils`,两模块读同一 `config.xml`)。 + +### 1.3 [修改] `app/src/main/java/com/pukka/jiecheng/utils/JetsenxSdkUtil.java` + +`getVideoUrls2()`(约 L776)与 `getFreeVideoUrls2()`(约 L841)**两处相同改动**: + +1. 主循环普通 1080P 匹配排除 5.1 档: + +```java +}else if (info.getFormat().contains("1080") + &&TextUtils.isEmpty(videoUrl_1080) + &&!Surround51Manager.isSurroundFormat(info.getFormat())){ + // 普通 1080P 档(排除 5.1 档) +``` + +2. SDK 回调尾部新增 5.1 覆盖块(原条件 `Build.VERSION.SDK_INT > LOLLIPOP_MR1` 改为开关判断): + +```java +if (Surround51Manager.is51Preferred()) { + for (VideoInfo info : videoInfos) { + if (Surround51Manager.isSurroundFormat(info.getFormat())) { + videoUrl_1080 = info.getVideoUrl(); + // PlayCodeBean 取码同步覆盖(照原 1080 分支写法) + break; + } + } +} +``` + +日志锚点:`videoUrl_51 applied:`(排障用)。 + +### 1.4 [修改] `app/src/main/java/com/pukka/jiecheng/video/utils/VideoDetailStatusManager.java` + +**改动 1**:`getDefinitionUrl()` 恒选 1080 档(约 L446): + +```java +public static String getDefinitionUrl(String videoUrl_1080, String videoUrl_720) { + if (!TextUtils.isEmpty(videoUrl_1080)) { + return videoUrl_1080; + } + return videoUrl_720; +} +``` + +**改动 2**:`setData()` 与 `changeData()` 保持"先释放旧播放器再 setVideoPath"链路(`videoViewRelease()` + `releaseError(true)` → `setVideoPath`),否则 `openVideo()` 里 `mMediaPlayer != null` 直接 return,新 URL 不生效。 + +### 1.5 [修改] `app/src/main/java/com/pukka/jiecheng/video/VideoDetailActivity.java` + +删除 `onDestroy()` 中强制重置清晰度到 720/1080 的逻辑(约 L3014-3020)。原因:与"5.1 档占用 1080 位置"的新模型冲突。 + +### 1.6 [修改] `app/src/main/java/com/pukka/jiecheng/video/manger/ResolutionManger.java` + +`initResolution()`(约 L87)恒隐藏 720P 选项: + +```java +public void initResolution() { + tv720.setVisibility(View.GONE); + tv1080.setVisibility(videoDetailStatusManager.isHave1080()?View.VISIBLE:View.GONE); + // ... +} +``` + +### 1.7 [修改] `app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordManager.java` + +上报取码从读 SP `"Definition"` 改为跟随**实际播放档位**,消除 NPE 风险与错报: + +```java +private PlayCodeBean getCurPlayCode() { + int def = videoDetailStatusManager != null ? videoDetailStatusManager.getCurResolutionUrl() : 0; + // def 优先当前档位 → 回落 1080 → 回落 720(按原 PlayCodeBean 组装逻辑) +} +``` + +`getCurResolutionUrl()` 在 `VideoDetailStatusManager` 中实现(读 `mVideoView.getUrl()` 与 1080/720 URL 比对,返回实际档位值)。 + +### 1.8 [修改] `app/src/main/java/com/pukka/jiecheng/device/DeviceActivity.kt` + +设备信息页新增音频能力显示行 + 5.1 开关行(入口:关于我们 → 长按"我"字 3 秒 → 设备信息页): + +```kotlin +// 新增字段 +var mTvAudioPath: TextView? = null +var mLlSurround51: LinearLayout? = null +var mTvSurround51Value: TextView? = null + +// initView() 中 +mTvAudioPath = findViewById(R.id.id_tv_audio_path) +mTvAudioPath!!.text = Surround51Manager.getAudioPathDesc(this) +mLlSurround51 = findViewById(R.id.id_ll_surround51) +mTvSurround51Value = findViewById(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 声道" + ) +} + +private fun updateSurround51Value() { + mTvSurround51Value?.text = if (Surround51Manager.is51Enabled()) "开" else "关" +} +``` + +### 1.9 [修改] `app/src/main/res/layout/activity_device.xml` + +在会员到期行后新增两行:音频能力行(`id_tv_audio_path`)+ 5.1 开关行(`id_ll_surround51` / `id_tv_surround51_value`),开关行带焦点背景 `bg_setting_item_selector`。 + +### 1.10 [新增] `app/src/main/res/drawable/bg_setting_item_selector.xml` + +焦点态半透明白色背景 selector。 + +### 1.11 [修改] `app/build.gradle` + +`versionName` 递增(本次验证链:v1.3.32 → v1.3.33 → v1.3.34 → v1.3.35)。 +依赖(已有,勿动): + +```groovy +implementation 'com.google.android.exoplayer:exoplayer:2.19.1' +implementation files('libs/exoplayer-ffmpeg-extension-2.19.1.aar') +``` + +`mediaplayertv/build.gradle` 同版本 exoplayer 2.19.1,且依赖 `project(':libcore')`。 + +--- + +## 2. 排障经验(可直接复用的方法论) + +### 2.1 问题一:"5.1 关掉后再打开不生效"(v1.3.34 现象:再开只有 2.0) + +排查链(每步均有证据支撑): + +| 假设 | 结论 | 证据 | +|---|---|---| +| ExoPlayer AudioCapabilities 静态缓存 | 不存在 | 反编译 2.19.1:`getCapabilities()` 每次实时查询 | +| ExoPlayer 实例未重建 | 正常重建 | `changeData()` → `videoViewRelease()` → `openVideo()` → `createPlayer()` 每次新建 | +| URL 选流错误 | 正常 | logcat `videoUrl_51 applied` 在开关后仍出现 | +| `encoded_surround_output` 被重置 | 值仍为 2 | `adb shell settings get global encoded_surround_output` | +| HAL 模式未切换 | **根因** | `dumpsys media.audio_flinger` 显示 `digital_audio_mode: PCM` | + +### 2.2 问题二:v1.3.35 现象升级为"完全无声" + +关键证据: + +``` +# HAL 日志(logcat) +audio_hw_hal_stream: [get_sink_format] format:AC3(0x9000000) digital_mode(PCM) sink cap:DOLBY_TRUEHD +audio_hw_hal_primary: [config_output] hal_internal_format:AC3 dolby_lib_type:1 reset_decoder:1 rate=48000 ch=6 + +# audio_flinger dump:DIRECT 线程有 AC3 流、Frames written 在涨,但全局 digital_audio_mode=PCM +``` + +解释:policy 层(ALWAYS)向 app 报告 AC3 能力 → ExoPlayer 走直通 → AC3 raw 流进 DIRECT 线程;但 HAL 全局 `digital_audio_mode=PCM` → HDMI 物理层按 PCM 输出 → 功放收到无法解码的数据 → 无声。 + +### 2.3 最终根因(本次最重要的经验) + +该 Amlogic 固件 SettingsObserver 对 `encoded_surround_output` 的映射(实验实测矩阵): + +| encoded_surround_output | HAL digital_audio_mode | 直通可用 | +|---|---|---| +| **0 (AUTO)** | **AUTO** | **是(正确部署值)** | +| 3 (MANUAL,Amlogic 系统设置里的"手动") | AUTO | 是 | +| 1 (NEVER) | PCM | 否 | +| **2 (ALWAYS,AOSP 标准"总是")** | **PCM** | **否(致命陷阱)** | + +**ALWAYS=2 时 policy 照常向 app 报告 AC3 能力,但 HAL 被 SettingsObserver 切到 PCM** —— 两层矛盾,表现为无声或静默回退 2.0。**部署必须用 0 (AUTO)**,与 Amlogic 系统设置 UI 里的"自动"选项对应(UI 只提供 PCM/自动/手动,对应 1/0/3)。 + +另:设备**重启后系统组件会把 `encoded_surround_output` 重置为 1 (NEVER)**(约开机 13 秒后 SettingsObserver 生效)。此时 app 侧安全回退 FFmpeg 软解 2.0,不会无声,但 5.1 失效,需设备侧固化(见 §4 待办)。 + +### 2.4 恢复 HAL AUTO 的手段(已验证) + +1. 重启设备(HAL 回 AUTO,但 setting 被重置为 1,需再设 0): + ```bash + adb reboot + adb shell settings put global encoded_surround_output 0 # 重启后必须补设 + ``` +2. 纯切 setting 不重启:`settings put global encoded_surround_output 0` 即可将 HAL 从 PCM 切回 AUTO(实测有效)。 + +### 2.5 CDN 流结构(实测修正) + +以《飞驰人生2》playAuth 响应为例,4 档: + +| 文件名特征 | 视频 | 音频(ffprobe 实测) | format 字段 | +|---|---|---|---| +| `.mp4.hls` | H264 1.5M 720P | AAC 2.0 | `720P` | +| `_h264_1.8m-720p` | H264 1.8M 720P | AAC 2.0 | `720P` | +| `_h264_2.5m-1080p` | H264 2.5M 1080P | **AAC 48000Hz 2ch** | `1080P` | +| `_1080P_h265_3.5M_SDR_AC3_51` | H265 3.5M 1080P | AC3 5.1(单音轨) | `1080P 5.1 环绕声` | + +- 早前"1080P 只有 AC3 5.1(隐式耦合)"的结论**是误判**,只适用于部分老片;新片有独立 1080P AAC 2.0 档。 +- "5.1 流单 AC3 音轨、无 2.0 兜底"结论**仍成立**——5.1 流内无 2.0 音轨,不支持直通必须整条换 URL。 +- 现有选流逻辑天然兼容两种片源结构:关 5.1 → 普通 1080P AAC(老片无此档回落 720P);开 5.1 → AC3 档。 + +### 2.6 v1.3.35 注入失效根因(腾讯T5 光纤场景排查实录,v1.3.36 修复) + +**现象**:腾讯 T5 投影(Amlogic TL1 / Android 9 / API 28),系统声音输出设为光纤 SPDIF RAW,设备页仍显示软解,AC3 走平台解码下混 2.0 PCM。 + +**排查链(三个 App 侧根因)**: +1. **注入被系统 caps 覆盖(核心)**:`DefaultAudioSink.Builder(context)` 传 context → `getFormatSupport()` 首次调用懒注册 `AudioCapabilitiesReceiver` 覆盖注入值 → **v1.3.35 注入从未生效**(166 的直通是系统 HDMI 自报 AC3,与注入无关;122 软解是平台无 AC3 decoder,也与注入无关)。修复 = 无参 `Builder()`,详见 §1.2 改动 3。 +2. **isAlias API 兼容**:`MediaCodecInfo.isAlias()` 是 API 29+ 方法,API 28 抛 `NoSuchMethodError` 被外层 catch 吞掉 → `hasPlatformAc3Decoder()` 循环中断、永远 false。修复 = `SDK_INT >= Q` 守卫。 +3. **能力检测口径(注入的价值所在)**:系统音频能力上报(HDMI plug 广播 / `getDirectPlaybackSupportedEncodings()`)**只感知 HDMI,不感知 SPDIF**——光纤场景检测 UI 显示"软解"是口径内必然;而 T5 平台实际支持 AC3 直通(policy 有 tunnel raw profile、注册 `audio/raw` 与 `audio/ac3` decoder)。caps 注入就是让 ExoPlayer 越过系统口径尝试直通,失败再由降级重试兜底。 + +**降级重试(L2 风险防护)**:注入生效后出现新风险面——有 raw decoder 但 policy 无 DIRECT 压缩输出的设备,`AudioTrack(ENCODING_AC3)` 初始化失败是**运行时硬失败不回落**。方案:错误码白名单(1000/4001/5001)→ 进程级 `sAc3InjectionDisabled` 置位 → 重建播放器软解重试(保留进度、一次为限、重试失败才上抛原错误)。 + +**验证结论(T5 光纤 SPDIF,v1.3.36 实测)**:注入生效 → `EventLogger` 音轨 `audio/ac3 channels=6 supported=YES` → HAL 日志 `SPDIF_FORMAT 2 + spdif mute 0`(AC3 raw 输出,对比修复前 `nPassThroughEnable:0` 解码模式)→ 客户环境播放出声,直通闭环成立,未触发降级(一次成功)。App 侧新增的可观察日志:`buildAudioSink: 系统未报告AC3,强制注入`。 + +**版本口径**:修复代码注释标记 v1.3.36,但随 versionName **v1.3.35** 发布(发版版本号未升,versionCode 按 yyMMddHH 自动递增)。 + +--- + +## 3. 验证手册 + +### 3.1 设备状态检查 + +```bash +adb connect 172.16.100.166 +adb shell settings get global encoded_surround_output # 期望 0 +adb shell dumpsys media.audio_flinger | grep digital_audio_mode # 期望 AUTO +adb shell cat /sys/class/audiodsp/digital_raw # 播放 AC3 中期望 1,空闲可能 0 +``` + +### 3.2 播放中直通链路验证 + +```bash +# DIRECT 线程应为 AC3 + RAW_DIRECT + active +adb shell dumpsys media.audio_flinger | grep -E "Output thread|HAL format|usecase|active" +# 期望: +# Output thread ... type 1 (DIRECT) +# HAL format: 0x9000000 (AUDIO_FORMAT_AC3) +# -usecase: RAW_DIRECT +# N Tracks of which M are active + +# App 侧关键日志 +adb logcat -d | grep -E "buildAudioSink|videoUrl_51 applied" +# 期望: +# buildAudioSink: 系统已报告AC3直通能力(正常路径) +# 或 buildAudioSink: 系统未报告AC3,强制注入(注入路径) +# videoUrl_51 applied: +# 直通初始化失败自动降级时(罕见,Amlogic 光纤直通一次成功的场景看不到): +# onPlayerError: 注入会话音频初始化失败(code=...),自动降级为软解重试 +# degradeAndRetry: 关闭AC3注入重建播放器, resumePos=... +# buildAudioSink: 注入已禁用(本进程曾直通失败),走系统 caps +``` + +### 3.3 功能验收用例 + +1. 开 5.1 播放 → 功放面板显示 AC3、5.1 效果 ✓(v1.3.35 + setting=0 已验收) +2. 关 5.1 重新起播 → 普通档 URL(`_h264_*` 非 AC3_51)、2.0 ✓ +3. 再开 5.1 重新起播 → AC3 档、直通恢复 ✓ +4. 切换仅重新起播即生效,无需杀 app ✓ +5. `encoded_surround_output=1`(NEVER)时播放 → 回退软解 2.0,不无声 ✓(HAL PCM + policy 不报 AC3 + 注入条件排除 NEVER,三者一致) +6. 光纤 SPDIF 设备(系统能力上报不感知 SPDIF):SPDIF RAW 模式 + 5.1 开启 → 注入路径直通 ✓(腾讯T5/Amlogic TL1/Android 9,客户环境实测出声,未触发降级) + +### 3.4 CDN 档位探测(排查片源问题用) + +```bash +# 从 logcat 拿 playAuth response 里的目标档位 URL(含 auth_key),然后: +curl -s "" | head -8 # 拿首段 ts 的 auth_key +curl -s -o /tmp/probe.ts "" +ffprobe -v error -show_entries stream=index,codec_name,codec_type,channels,sample_rate -of csv /tmp/probe.ts +``` + +--- + +## 4. 遗留待办 + +1. **设备侧固化 `encoded_surround_output=0`**:排查开机把它重置为 1 的系统组件(疑似 Amlogic 设置应用/固件默认逻辑),部署设备需固化,否则每次重启后 5.1 静默降级 2.0。 +2. **5.1 节目标识角标**:详情页/海报角标"5.1",基于 `VideoInfo.format` 判断(EPG 无字段)。 +3. **清晰度菜单档位名**:5.1 开启时显示"1080P 环绕声"替代直接替换 1080 档文案。 +4. (可选)播放器内 5.1 实时状态指示:读取当前流音轨格式显示直通/软解状态。 +5. **122 (MTK) 降级重试回归实测**:v1.3.36 注入真正生效后,有 `audio/raw` decoder 的设备将首次尝试 `AudioTrack(AC3)` 直通,预期触发 `degradeAndRetry` 回落软解——降级路径尚未真机验证,装正式包后需顺带回归(日志关键字见 §3.2)。 + +--- + +## 5. 分支重建操作建议 + +```bash +# 从 main 建 5.1 渠道分支 +git checkout main && git pull +git checkout -b feature/51-surround +# 按 §1 清单逐文件套用(1.1/1.10 为新增文件,其余为修改) +# 编译验证 +sh gradlew :app:assembleaishiRemoteRelease +# 按本机既有命名约定打 AS 包(aishiRemoteRelease 变体) +``` + +验收顺序:§3.1 设备状态 → §3.2 直通链路 → §3.3 五个用例。 diff --git a/README.md b/README.md old mode 100755 new mode 100644 diff --git a/app/.gitignore b/app/.gitignore old mode 100755 new mode 100644 diff --git a/app/build.gradle b/app/build.gradle old mode 100755 new mode 100644 index 90c422e..dabf66d --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,3 @@ -import com.android.build.api.variant.BuildConfigField - plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' @@ -16,13 +14,21 @@ android { checkReleaseBuilds false abortOnError false } + // 单元测试跑在 JVM 上,org.json 等 Android SDK 类默认抛 "not mocked"; + // voice/parser(SkyworthVodParser 等)依赖 org.json 解析 payload, + // 需要真实实现。这里引入官方 json.org 的 JVM 版本(仅 test classpath)。 + testOptions { + unitTests { + includeAndroidResources = true + } + } def versionSuffix = "0" // 定义版本后缀变量,必须小于或等于9大于0 defaultConfig { applicationId "tv.huashi.jetsenx.jetsenfilx.jsttv" minSdk app_android.minSdkVersion targetSdk app_android.targetSdkVersion versionCode Integer.parseInt(new Date().format("yyMMddHH") + versionSuffix) - versionName "v1.3.28" + versionName "v1.3.35" multiDexEnabled true //压缩语言种类,目前只支持中文语言 resConfigs('zh-rCN') @@ -30,6 +36,10 @@ android { // Define flavor dimensions flavorDimensions "channel", "control" + // ===== 语音协议占位符默认值(文档 §6.2 signature-level permission) ===== + // debug 通过 flavor 覆盖为 normal 方便联调,release 维持 signature。 + manifestPlaceholders = [voiceProtectionLevel: "signature"] + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" //酒店 ANNING 安柠 AISHI 爱视(不要手柄) buildConfigField "String", "HOTEL_TYPE", "\"AISHI\"" @@ -39,9 +49,11 @@ android { buildConfigField "boolean", "NEED_ACTIVATION_CODE", "true" //是否为扫码变体(as8Scan 覆盖为 true;asAScan 同样覆盖为 true) buildConfigField "boolean", "SCAN_VARIANT", "false" + //是否为家用版(home flavor 覆盖为 true) + buildConfigField "boolean", "IS_HOME_EDITION", "false" + //默认激活码:扫码变体在 NEED_ACTIVATION_CODE=false 时使用,普通渠道为空字符串 + buildConfigField "String", "DEFAULT_ACTIVATION_CODE", "\"\"" buildConfigField "String", "BUGLY_APP_ID", "\"01f1b10fca\"" - //是否为第三方变体(thirdparty flavor 覆盖为 true) - buildConfigField "boolean", "THIRDPARTY_VARIANT", "false" ndk { abiFilters "armeabi-v7a", "arm64-v8a" @@ -71,6 +83,8 @@ android { zipAlignEnabled true // 是否开启zip压缩 signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + // release 严格按文档 §6.2 使用 signature-level permission + manifestPlaceholders = [voiceProtectionLevel: "signature"] } // preview { @@ -86,12 +100,14 @@ android { debug { // AS 域 baseUrl:debug 走测试/内网域名 - buildConfigField "String", "AS_BASE_URL", "\"https://jst-test.ainetv.cn/api/\"" //http://as-partner-end.172.16.100.110.sslip.io/ + buildConfigField "String", "AS_BASE_URL", "\"http://as-partner-end.172.16.100.105.sslip.io/\"" //http://as-partner-end.172.16.100.105.sslip.io/ minifyEnabled false shrinkResources false zipAlignEnabled true // 是否开启zip压缩 signingConfig signingConfigs.release proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + // 联调期间把 voice 协议自定义 permission 降级为 normal,让 adb shell 可投递(release 仍走 signature)。 + manifestPlaceholders = [voiceProtectionLevel: "normal"] } } @@ -138,16 +154,29 @@ android { buildConfigField "String", "CHANNEL_ID", "\"TS\"" } - // --- aishi 子方向:扫码版(无激活码 默认 8888 的激活码) --- + // --- aishi 子方向:扫码版(无激活码 默认 88888 的激活码) --- // 共享 aishi 渠道路由(X-Channel-Id="AS"),仅客户端激活流程差异 as8Scan { dimension "channel" manifestPlaceholders = [ - APP_CHANNEL_ID : "aishi", + APP_CHANNEL_ID : "as8Scan", splash_theme : "@style/SplashTheme"] buildConfigField "String", "CHANNEL_ID", "\"AS\"" buildConfigField "boolean", "NEED_ACTIVATION_CODE", "false" buildConfigField "boolean", "SCAN_VARIANT", "true" + buildConfigField "String", "DEFAULT_ACTIVATION_CODE", "\"88888\"" + } + + // --- 声立方:扫码版(无激活码 默认 UK4KQ 的激活码) --- + fxScan { + dimension "channel" + manifestPlaceholders = [ + APP_CHANNEL_ID : "fxScan", + splash_theme : "@style/SplashTheme"] + buildConfigField "String", "CHANNEL_ID", "\"FX\"" + buildConfigField "boolean", "NEED_ACTIVATION_CODE", "false" + buildConfigField "boolean", "SCAN_VARIANT", "true" + buildConfigField "String", "DEFAULT_ACTIVATION_CODE", "\"UK4KQ\"" } // --- aishi 子方向:激活扫码版(有激活码) --- @@ -157,23 +186,58 @@ android { asAScan { dimension "channel" manifestPlaceholders = [ - APP_CHANNEL_ID : "aishi", + APP_CHANNEL_ID : "asAScan", splash_theme : "@style/SplashTheme"] buildConfigField "String", "CHANNEL_ID", "\"AS\"" buildConfigField "boolean", "SCAN_VARIANT", "true" } - // --- 第三方渠道(thirdparty) --- - thirdparty { + // 测试渠道分销 + qnAScan { dimension "channel" - applicationIdSuffix ".thirdparty" manifestPlaceholders = [ - APP_CHANNEL_ID : "thirdparty", + APP_CHANNEL_ID : "qnAScan", splash_theme : "@style/SplashTheme"] - buildConfigField "String", "CHANNEL_ID", "\"TP\"" - buildConfigField "boolean", "THIRDPARTY_VARIANT", "true" - // 注意:AS_BASE_URL 不能在此处覆盖,buildType 的 buildConfigField 优先级更高会将其覆盖。 - // 第三方域名通过下方 androidComponents.onVariants 设置(Variant API 优先级最高)。 + buildConfigField "String", "CHANNEL_ID", "\"QN\"" + buildConfigField "boolean", "SCAN_VARIANT", "true" + } + + + // --- 家用版:独立 applicationId,与商用渠道并行发行 --- + // 暂与商用版共用 BUGLY_APP_ID / versionCode(家用版需独立时再覆盖)。 + // 未来若需按地区/子渠道拆分,加为 sibling flavor 并各自挂 applicationIdSuffix 即可。 + // + // CHANNEL_ID="HM":与 CLAUDE.md 渠道表一致,且避免与 fxScan("FX") 在 X-Channel-ID 头冲突 + // (OkHttpClientUtil + HttpRequestInterceptor 注入该头,相同 CHANNEL_ID 会让后端把家用版路由为声立方扫码版)。 + // SCAN_VARIANT=true + DEFAULT_ACTIVATION_CODE="UK4KQ":保持与 as8Scan/fxScan 同款的"扫码 + 静默激活"语义, + // 配合 tryAutoActivateWithDefaultCode 在 MainActivity 静默激活家用版设备。 + // 若后续家用版切换为需要激活码输入面板,可将 NEED_ACTIVATION_CODE 改回 true 并清空 DEFAULT_ACTIVATION_CODE。 + home { + dimension "channel" + applicationIdSuffix ".home" + manifestPlaceholders = [ + APP_CHANNEL_ID : "home", + splash_theme : "@style/SplashTheme"] + buildConfigField "String", "CHANNEL_ID", "\"FX\"" + buildConfigField "String", "HOTEL_TYPE", "\"HOME\"" + buildConfigField "boolean", "IS_HOME_EDITION", "true" + buildConfigField "boolean", "NEED_ACTIVATION_CODE", "false" + buildConfigField "boolean", "SCAN_VARIANT", "false" + buildConfigField "String", "DEFAULT_ACTIVATION_CODE", "\"UK4KQ\"" + } + + ashome { + dimension "channel" + applicationIdSuffix ".home" + manifestPlaceholders = [ + APP_CHANNEL_ID : "home", + splash_theme : "@style/SplashTheme"] + buildConfigField "String", "CHANNEL_ID", "\"AS\"" + buildConfigField "String", "HOTEL_TYPE", "\"HOME\"" + buildConfigField "boolean", "IS_HOME_EDITION", "true" + buildConfigField "boolean", "NEED_ACTIVATION_CODE", "false" + buildConfigField "boolean", "SCAN_VARIANT", "false" + buildConfigField "String", "DEFAULT_ACTIVATION_CODE", "\"88888\"" } // Add more channels to this list as needed @@ -222,18 +286,6 @@ android { } } -// 第三方变体 AS_BASE_URL 覆盖: -// buildType 的 buildConfigField 优先级高于 flavor,因此 thirdparty flavor 内直接设置会被 -// debug/release buildType 覆盖为 jst-test/jst.ainetv.cn。Variant API 的优先级最高,在此统一覆盖。 -androidComponents { - onVariants(selector().withFlavor("channel", "thirdparty")) { variant -> - variant.buildConfigFields.put( - "AS_BASE_URL", - new BuildConfigField("String", "\"http://58.220.83.58:8084/\"", null) - ) - } -} - dependencies { @@ -260,12 +312,13 @@ dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'com.google.android.exoplayer:exoplayer:2.19.1' - //implementation(name: 'exoplayer-ffmpeg-extension-2.19.1', ext: 'aar') + // 5.1声道兜底(AC3/EAC3/DTS/TrueHD),配合 PkExoMediaPlayer 的 EXTENSION_RENDERER_MODE_ON: + // 平台渲染器优先(支持AC3直通/HW解码的设备走平台),FFmpeg仅在平台不支持时兜底软解为PCM + implementation files('libs/exoplayer-ffmpeg-extension-2.19.1.aar') // implementation 'com.sensorsdata.analytics.android:SensorsAnalyticsSDK:6.8.3' implementation 'commons-codec:commons-codec:1.15' - // 3.12.0:3.12.1+(含 3.12.13)引入 Android10Platform 触发 Dalvik VerifyError,必须精确锁 3.12.0 - implementation 'com.squareup.okhttp3:okhttp:3.12.0' + implementation 'com.squareup.okhttp3:okhttp:3.12.13' implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.github.gzu-liyujiang:Android_CN_OAID:4.1.3' implementation('io.github.volcengine-tls:tls-android-producer:2.0.2-api16') { @@ -279,32 +332,30 @@ dependencies { implementation 'com.google.guava:guava:31.1-android' // ===== 网络层(脚手架新引入,与既有 HttpUtils 双栈共存) ===== - // ===== Android 4.2 兼容(对齐 mediaapp 已验证组合,见 Android4.2兼容性改动文档)===== - // - OkHttp 4.x 官方要求 API 21+;3.13+ 含 Android10Platform(API 10 SSL API)触发 Dalvik VerifyError。 - // 3.12.0 是支持 API 9-20 的长期分支(与 libcore/libdata 老栈版本一致),配合下方 force 全局锁定。 - // - Retrofit 2.6.0:suspend 函数 2.6.0 起支持;2.9+ 传递依赖 OkHttp 3.14.9(含 Android10Platform)。 - // - Coroutines 1.6.4:1.7+ 要求 minSdk 21,1.6.4 兼容 API 16+。 - implementation 'com.squareup.retrofit2:retrofit:2.6.0' - implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' - implementation 'com.squareup.okhttp3:okhttp:3.12.0' - implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0' + // 为什么显式指定版本:app 模块此前未声明 retrofit/okhttp/moshi/coroutines,避免被根 config.gradle 隐式拉入不同版本。 + // Kotlin 1.9.22 -> KSP 1.9.22-1.0.17 已对齐。 + // 为什么用 `platform(enforcedPlatform)` 而非 `implementation`:okhttp-bom 是 Bill-of-Materials,本身仅提供版本对齐, + // 不能被当作运行时产物拉到 runtime classpath(这正是上一个编译报错的根因)。 + implementation platform('com.squareup.okhttp3:okhttp-bom:4.12.0') + implementation 'com.squareup.retrofit2:retrofit:2.11.0' + implementation 'com.squareup.retrofit2:converter-moshi:2.11.0' + implementation 'com.squareup.okhttp3:okhttp' + implementation 'com.squareup.okhttp3:logging-interceptor' implementation 'com.squareup.moshi:moshi:1.15.1' implementation 'com.squareup.moshi:moshi-kotlin:1.15.1' ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.15.1' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' - // 防止其他依赖(tls-producer/libdata 等)传递引入更高版本 OkHttp/Retrofit 破坏 4.2 兼容 - configurations.all { - resolutionStrategy.force 'com.squareup.okhttp3:okhttp:3.12.0' - resolutionStrategy.force 'com.squareup.retrofit2:retrofit:2.6.0' - } - implementation 'net.jpountz.lz4:lz4:1.3.0' implementation 'org.slf4j:slf4j-android:1.7.36' //腾讯buggly implementation 'com.tencent.bugly:crashreport:latest.release' //爱奇艺本地奔溃日志 implementation 'com.iqiyi.xcrash:xcrash-android-lib:3.1.0' + + // org.json 的 JVM 实现:Android SDK 自带的 org.json 在本地单测里是空壳(not mocked), + // 供 voice/parser 下基于 JSONObject 的解析逻辑做真实单测。 + testImplementation 'org.json:json:20180813' } diff --git a/app/key/jiecheng.jks b/app/key/jiecheng.jks old mode 100755 new mode 100644 diff --git a/app/key/keystore.properties b/app/key/keystore.properties old mode 100755 new mode 100644 diff --git a/app/libs/JetsenxSdk-api-future-release-v1.23.aar b/app/libs/JetsenxSdk-api-future-release-v1.23.aar old mode 100755 new mode 100644 diff --git a/app/libs/JetsenxSdk-api-release_v1.14.0.aar b/app/libs/JetsenxSdk-api-release_v1.14.0.aar old mode 100755 new mode 100644 diff --git a/app/libs/JetsenxSdk-external-future-release-v1.23.aar b/app/libs/JetsenxSdk-external-future-release-v1.23.aar old mode 100755 new mode 100644 diff --git a/app/libs/JetsenxSdk-external-release_v1.14.0.aar b/app/libs/JetsenxSdk-external-release_v1.14.0.aar old mode 100755 new mode 100644 diff --git a/app/libs/NewTVSDK-v4.0.3-app-lite.jar b/app/libs/NewTVSDK-v4.0.3-app-lite.jar old mode 100755 new mode 100644 diff --git a/app/libs/arm64-v8a/libnewtvsdk.so b/app/libs/arm64-v8a/libnewtvsdk.so old mode 100755 new mode 100644 diff --git a/app/libs/armeabi-v7a/libnewtvsdk.so b/app/libs/armeabi-v7a/libnewtvsdk.so old mode 100755 new mode 100644 diff --git a/app/libs/exoplayer-ffmpeg-extension-2.19.1.aar b/app/libs/exoplayer-ffmpeg-extension-2.19.1.aar old mode 100755 new mode 100644 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro old mode 100755 new mode 100644 diff --git a/app/src/androidTest/androidmanifest.xml b/app/src/androidTest/androidmanifest.xml old mode 100755 new mode 100644 diff --git a/app/src/androidTest/java/com/migu/jiecheng/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/migu/jiecheng/ExampleInstrumentedTest.java old mode 100755 new mode 100644 diff --git a/app/src/androidTest/java/com/migu/jiecheng/IndexNavType6TestActivity.java b/app/src/androidTest/java/com/migu/jiecheng/IndexNavType6TestActivity.java old mode 100755 new mode 100644 diff --git a/app/src/androidTest/java/com/migu/jiecheng/IndexNavType6UiTest.java b/app/src/androidTest/java/com/migu/jiecheng/IndexNavType6UiTest.java old mode 100755 new mode 100644 diff --git a/app/src/aishiActivation/res/values/strings.xml b/app/src/as8Scan/res/values/strings.xml old mode 100755 new mode 100644 similarity index 100% rename from app/src/aishiActivation/res/values/strings.xml rename to app/src/as8Scan/res/values/strings.xml diff --git a/app/src/aishiScan/res/values/strings.xml b/app/src/asAScan/res/values/strings.xml old mode 100755 new mode 100644 similarity index 100% rename from app/src/aishiScan/res/values/strings.xml rename to app/src/asAScan/res/values/strings.xml diff --git a/app/src/ashome/res/values/strings.xml b/app/src/ashome/res/values/strings.xml new file mode 100644 index 0000000..b1ab3a3 --- /dev/null +++ b/app/src/ashome/res/values/strings.xml @@ -0,0 +1,4 @@ + + + 橘视厅家用版 + \ No newline at end of file diff --git a/app/src/fxScan/res/values/strings.xml b/app/src/fxScan/res/values/strings.xml new file mode 100644 index 0000000..03ec69a --- /dev/null +++ b/app/src/fxScan/res/values/strings.xml @@ -0,0 +1,4 @@ + + + 橘视厅扫码版 + \ No newline at end of file diff --git a/app/src/home/res/values/strings.xml b/app/src/home/res/values/strings.xml new file mode 100644 index 0000000..b1ab3a3 --- /dev/null +++ b/app/src/home/res/values/strings.xml @@ -0,0 +1,4 @@ + + + 橘视厅家用版 + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml old mode 100755 new mode 100644 index 5b584c8..09b5312 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,10 +4,6 @@ package="com.pukka.jiecheng" tools:ignore="MissingLeanbackLauncher"> - - - @@ -41,6 +37,23 @@ + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/pukka/jiecheng/JcConfig.java b/app/src/main/java/com/pukka/jiecheng/JcConfig.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/MainActivity.java b/app/src/main/java/com/pukka/jiecheng/MainActivity.java old mode 100755 new mode 100644 index d1304c6..03ce36d --- a/app/src/main/java/com/pukka/jiecheng/MainActivity.java +++ b/app/src/main/java/com/pukka/jiecheng/MainActivity.java @@ -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,否则主进程未激活路径下入队会静默丢失。 + * + *

详见 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。 + * + *

两路来源: + *

    + *
  1. 当前 Intent 带 {@code __voice_pending=true}(典型场景::voice 进程 cold launch MainActivity, + * 或主进程已存活但未激活时 VoiceInternalReceiver 冷启 MainActivity)
  2. + *
  3. Intent 不带 extras 但 SP 标记为 true(典型场景:强制升级后新版 App 启动 / appExit 后再次启动)
  4. + *
+ * + *

复原后写入 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。 + * + *

状态清理规则(修复 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); } diff --git a/app/src/main/java/com/pukka/jiecheng/MyApplication.java b/app/src/main/java/com/pukka/jiecheng/MyApplication.java old mode 100755 new mode 100644 index 061af05..8310773 --- a/app/src/main/java/com/pukka/jiecheng/MyApplication.java +++ b/app/src/main/java/com/pukka/jiecheng/MyApplication.java @@ -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} 显式广播。 + * + *

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 进程不存在时启动,避免通知闪烁。 + * + *

触发场景: + *

    + *
  • 桌面图标 cold start(开机后用户首次点开 App)
  • + *
  • appExit() 后再次进入(主进程降为后台但 :voice 已被 LMK 杀掉)
  • + *
  • force-stop 后用户首次启动
  • + *
+ * + *

BOOT_COMPLETED 拉起由 {@link com.pukka.jiecheng.voice.receiver.BootCompletedReceiver} + * 负责;本方法只在 MyApplication.onCreate 时(即用户进 App 的时刻)兜底。 + * + *

另外通过 {@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)。 * diff --git a/app/src/main/java/com/pukka/jiecheng/TestActivity.java b/app/src/main/java/com/pukka/jiecheng/TestActivity.java old mode 100755 new mode 100644 index 7f8f48f..404291f --- a/app/src/main/java/com/pukka/jiecheng/TestActivity.java +++ b/app/src/main/java/com/pukka/jiecheng/TestActivity.java @@ -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(); + } + + /** + * 触发首签接口,并在成功后启动心跳调度。 + * + *

sign 同步链路(关键,整个项目都依赖这条路径):

+ *
    + *
  1. requestAsInitDeviceApi 成功后,AsApiRequester 内部调用 + * {@code Network.tokenStore.refreshAndPersist(sign)}:
  2. + *
      + *
    • 写入 TokenStore(SpTokenStore,内存 + SP 异步)—— HeaderInterceptor 每次发请求都从这里读
    • + *
    • 同步写入 SharedPreferenceUtils(key="as_sign")—— 兼容旧 Java 代码直接读 SP
    • + *
    + *
  3. HeaderInterceptor.intercept() 在每个 OkHttp 请求发出前注入 {@code sign} 请求头 + * (首签/刷新自身接口除外),所以从 TestActivity 跳转到 OrderActivity 后, + * OrderActivity 发起的任意接口(订单列表、扫码等)都会自动带上正确的 sign, + * 无需在跳转处做任何额外传参。
  4. + *
  5. AsHeartBeatManager 内部的心跳请求也走同一套 HeaderInterceptor, + * 因此心跳能持续携带有效 sign;服务端后续若在心跳响应里下发新的 sign + * ({@code refreshSign} 等命令),TokenStore 也会被同步刷新,下一次请求自动接力。
  6. + *
+ * + *

心跳启动必须用 getApplicationContext():AsHeartBeatManager 内部用 + * DispatchQueue.global.timer 启动了一个常驻定时器(应用生命周期内保持活跃), + * Activity context 会泄漏,applicationContext 安全。

+ */ + 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); + } + }); } } diff --git a/app/src/main/java/com/pukka/jiecheng/ThirdPartyEntryActivity.java b/app/src/main/java/com/pukka/jiecheng/ThirdPartyEntryActivity.java deleted file mode 100644 index b1aa256..0000000 --- a/app/src/main/java/com/pukka/jiecheng/ThirdPartyEntryActivity.java +++ /dev/null @@ -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 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 collectDeviceMacAddresses() { - Set macs = new HashSet<>(); - // 通道一:NetworkInterface 遍历(覆盖 eth0/wlan0/物理及虚拟网卡) - try { - Enumeration 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 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); - } -} diff --git a/app/src/main/java/com/pukka/jiecheng/aboutmy/AboutMyActivity.java b/app/src/main/java/com/pukka/jiecheng/aboutmy/AboutMyActivity.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/aboutmy/LongPressSpanHelper.kt b/app/src/main/java/com/pukka/jiecheng/aboutmy/LongPressSpanHelper.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/base/BaseActivity.java b/app/src/main/java/com/pukka/jiecheng/base/BaseActivity.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/base/BaseOnItemClickListener.java b/app/src/main/java/com/pukka/jiecheng/base/BaseOnItemClickListener.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/base/BaseOnItemFocusChangeListener.java b/app/src/main/java/com/pukka/jiecheng/base/BaseOnItemFocusChangeListener.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/base/BasePresenter.java b/app/src/main/java/com/pukka/jiecheng/base/BasePresenter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/base/BasePresenterViewHolder.java b/app/src/main/java/com/pukka/jiecheng/base/BasePresenterViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/base/BaseRecyclerAdapter.java b/app/src/main/java/com/pukka/jiecheng/base/BaseRecyclerAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/base/BaseRecyclerViewHolder.java b/app/src/main/java/com/pukka/jiecheng/base/BaseRecyclerViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/base/BaseViewHolder.java b/app/src/main/java/com/pukka/jiecheng/base/BaseViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/base/SystemConfigManager.java b/app/src/main/java/com/pukka/jiecheng/base/SystemConfigManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/config/AppConfiguration.java b/app/src/main/java/com/pukka/jiecheng/config/AppConfiguration.java old mode 100755 new mode 100644 index 2a740ea..3e4df08 --- a/app/src/main/java/com/pukka/jiecheng/config/AppConfiguration.java +++ b/app/src/main/java/com/pukka/jiecheng/config/AppConfiguration.java @@ -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; } diff --git a/app/src/main/java/com/pukka/jiecheng/device/DeviceActivity.kt b/app/src/main/java/com/pukka/jiecheng/device/DeviceActivity.kt old mode 100755 new mode 100644 index 924123f..fe3aaf8 --- a/app/src/main/java/com/pukka/jiecheng/device/DeviceActivity.kt +++ b/app/src/main/java/com/pukka/jiecheng/device/DeviceActivity.kt @@ -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(R.id.id_cl_check_update) + // 音频能力检测:AC3直通 > 平台硬解 > FFmpeg软解 + mTvAudioPath = findViewById(R.id.id_tv_audio_path) + mTvAudioPath!!.text = Surround51Manager.getAudioPathDesc(this) + // 5.1 环绕声播放开关:关闭时播放普通 2.0 档 + mLlSurround51 = findViewById(R.id.id_ll_surround51) + mTvSurround51Value = findViewById(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(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("暂无新版本") -// }) + }) } } diff --git a/app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityInitManager.java b/app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityInitManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityProvider.java b/app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityProvider.java old mode 100755 new mode 100644 index 08bd38b..b066e4d --- a/app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityProvider.java +++ b/app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityProvider.java @@ -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)); diff --git a/app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityStore.java b/app/src/main/java/com/pukka/jiecheng/device/DeviceIdentityStore.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/device/OaidProvider.java b/app/src/main/java/com/pukka/jiecheng/device/OaidProvider.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/downapk/DownApkDialog.java b/app/src/main/java/com/pukka/jiecheng/downapk/DownApkDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/downapk/DownloadApkManager.java b/app/src/main/java/com/pukka/jiecheng/downapk/DownloadApkManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/downapk/StoreVersionUpgrader.kt b/app/src/main/java/com/pukka/jiecheng/downapk/StoreVersionUpgrader.kt new file mode 100644 index 0000000..3b0c09b --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/downapk/StoreVersionUpgrader.kt @@ -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 = 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() + .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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/downapk/UpgradeHelper.java b/app/src/main/java/com/pukka/jiecheng/downapk/UpgradeHelper.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/downapk/amber/DownApkAmberManager.java b/app/src/main/java/com/pukka/jiecheng/downapk/amber/DownApkAmberManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/filter/FilterActivity.java b/app/src/main/java/com/pukka/jiecheng/filter/FilterActivity.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/filter/FilterFragment.java b/app/src/main/java/com/pukka/jiecheng/filter/FilterFragment.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/filter/HeaderVisibilityHelper.java b/app/src/main/java/com/pukka/jiecheng/filter/HeaderVisibilityHelper.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/filter/adapter/ContentAdapter.java b/app/src/main/java/com/pukka/jiecheng/filter/adapter/ContentAdapter.java old mode 100755 new mode 100644 index 70fa23c..f5ed62b --- a/app/src/main/java/com/pukka/jiecheng/filter/adapter/ContentAdapter.java +++ b/app/src/main/java/com/pukka/jiecheng/filter/adapter/ContentAdapter.java @@ -90,7 +90,7 @@ public class ContentAdapter extends RecyclerView.Adapter判断依据:{@code BuildConfig.SCAN_VARIANT} 由 build.gradle 的 flavor 维度注入, + * 扫码版自动置为 true。扫码版默认 tab 落「电影」+ 切 contentFragment;行业版默认 tab + * 按 navName 包含"推荐"定位(接口 type 字段不可靠) + 动态切换对应 Fragment。

+ */ + private boolean isScanVariant() { + return BuildConfig.SCAN_VARIANT; + } + + /** + * 行业版定位默认「推荐」tab:接口下发的 type 字段不可靠(如 navList[0]=「我的」type=2), + * 直接遍历 navList 按 navName 包含"推荐"来定位(与扫码版找「电影」的方式一致)。 + * + *

找不到时兜底回退到第 0 个 tab(与之前行为兼容)。

+ */ + private NavFocusBean findDefaultNavIndex(List 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 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(); } } }); } + /** + * 冷启动语音命令回放:导航就绪、首帧渲染完成后触发。 + *

+ * 设计要点(DIP 解耦):IndexActivity 属于 UI 层,voice 模块属于业务层, + * 为避免相互 import 产生循环依赖,这里用反射调用 + * {@code VoiceCommandService.getInstance(context).replayColdStartQueue()}, + * 方法签名固定、零性能影响(仅首帧触发一次)。 + *

+ */ + 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 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) { diff --git a/app/src/main/java/com/pukka/jiecheng/index/IndexAmberManager.java b/app/src/main/java/com/pukka/jiecheng/index/IndexAmberManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/IndexConstant.java b/app/src/main/java/com/pukka/jiecheng/index/IndexConstant.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/D1ContentAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/D1ContentAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/D3ContentAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/D3ContentAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/D3NavAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/D3NavAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/D4ContentAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/D4ContentAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexContentAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexContentAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexGroupNavAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexGroupNavAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMyCollectAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMyCollectAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMyHistoryAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMyHistoryAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMySetMoreAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexMySetMoreAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexNavAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/IndexNavAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/MatchListAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/MatchListAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/MatchListHorAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/MatchListHorAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/ShortVideoAdapter.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/ShortVideoAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/interfaces/OnNavidListener.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/interfaces/OnNavidListener.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/viewholder/GroupNavViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/viewholder/GroupNavViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/adapter/viewholder/NavViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/adapter/viewholder/NavViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/bean/CornerMarkBean.java b/app/src/main/java/com/pukka/jiecheng/index/bean/CornerMarkBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/bean/D1TypeBean.java b/app/src/main/java/com/pukka/jiecheng/index/bean/D1TypeBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/bean/DefaultNavFocusBean.java b/app/src/main/java/com/pukka/jiecheng/index/bean/DefaultNavFocusBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/bean/ItemTypeBean.java b/app/src/main/java/com/pukka/jiecheng/index/bean/ItemTypeBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/bean/LogOutBean.java b/app/src/main/java/com/pukka/jiecheng/index/bean/LogOutBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/bean/MyIndexBean.java b/app/src/main/java/com/pukka/jiecheng/index/bean/MyIndexBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/bean/NavBean.java b/app/src/main/java/com/pukka/jiecheng/index/bean/NavBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/bean/NavFocusBean.java b/app/src/main/java/com/pukka/jiecheng/index/bean/NavFocusBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/bean/NavResideBean.java b/app/src/main/java/com/pukka/jiecheng/index/bean/NavResideBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/dialog/BackImgDialog.java b/app/src/main/java/com/pukka/jiecheng/index/dialog/BackImgDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/dialog/BackRetentionDialog.java b/app/src/main/java/com/pukka/jiecheng/index/dialog/BackRetentionDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/dialog/FullimgDialog.java b/app/src/main/java/com/pukka/jiecheng/index/dialog/FullimgDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/dialog/HomeLogoutConfirmDialog.kt b/app/src/main/java/com/pukka/jiecheng/index/dialog/HomeLogoutConfirmDialog.kt new file mode 100644 index 0000000..5ee4beb --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/index/dialog/HomeLogoutConfirmDialog.kt @@ -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(R.id.btnConfirm).setOnClickListener { + dismiss() + onConfirm() + } + findViewById(R.id.btnCancel).setOnClickListener { dismiss() } + } + + override fun show() { + super.show() + // 破坏性操作:默认焦点落在"取消",用户需主动右移确认 + findViewById(R.id.btnCancel)?.requestFocus() + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/index/dialog/UserExitDialog.java b/app/src/main/java/com/pukka/jiecheng/index/dialog/UserExitDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/fragment/ContentFragment.java b/app/src/main/java/com/pukka/jiecheng/index/fragment/ContentFragment.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/fragment/HomeMyFragment.kt b/app/src/main/java/com/pukka/jiecheng/index/fragment/HomeMyFragment.kt new file mode 100644 index 0000000..c644930 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/index/fragment/HomeMyFragment.kt @@ -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(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() + } + } + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/index/fragment/HotelMyFragment.java b/app/src/main/java/com/pukka/jiecheng/index/fragment/HotelMyFragment.java old mode 100755 new mode 100644 index 7b1b441..3e24aef --- a/app/src/main/java/com/pukka/jiecheng/index/fragment/HotelMyFragment.java +++ b/app/src/main/java/com/pukka/jiecheng/index/fragment/HotelMyFragment.java @@ -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(); diff --git a/app/src/main/java/com/pukka/jiecheng/index/fragment/MyFragment.java b/app/src/main/java/com/pukka/jiecheng/index/fragment/MyFragment.java old mode 100755 new mode 100644 index 93e8393..ddb46fb --- a/app/src/main/java/com/pukka/jiecheng/index/fragment/MyFragment.java +++ b/app/src/main/java/com/pukka/jiecheng/index/fragment/MyFragment.java @@ -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; } diff --git a/app/src/main/java/com/pukka/jiecheng/index/fragment/ShortVideoFragment.java b/app/src/main/java/com/pukka/jiecheng/index/fragment/ShortVideoFragment.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/manager/FloorOnBindViewHolderManager.java b/app/src/main/java/com/pukka/jiecheng/index/manager/FloorOnBindViewHolderManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/manager/FloorOnCreateViewHolderManager.java b/app/src/main/java/com/pukka/jiecheng/index/manager/FloorOnCreateViewHolderManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/A49ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/A49ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/A50ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/A50ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B10ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B10ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B11ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B11ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B12ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B12ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B13ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B13ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B14ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B14ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B15ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B15ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B1ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B1ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B2ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B2ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B2_2ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B2_2ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B2_3ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B2_3ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B3ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B3ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B4ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B4ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B5ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B5ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B5_2ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B5_2ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B6ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B6ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B7ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B7ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B8ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B8ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/B9ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/B9ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseBannerViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseBannerViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseExVideoViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseExVideoViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseTitleViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseTitleViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/BaseViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/C1ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/C1ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/C2ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/C2ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/C3ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/C3ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/C4ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/C4ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/C5ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/C5ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/C6ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/C6ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/C6_1ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/C6_1ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/C7ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/C7ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/D1ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/D1ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/D2ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/D2ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/D2_1ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/D2_1ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/D3ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/D3ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/D4ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/D4ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/JC1ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/JC1ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/JC32ViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/JC32ViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewholder/ToTopViewHolder.java b/app/src/main/java/com/pukka/jiecheng/index/viewholder/ToTopViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewmodel/ContentViewModel.java b/app/src/main/java/com/pukka/jiecheng/index/viewmodel/ContentViewModel.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewmodel/IndexViewModel.java b/app/src/main/java/com/pukka/jiecheng/index/viewmodel/IndexViewModel.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewmodel/MyViewModel.java b/app/src/main/java/com/pukka/jiecheng/index/viewmodel/MyViewModel.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewmodel/NavViewModel.java b/app/src/main/java/com/pukka/jiecheng/index/viewmodel/NavViewModel.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewmodel/ShortVideoViewModel.java b/app/src/main/java/com/pukka/jiecheng/index/viewmodel/ShortVideoViewModel.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/index/viewmodel/TopViewModel.java b/app/src/main/java/com/pukka/jiecheng/index/viewmodel/TopViewModel.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/list/ListActivity.java b/app/src/main/java/com/pukka/jiecheng/list/ListActivity.java old mode 100755 new mode 100644 index de04b67..62e47fc --- a/app/src/main/java/com/pukka/jiecheng/list/ListActivity.java +++ b/app/src/main/java/com/pukka/jiecheng/list/ListActivity.java @@ -4,6 +4,7 @@ import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.text.Html; +import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; import android.widget.LinearLayout; @@ -39,6 +40,10 @@ public class ListActivity extends BaseActivity { public static final String PARENTID="parentId"; public static final String LISTID="listId"; + /** 语音协议分类跳转携带的分类 id。 */ + public static final String EXTRA_CATEGORY_ID = "category_id"; + /** 语音协议分类跳转携带的分类名。 */ + public static final String EXTRA_CATEGORY_NAME = "category_name"; VerticalRecyclerViewTv mTagRecyclerViewTv; ContentRecyclerViewTv mContentRecyclerViewTv; LinearLayout mllSearch; @@ -118,6 +123,12 @@ public class ListActivity extends BaseActivity { if (intent != null) { parentId = intent.getStringExtra(PARENTID); listId = intent.getStringExtra(LISTID); + // 语音协议分类跳转:category_id 优先 listId; + // category_name 仅用于埋点与标题展示,不参与网络请求。 + String voiceCategoryId = intent.getStringExtra(EXTRA_CATEGORY_ID); + if (!TextUtils.isEmpty(voiceCategoryId)) { + listId = voiceCategoryId; + } LogUtils.d("parentId="+parentId+" listId="+listId); ListAmberManager.listEnter("导航页入口"); ListAmberManager.top_level_classification = parentName; diff --git a/app/src/main/java/com/pukka/jiecheng/list/ListAmberManager.java b/app/src/main/java/com/pukka/jiecheng/list/ListAmberManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/list/ListStyleUtils.java b/app/src/main/java/com/pukka/jiecheng/list/ListStyleUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/list/adapter/ListContentAdapter.java b/app/src/main/java/com/pukka/jiecheng/list/adapter/ListContentAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/list/adapter/ListNavAdapter.java b/app/src/main/java/com/pukka/jiecheng/list/adapter/ListNavAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/list/viewmodel/ListViewModel.java b/app/src/main/java/com/pukka/jiecheng/list/viewmodel/ListViewModel.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/login/HomeLogoutHandler.kt b/app/src/main/java/com/pukka/jiecheng/login/HomeLogoutHandler.kt new file mode 100644 index 0000000..3066d43 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/login/HomeLogoutHandler.kt @@ -0,0 +1,73 @@ +package com.pukka.jiecheng.login + +import com.pukka.jiecheng.login.store.HomeUserStore +import com.pukka.jiecheng.request.AsApiRequester +import com.pukka.jiecheng.utils.JetsenxSdkUtil +import com.pukka.libcore.utils.LogUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +/** + * 家用版扫码登录域的设备登出公共编排。 + * + *

被以下两类入口复用,确保两条路径行为完全一致: + *

    + *
  • [com.pukka.jiecheng.index.fragment.HomeMyFragment.performLogout] — 用户点击"退出登录"按钮
  • + *
  • [com.pukka.jiecheng.request.AsHeartBeatManager.handleRevokeCardSecret] — + * 服务端在心跳响应里下发 `revokeCardSecret` 命令(权益被撤销)时
  • + *
+ * + *

三步顺序执行: + *

    + *
  1. 调 `POST /api/jst/benefit/device/logout` 通知后端设备登出
  2. + *
  3. 清 [HomeUserStore] 全字段(nickname / avatarUrl / unionid / expireTime / loggedIn)
  4. + *
  5. 清进程内 SDK 卡密缓存 [JetsenxSdkUtil.clearReusableUid]
  6. + *
+ * + *

容错策略:第 1 步失败(网络异常或服务端返回非 0)仅打日志,后续两步照常执行 — + * 本地状态必须与服务端"权益已撤销"语义一致,即使用户已确认退出也不会因为网络问题 + * 导致本地仍保留旧用户头像昵称。

+ * + *

协程上下文:心跳回调跑在 [com.pukka.libcore.threading.DispatchQueue.global] 工作线程, + * 不绑定任何 Activity lifecycle;此处用 [GlobalScope] + [Dispatchers.IO] 包装, + * 不依赖 UI 生命周期,即使 Activity 已退也不影响执行。弹框与 `appExit()` 不等待网络结果。

+ */ +object HomeLogoutHandler { + + private const val TAG = "HomeLogoutHandler" + + /** + * 执行设备登出流程。立刻返回(网络请求异步进行), + * 不抛异常 — 所有失败情况仅写日志。 + */ + @JvmStatic + fun performDeviceLogout() { + GlobalScope.launch(Dispatchers.IO) { + try { + LogUtils.d(TAG, "performDeviceLogout: requesting device logout") + AsApiRequester.requestAsLogoutApi() + LogUtils.d(TAG, "performDeviceLogout: device logout api success") + } catch (e: java.lang.Exception) { + // 容错:网络异常 / 服务端错误码 — 仍继续本地清理, + // 与 HomeMyFragment.performLogout 的语义一致。 + // GlobalScope 没有父 Job 可取消,无需也无法继续传播 CancellationException, + // 一律吞掉走日志,避免触发 Thread.UncaughtExceptionHandler 引发的应用崩溃。 + // 收紧到 Exception:把 Error(OOM/StackOverflowError 等 JVM 致命信号)留给 JVM 处理。 + LogUtils.e(TAG, "performDeviceLogout: device logout failed: ${e.message}") + } + try { + HomeUserStore.clear() + LogUtils.d(TAG, "performDeviceLogout: HomeUserStore cleared") + } catch (e: java.lang.Exception) { + LogUtils.e(TAG, "performDeviceLogout: clear HomeUserStore failed: ${e.message}") + } + try { + JetsenxSdkUtil.clearReusableUid() + LogUtils.d(TAG, "performDeviceLogout: JetsenxSdkUtil reusable uid cleared") + } catch (e: java.lang.Exception) { + LogUtils.e(TAG, "performDeviceLogout: clearReusableUid failed: ${e.message}") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/login/KeyboardAdapter.java b/app/src/main/java/com/pukka/jiecheng/login/KeyboardAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/login/LoginActivity.java b/app/src/main/java/com/pukka/jiecheng/login/LoginActivity.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/login/WxMpLoginActivity.java b/app/src/main/java/com/pukka/jiecheng/login/WxMpLoginActivity.java new file mode 100644 index 0000000..cec7690 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/login/WxMpLoginActivity.java @@ -0,0 +1,120 @@ +/** + * @file WxMpLoginActivity.java + * @description 家用版微信扫码登录 Activity(第二阶段:真实扫码)。 + * 1. onCreate:绑定视图,显示悬浮返回按钮; + * 2. onResume:启动 WxMpLoginManager.startLogin(this),订阅 state LiveData; + * 3. Manager.onQrBitmapReady:填充中央 QR ImageView; + * 4. Confirmed 状态:finish() 关闭页面,回到 HomeMyFragment; + * 5. onDestroy:调 WxMpLoginManager.cancelLogin() 释放 Activity 引用。 + * + *

轮询简化逻辑:/me envelope code == 200 即视为微信扫码授权完成; + * 非 200 继续轮询,不区分 401 中间态(用户明确指示)。

+ * @author ainetv + * @date 2026-09-16 + */ +package com.pukka.jiecheng.login; + +import android.graphics.Bitmap; +import android.os.Bundle; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.lifecycle.Observer; + +import com.pukka.jiecheng.R; +import com.pukka.jiecheng.base.BaseActivity; + +public class WxMpLoginActivity extends BaseActivity implements WxMpLoginCallback { + + private ImageView mIvQrCode; + private TextView mTvStatus; + private Button mBtnCancel; + + private Observer mStateObserver; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_wx_mp_login); + + mIvQrCode = findViewById(R.id.id_iv_wx_mp_qr_code); + mTvStatus = findViewById(R.id.id_tv_wx_mp_status); + mBtnCancel = findViewById(R.id.id_btn_wx_mp_login_cancel); + + // 触摸模式悬浮返回按钮(未传监听时默认 finish()) + enableBackButton(); + + // 取消按钮:点击 finish + if (mBtnCancel != null) { + mBtnCancel.setOnClickListener(v -> finish()); + } + + // 一次性创建 state observer:onCreate 创建,避免 onResume→onPause→onResume + // 重复 observeForever 导致旧 lambda 持有旧 Activity 引用泄漏。 + // mTvStatus 仍是字段(非 final),此 lambda 仅在主线程被 LiveData 回调, + // 调用前 Activity 已 attach,空检查 mTvStatus == null 足以防 onDestroy 后竞态。 + mStateObserver = state -> { + if (mTvStatus == null) return; + String text; + if (state instanceof LoginUiState.WaitingQr) { + text = getString(R.string.wx_mp_login_status_waiting_qr); + } else if (state instanceof LoginUiState.WaitingScan) { + text = getString(R.string.wx_mp_login_status_waiting_scan); + } else if (state instanceof LoginUiState.Confirmed) { + // 登录成功 → finish;HomeMyFragment.onResume 会刷新真实状态 + finish(); + return; + } else if (state instanceof LoginUiState.Failed) { + LoginUiState.Failed f = (LoginUiState.Failed) state; + text = getString(R.string.wx_mp_login_status_failed, f.getMessage()); + } else { + return; + } + mTvStatus.setText(text); + }; + } + + @Override + protected void onResume() { + super.onResume(); + // 仅订阅(state observer 已在 onCreate 一次性创建) + WxMpLoginManager.INSTANCE.getState().observeForever(mStateObserver); + + // 启动扫码流程 + WxMpLoginManager.INSTANCE.startLogin(this); + } + + @Override + protected void onPause() { + super.onPause(); + // onPause 反订阅,进一步压缩 observer 持有 Activity 引用窗口: + // onDestroy 在 finish()/back 后仍会兜底 removeObserver + cancelLogin。 + if (mStateObserver != null) { + WxMpLoginManager.INSTANCE.getState().removeObserver(mStateObserver); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mStateObserver != null) { + WxMpLoginManager.INSTANCE.getState().removeObserver(mStateObserver); + mStateObserver = null; + } + // 释放 Manager 持有的 Activity 引用,避免内存泄漏 + WxMpLoginManager.INSTANCE.cancelLogin(); + } + + /** + * WxMpLoginManager 回调:把 login-url 渲染出的 QR Bitmap 喂到 ImageView。 + * 由 WxMpLoginManager 内部通过 withContext(Main) 切到主线程后调用,UI 操作安全。 + */ + @Override + public void onQrBitmapReady(@Nullable Bitmap bitmap) { + if (mIvQrCode != null) { + mIvQrCode.setImageBitmap(bitmap); + } + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/login/WxMpLoginManager.kt b/app/src/main/java/com/pukka/jiecheng/login/WxMpLoginManager.kt new file mode 100644 index 0000000..ae31e09 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/login/WxMpLoginManager.kt @@ -0,0 +1,351 @@ +package com.pukka.jiecheng.login + +import android.graphics.Bitmap +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.pukka.jiecheng.login.store.HomeUserInfo +import com.pukka.jiecheng.login.store.HomeUserStore +import com.pukka.jiecheng.network.ApiRequesters +import com.pukka.jiecheng.network.api.AsApi +import com.pukka.jiecheng.network.dto.CheckReq +import com.pukka.jiecheng.network.dto.DeviceUserDto +import com.pukka.jiecheng.network.dto.LoginUrlDto +import com.pukka.jiecheng.network.from +import com.pukka.jiecheng.network.infra.NetResponse +import com.pukka.jiecheng.utils.JetsenxSdkUtil +import com.pukka.jiecheng.utils.QRCodeUtil +import com.pukka.libcore.utils.ContextUtils +import com.pukka.libcore.utils.LogUtils +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * 家用版扫码登录流程编排(进程级单例)。 + * + *

职责(简化版流程,依据用户明确指示): + *

    + *
  1. 调 login-url 拿 loginUrl,生成二维码 Bitmap,通知 Activity 渲染
  2. + *
  3. 3s 周期轮询 /me;envelope code == 200 + data.user 非空 → 视为微信扫码授权完成; + * 非 200 → 继续轮询(不区分 401 scanned/expired 中间态)
  4. + *
  5. /me 命中 → 调 /me/benefits 拿权益上下文 + 写 HomeUserStore
  6. + *
  7. 跑一次 /api/jst/check(单次),若 thirdCardNo 非空 → 调 JetsenxSdkUtil.cacheReusableUid + ensureGlobalInitAsync
  8. + *
  9. 整个链路串行;任何环节抛 CancellationException 即静默退出(Activity 关闭场景)
  10. + *
+ * + *

线程模型: + * - 顶层 AppScope(进程级 SupervisorJob + Dispatchers.IO); + * - QR 渲染走 withContext(Main)切主线程喂 Bitmap 给 ImageView; + * - UI 状态通过 [state] LiveData 推送,WxMpLoginActivity 观察。

+ * + *

调用方契约: + * - [startLogin] 必须在 [WxMpLoginActivity.onResume] 调一次; + * - [cancelLogin] 必须在 [WxMpLoginActivity.onDestroy] 调,释放 Activity 引用避免内存泄漏。

+ */ +object WxMpLoginManager { + + private const val TAG = "WxMpLoginManager" + + /** /me 轮询周期(ms);按 spec §1.5 + device-integration.md §8.4 取 3s */ + private const val POLL_PERIOD_MS = 3_000L + + /** /login-url 重试最大次数(设备未 init / 配置缺失等);超过后转 Failed */ + private const val LOGIN_URL_RETRY_MAX = 3 + + private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + /** 当前持有的 Activity(供回调刷新 QR / status);Activity onDestroy 须调 cancelLogin 清空 */ + @Volatile private var attachedActivity: WxMpLoginActivity? = null + + /** 当前主流程 Job;cancelLogin 时取消,串行链路随之终止 */ + @Volatile private var loginJob: Job? = null + + private val _state = MutableLiveData(LoginUiState.Idle) + @JvmStatic + val state: LiveData get() = _state + + /** 登录成功事件(用于 IndexActivity 通知 HomeMyFragment 刷新);null 表示无新事件 */ + private val _loginSuccessEvent = MutableLiveData() + @JvmStatic + val loginSuccessEvent: LiveData get() = _loginSuccessEvent + + /** + * 启动一次登录流程。若已在运行则先取消旧 Job,避免与新 Activity 实例冲突。 + */ + @Synchronized + fun startLogin(activity: WxMpLoginActivity) { + attachedActivity = activity + loginJob?.cancel() + loginJob = appScope.launch { + try { + runLoginFlow() + } catch (ce: CancellationException) { + LogUtils.d(TAG, "login flow cancelled (activity destroyed)") + } catch (e: Throwable) { + LogUtils.e(TAG, "login flow crashed: ${e.message}") + _state.postValue(LoginUiState.Failed(e.message ?: "未知错误")) + } + } + } + + /** + * 取消当前在途登录流程,释放 Activity 引用。 + */ + @Synchronized + @JvmStatic + fun cancelLogin() { + loginJob?.cancel() + loginJob = null + attachedActivity = null + } + + /** + * 异步刷新 HomeMyFragment 用户状态(用于 onResume)。 + * + *

先同步读 [HomeUserStore.getUser] 回调(已登录时立即回显头像昵称),再异步调 /me/benefits + * 取最新状态——无论本地是否有缓存(未登录)都请求,覆盖"后端已登录但本地缓存被清"的场景 + * (如 SP 被清/换设备初始化后后端仍记住该设备)。若新响应的字段与缓存不一致 → 写 HomeUserStore + * + 回调 true(强制 UI 重绘);响应中 user 为 null 表示后端也未登录,维持现状。

+ * + * @param scope Fragment lifecycleScope + * @param onResult (info, hasUpdate) hasUpdate=true 表示本地缓存被更新,UI 需要重绘 + */ + @JvmStatic + fun refreshUserState(scope: CoroutineScope, onResult: (HomeUserInfo, Boolean) -> Unit) { + val cached = HomeUserStore.getUser() + onResult(cached, false) + scope.launch { + try { + val resp = withContext(Dispatchers.IO) { + ApiRequesters.ui(scope) + .from() + .action { it.deviceBenefits() } + .execute() + } + when (resp) { + is com.pukka.jiecheng.network.infra.ApiResult.Success -> { + val data = resp.value.data + val user = data?.user + val benefits = data?.benefits + val expireTime = HomeUserStore.pickMaxExpireTime(benefits) + if (user != null) { + val newInfo = HomeUserInfo( + isLoggedIn = true, + nickname = user.nickname, + avatarUrl = user.avatarUrl, + expireTime = expireTime ?: cached.expireTime, + unionid = user.unionid ?: cached.unionid + ) + // 仅当字段有变化才写 + 回调 + val changed = newInfo != cached + if (changed) { + HomeUserStore.saveLogin(user, expireTime) + onResult(newInfo, true) + } + } + } + is com.pukka.jiecheng.network.infra.ApiResult.Failure -> { + LogUtils.w(TAG, "refreshUserState benefits failed: ${resp.error.message}") + } + } + } catch (ce: CancellationException) { + LogUtils.w(TAG, "refreshUserState benefits CancellationException: ${ce.message}") + throw ce + } catch (e: Throwable) { + LogUtils.w(TAG, "refreshUserState benefits exception: ${e.message}") + } + } + } + + // ===================== 内部流程 ===================== + + private suspend fun runLoginFlow() { + _state.postValue(LoginUiState.WaitingQr) + + // 1) 拉 login-url + val loginUrl = fetchLoginUrlWithRetry() + if (loginUrl.isNullOrBlank()) { + _state.postValue(LoginUiState.Failed("生成二维码失败,请稍后重试")) + return + } + + // 2) 渲染 QR + 回调(均需主线程) + withContext(Dispatchers.Main) { + val bmp = QRCodeUtil.createQRCodeBitmap(loginUrl, 600, 600) + attachedActivity?.let { (it as? WxMpLoginCallback)?.onQrBitmapReady(bmp) } + } + + // 3) 轮询 /me:code == 200 → 完成;非 200 → 继续 + _state.postValue(LoginUiState.WaitingScan) + val meUser = pollDeviceMeUntilSuccess() + if (meUser == null) { + // 走到这里只可能是 CancellationException(Activity 关闭);不暴露 Failed 给 UI + return + } + + // 4) 命中 → 拉 benefits + 写 HomeUserStore + 跑一次 /jst/check + onLoginConfirmed(meUser) + } + + /** + * 调 login-url,允许最多 [LOGIN_URL_RETRY_MAX] 次重试。 + * @return 成功时返回 loginUrl 字符串;否则 null + */ + private suspend fun fetchLoginUrlWithRetry(): String? { + var urlDto: LoginUrlDto? = null + var retry = 0 + while (retry < LOGIN_URL_RETRY_MAX && urlDto == null) { + retry++ + val resp = withContext(Dispatchers.IO) { + ApiRequesters.app().from().action { it.loginUrl() }.execute() + } + urlDto = when (resp) { + is com.pukka.jiecheng.network.infra.ApiResult.Success -> { + if (resp.value.code == 0) resp.value.data else { + LogUtils.w(TAG, "login-url business error code=${resp.value.code} msg=${resp.value.msg}") + null + } + } + is com.pukka.jiecheng.network.infra.ApiResult.Failure -> { + LogUtils.w(TAG, "login-url request failed: ${resp.error.message}") + null + } + } + if (urlDto == null && retry < LOGIN_URL_RETRY_MAX) delay(POLL_PERIOD_MS) + } + return urlDto?.loginUrl + } + + /** + * 3s 周期轮询 /me,直到 envelope code == 0 + data 非空。 + * 非 0 一律继续轮询;Activity 关闭时 CancellationException 直接向上抛。 + * + *

data 形态:后端直接平铺 DeviceUserDto 字段(openid/appId/nickname/avatarUrl/unionid), + * 无 user 包装层。data 为 null 视为未授权,继续轮询。

+ */ + private suspend fun pollDeviceMeUntilSuccess(): DeviceUserDto? { + var result: DeviceUserDto? = null + while (result == null) { + delay(POLL_PERIOD_MS) + val resp = withContext(Dispatchers.IO) { + ApiRequesters.app().from().action { it.deviceMe() }.execute() + } + when (resp) { + is com.pukka.jiecheng.network.infra.ApiResult.Success -> { + val netResp: NetResponse = resp.value + val code = netResp.code ?: -1 + if (code == 0 && netResp.data != null) { + // 微信扫码授权完成 + result = netResp.data + } else { + // 非 0 或 data 为空 → 继续轮询 + LogUtils.d(TAG, "/me polling: code=$code msg=${netResp.msg}, continue") + } + } + is com.pukka.jiecheng.network.infra.ApiResult.Failure -> { + // 网络/解析失败也按"非 0"处理,继续轮询 + LogUtils.w(TAG, "/me request failed: ${resp.error.message}, continue") + } + } + } + return result + } + + /** + * /me 命中后的收尾流程: + * 1) 拉 /me/benefits → 取 expireTime(权益 validUntil 最大值) + * 2) 写 HomeUserStore + * 3) 跑一次 /api/jst/check → 若 thirdCardNo 非空 → JetsenxSdkUtil 初始化 + * 4) 状态切到 Confirmed;Activity 收到后 finish + */ + private suspend fun onLoginConfirmed(user: DeviceUserDto) { + val benefitsResp = withContext(Dispatchers.IO) { + ApiRequesters.app().from().action { it.deviceBenefits() }.execute() + } + val expireTime = when (benefitsResp) { + is com.pukka.jiecheng.network.infra.ApiResult.Success -> { + if (benefitsResp.value.code == 0) { + HomeUserStore.pickMaxExpireTime(benefitsResp.value.data?.benefits) + } else { + LogUtils.w(TAG, "benefits business error code=${benefitsResp.value.code}") + null + } + } + is com.pukka.jiecheng.network.infra.ApiResult.Failure -> { + LogUtils.w(TAG, "benefits request failed: ${benefitsResp.error.message}") + null + } + } + + HomeUserStore.saveLogin(user, expireTime) + + // 跑一次 /api/jst/check(spec:检查是否有下发卡密,一次检查即可) + runSingleCardCheck() + + _state.postValue(LoginUiState.Confirmed) + _loginSuccessEvent.postValue(true) + } + + /** + * 单次 /api/jst/check:thirdCardNo 非空 → 写 JetsenxSdkUtil + ensureGlobalInitAsync。 + */ + private suspend fun runSingleCardCheck() { + val resp = withContext(Dispatchers.IO) { + ApiRequesters.app().from().action { it.check(CheckReq()) }.execute() + } + val cardNo = when (resp) { + is com.pukka.jiecheng.network.infra.ApiResult.Success -> { + val dto = resp.value.data + if (resp.value.code == 0 && !dto?.thirdCardNo.isNullOrBlank()) dto?.thirdCardNo else null + } + is com.pukka.jiecheng.network.infra.ApiResult.Failure -> { + LogUtils.w(TAG, "/jst/check request failed: ${resp.error.message}") + null + } + } + if (cardNo != null) { + LogUtils.i(TAG, "card secret issued, init SDK uid=${cardNo.take(4)}***") + JetsenxSdkUtil.cacheReusableUid(cardNo) + JetsenxSdkUtil.ensureGlobalInitAsync( + ContextUtils.getApplicationContext(), + object : JetsenxSdkUtil.FallbackInitCallback { + override fun onReady(memberCode: String) { + LogUtils.i(TAG, "post-login SDK init success memberCode=$memberCode") + } + override fun onNoEntitlement(businessCode: Int, message: String) { + LogUtils.w(TAG, "post-login SDK init no entitlement code=$businessCode msg=$message") + } + override fun onNetworkError(businessCode: Int, message: String) { + LogUtils.w(TAG, "post-login SDK init network error code=$businessCode msg=$message") + } + } + ) + } else { + LogUtils.d(TAG, "/jst/check: no card secret issued, skip SDK init") + } + } +} + +/** + * 登录 UI 状态机(简化版:无 Expired 中间态)。 + */ +sealed class LoginUiState { + object Idle : LoginUiState() + object WaitingQr : LoginUiState() + object WaitingScan : LoginUiState() + object Confirmed : LoginUiState() + data class Failed(val message: String) : LoginUiState() +} + +/** + * WxMpLoginActivity 暴露给 Manager 用的回调接口(避免 Activity 直接依赖 Manager 私有状态)。 + */ +interface WxMpLoginCallback { + fun onQrBitmapReady(bitmap: Bitmap?) +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/login/bean/GetCloudLoginBean.java b/app/src/main/java/com/pukka/jiecheng/login/bean/GetCloudLoginBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/login/bean/GetQueryUsableBean.java b/app/src/main/java/com/pukka/jiecheng/login/bean/GetQueryUsableBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/login/bean/LoginCodeBean.java b/app/src/main/java/com/pukka/jiecheng/login/bean/LoginCodeBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/login/bean/LoginLogonBean.java b/app/src/main/java/com/pukka/jiecheng/login/bean/LoginLogonBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/login/bean/QueryVipInfoBean.java b/app/src/main/java/com/pukka/jiecheng/login/bean/QueryVipInfoBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/login/store/HomeUserStore.kt b/app/src/main/java/com/pukka/jiecheng/login/store/HomeUserStore.kt new file mode 100644 index 0000000..3ca8c70 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/login/store/HomeUserStore.kt @@ -0,0 +1,103 @@ +package com.pukka.jiecheng.login.store + +import android.content.Context +import android.content.SharedPreferences +import com.pukka.jiecheng.network.dto.DeviceBenefitItemDto +import com.pukka.jiecheng.network.dto.DeviceUserDto +import com.pukka.libcore.utils.ContextUtils + +/** + * 家用版扫码登录用户信息本地缓存。 + * + *

为什么不复用 [BoxSharedParameters]:第一阶段 spec §1.3 + §7 明确"不动 BoxSharedParameters", + * 商用版 HotelMyFragment 也读 BoxSharedParameters,直接往里写字段会污染商用版行为。 + * 独立 SP 文件(`home_user_prefs`)按 flavor 隔离,即使 applicationId 相同也不会跨 flavor 串数据。

+ * + *

字段语义: + * - nickname / avatarUrl: 微信昵称与头像(用于 HomeMyFragment 已登录态头部展示) + * - expireTime: ISO-8601 字符串,取自权益列表中 validUntil(按 validUntil 倒序取最大者) + * - unionid: 唯一标识,SDK init / 退出登录时透传给 JetsenxSdkUtil + * - isLoggedIn: 持久化登录标记(nickname 非空 → true;clear() 后 → false)

+ * + *

线程安全:SharedPreferences 自身线程安全(getXxx / edit / put 均为同步);无需额外加锁。

+ */ +object HomeUserStore { + + private const val PREFS_NAME = "home_user_prefs" + private const val KEY_NICKNAME = "nickname" + private const val KEY_AVATAR_URL = "avatarUrl" + private const val KEY_EXPIRE_TIME = "expireTime" + private const val KEY_UNIONID = "unionid" + private const val KEY_LOGGED_IN = "loggedIn" + + private fun prefs(): SharedPreferences = + ContextUtils.getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + /** + * 读取当前已登录用户信息(从 SP 同步读)。 + * + * @return 默认返回 `HomeUserInfo(isLoggedIn=false, ...)` 表示未登录 + */ + @JvmStatic + fun getUser(): HomeUserInfo { + val sp = prefs() + val loggedIn = sp.getBoolean(KEY_LOGGED_IN, false) + if (!loggedIn) return HomeUserInfo(isLoggedIn = false, nickname = null, avatarUrl = null, expireTime = null, unionid = null) + return HomeUserInfo( + isLoggedIn = true, + nickname = sp.getString(KEY_NICKNAME, null), + avatarUrl = sp.getString(KEY_AVATAR_URL, null), + expireTime = sp.getString(KEY_EXPIRE_TIME, null), + unionid = sp.getString(KEY_UNIONID, null) + ) + } + + /** + * 写入登录态(由 WxMpLoginManager 命中 /me 后调用)。 + * + * @param user /me 返回的用户身份(nickname / avatarUrl / unionid) + * @param expireTime 取自 /me/benefits 的权益 validUntil 最大值(无权益时传 null) + */ + @JvmStatic + fun saveLogin(user: DeviceUserDto, expireTime: String?) { + val sp = prefs() + sp.edit().apply { + putBoolean(KEY_LOGGED_IN, true) + putString(KEY_NICKNAME, user.nickname) + putString(KEY_AVATAR_URL, user.avatarUrl) + putString(KEY_UNIONID, user.unionid) + if (expireTime != null) putString(KEY_EXPIRE_TIME, expireTime) + else remove(KEY_EXPIRE_TIME) + }.apply() + } + + /** + * 清除登录态(退出登录 / token 过期)。 + */ + @JvmStatic + fun clear() { + prefs().edit().clear().apply() + } + + /** + * 仅从权益列表提取最大 validUntil(按 ISO-8601 字符串字典序即可,与 JsapiOrderMonitor.parseIso8601ToMillis 同源)。 + * + * @return ISO-8601 字符串;权益列表为空时返回 null + */ + @JvmStatic + fun pickMaxExpireTime(benefits: List?): String? { + if (benefits.isNullOrEmpty()) return null + return benefits.mapNotNull { it.validUntil }.maxOrNull() + } +} + +/** + * 已登录用户信息快照(用于 HomeMyFragment 直接渲染)。 + */ +data class HomeUserInfo( + val isLoggedIn: Boolean, + val nickname: String?, + val avatarUrl: String?, + val expireTime: String?, + val unionid: String? +) diff --git a/app/src/main/java/com/pukka/jiecheng/login/viewmodel/IsLoginViewModel.java b/app/src/main/java/com/pukka/jiecheng/login/viewmodel/IsLoginViewModel.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/monitor/HomeMonitor.java b/app/src/main/java/com/pukka/jiecheng/monitor/HomeMonitor.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/monitor/NetworkMonitor.java b/app/src/main/java/com/pukka/jiecheng/monitor/NetworkMonitor.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/ApiRequesters.kt b/app/src/main/java/com/pukka/jiecheng/network/ApiRequesters.kt old mode 100755 new mode 100644 index 295d5df..fb2a099 --- a/app/src/main/java/com/pukka/jiecheng/network/ApiRequesters.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/ApiRequesters.kt @@ -391,13 +391,23 @@ class ResultBuilder( /** * 触发回调钩子(内部,调用方需要在 [deferred] 完成时调用)。 * 由 [ApiCallBuilder.build] 在 init 块中通过 [deferred].invokeOnCompletion 注册一次。 + * + * CE 短路:调用方 scope 被取消时,build() 协程已通过 throw ce 完成结构化取消传播, + * 不应再让用户业务层收到任何通知,否则: + * - 用户 onFailure 链路里的 `if (e is CancellationException) throw e` 会让 CE + * 从 invokeOnCompletion 回调内抛出 → JobSupport.completeStateFinalization 把它 + * 二次打包为 CompletionHandlerException(污染 logcat + 干扰上游 CEH)。 + * - 因此 result 是 Result.failure(ce) 时静默 return,连回调都不触发。 */ @PublishedApi internal fun triggerHook(result: Result) { + if (result.exceptionOrNull() is CancellationException) return try { onResultHook?.invoke(result) + } catch (e: CancellationException) { + // 兜底:hook 链路里如果仍有 CE 透传(理论上不应触发),静默吞掉, + // 绝不能再从 invokeOnCompletion handler 内抛 CE。 } catch (e: Throwable) { - if (e is CancellationException) throw e // 业务回调抛出非取消异常,吞掉避免影响协程。记录日志便于排查。 android.util.Log.e("ResultBuilder", "onResultHook threw", e) } @@ -465,7 +475,9 @@ inline fun ResultBuilder>.map(): ResultBuilder { originalHook ) } catch (ce: CancellationException) { - newDeferred.complete(Result.failure(ce)) + // CE 短路:调用方 scope 被取消 → newDeferred 直接 cancel, + // 不要 complete(failure(ce)),否则 newBuilder.invokeOnCompletion 会再次触发 CompletionHandlerException。 + newDeferred.cancel() } catch (e: Throwable) { newDeferred.complete(Result.failure(e)) } @@ -514,8 +526,11 @@ fun ResultBuilder.retry( val parentScope = this.scope val newBuilder = ResultBuilder(this.job, parentScope, this.deferred, plan) - newBuilder.onResultHook = { firstResult -> - firstResult.fold( + newBuilder.onResultHook = hook@{ r -> + // CE 短路:scope 取消时不应启动重试协程,也不应触发任何用户回调。 + // 取消传播由 build() 协程的 throw ce 负责。 + if (r.exceptionOrNull() is CancellationException) return@hook + r.fold( onSuccess = { v -> originalHook?.invoke(Result.success(v)) }, onFailure = { firstError -> if (firstError is CancellationException) throw firstError diff --git a/app/src/main/java/com/pukka/jiecheng/network/ApiRequesters_MapHelper.kt b/app/src/main/java/com/pukka/jiecheng/network/ApiRequesters_MapHelper.kt old mode 100755 new mode 100644 index a5639de..1f96e8f --- a/app/src/main/java/com/pukka/jiecheng/network/ApiRequesters_MapHelper.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/ApiRequesters_MapHelper.kt @@ -62,8 +62,10 @@ internal object ApiRequesters_MapHelper { }, onFailure = { e -> if (e is CancellationException) { - newDeferred.complete(Result.failure(e)) - throw e + // CE 短路:调用方(map() 的 invokeOnCompletion handler)会 cancel newDeferred, + // 这里静默返回 —— 不能从 invokeOnCompletion handler 内抛 CE, + // 否则会触发 CompletionHandlerException。 + return@fold } newDeferred.complete(Result.failure(e)) invokeHookSafely("completeMap:transport-hook", originalHook, Result.failure(e)) diff --git a/app/src/main/java/com/pukka/jiecheng/network/api/AsApi.kt b/app/src/main/java/com/pukka/jiecheng/network/api/AsApi.kt old mode 100755 new mode 100644 index 15a87f2..0a7477f --- a/app/src/main/java/com/pukka/jiecheng/network/api/AsApi.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/api/AsApi.kt @@ -4,16 +4,21 @@ import com.pukka.jiecheng.network.dto.ActivateReq import com.pukka.jiecheng.network.dto.CheckBenefitsReq import com.pukka.jiecheng.network.dto.CheckDto import com.pukka.jiecheng.network.dto.CheckReq +import com.pukka.jiecheng.network.dto.DeviceBenefitsDto +import com.pukka.jiecheng.network.dto.DeviceUserDto import com.pukka.jiecheng.network.dto.HeartBeatReq import com.pukka.jiecheng.network.dto.HeartBeatDto import com.pukka.jiecheng.network.dto.InitDto import com.pukka.jiecheng.network.dto.InitReq +import com.pukka.jiecheng.network.dto.LogoutDto +import com.pukka.jiecheng.network.dto.LoginUrlDto import com.pukka.jiecheng.network.dto.RefreshSignDto import com.pukka.jiecheng.network.dto.RefreshSignReq import com.pukka.jiecheng.network.dto.TrackingDto import com.pukka.jiecheng.network.dto.TrackingReq import com.pukka.jiecheng.network.infra.NetResponse import retrofit2.http.Body +import retrofit2.http.GET import retrofit2.http.POST /** @@ -41,7 +46,7 @@ interface AsApi { @POST("api/jst/checkBenefits") suspend fun checkBenefits(@Body req: CheckBenefitsReq): NetResponse - /** 刷新 sign。Authenticator 401 时调用,SignRequestPolicy 已将其排除出 sign 注入与刷新触发列表。 */ + /** 刷新 sign。Authenticator 401 时调用,SignRequestPolicy 已把其排除出刷新触发列表(避免自己拦截自己死循环);HeaderInterceptor 仍会注入旧 sign 头供服务端鉴权。 */ @POST("api/jst/refreshSign") suspend fun refreshSign(@Body req: RefreshSignReq): NetResponse @@ -52,4 +57,47 @@ interface AsApi { /** 埋点上报(fire-and-forget)。 */ @POST("api/jst/tracking") suspend fun tracking(@Body req: TrackingReq): NetResponse + + /** + * GET /api/jst/benefit/device/login-url + * + *

生成(或复用 PENDING)登录二维码(device-integration.md §3)。 + * 响应 data.loginUrl 直接喂给 QR 渲染库; + * isNew=true 表示屏幕 QR 需要重渲染,false 表示可继续使用旧 QR。

+ * + *

错误:500 系列(设备不存在 / 支付配置缺失 / 支付配置 appId 缺失 / H5 登录域名未配置) + * 由 service 层抛错;envelope code!=0 业务错误由调用方按 msg 分支处理。

+ */ + @GET("api/jst/benefit/device/login-url") + suspend fun loginUrl(): NetResponse + + /** + * GET /api/jst/benefit/device/me + * + *

轮询扫码用户身份(device-integration.md §4)。 + * 响应 data 直接是 DeviceUserDto(后端无 user 包装层);调用方简化逻辑: + * envelope code == 0 + data 非空 → 视为微信扫码授权完成,停止轮询; + * 非 0 一律继续轮询,不区分 401 "scanned 等待授权" 与 "过期" 中间态 + * (用户明确指定的简化语义)。

+ */ + @GET("api/jst/benefit/device/me") + suspend fun deviceMe(): NetResponse + + /** + * GET /api/jst/benefit/device/me/benefits + * + *

查询当前扫码用户的权益上下文(device-integration.md §5), + * 用于 HomeMyFragment 已登录态头部展示(头像 / 昵称 / 会员到期日)。

+ */ + @GET("api/jst/benefit/device/me/benefits") + suspend fun deviceBenefits(): NetResponse + + /** + * POST /api/jst/benefit/device/logout + * + *

设备侧登出(微信扫码登录的逆操作)。无请求体;sign 请求头由 HeaderInterceptor + * 自动注入(本路径不在 SignRequestPolicy 排除名单内)。成功响应 code == 0,data 恒为 null。

+ */ + @POST("api/jst/benefit/device/logout") + suspend fun deviceLogout(): NetResponse } \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/network/api/BenefitOrderApi.kt b/app/src/main/java/com/pukka/jiecheng/network/api/BenefitOrderApi.kt old mode 100755 new mode 100644 index b77c121..5cc195e --- a/app/src/main/java/com/pukka/jiecheng/network/api/BenefitOrderApi.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/api/BenefitOrderApi.kt @@ -1,8 +1,12 @@ package com.pukka.jiecheng.network.api +import com.pukka.jiecheng.network.dto.BenefitJsapiOrderDto +import com.pukka.jiecheng.network.dto.BenefitJsapiOrderReq +import com.pukka.jiecheng.network.dto.BenefitJsapiStatusDto import com.pukka.jiecheng.network.dto.BenefitOrderReq import com.pukka.jiecheng.network.dto.BenefitOrderRespDto import com.pukka.jiecheng.network.dto.BenefitOrderStatusDto +import com.pukka.jiecheng.network.dto.BenefitPriceDto import com.pukka.jiecheng.network.infra.NetResponse import retrofit2.http.Body import retrofit2.http.GET @@ -42,4 +46,42 @@ interface BenefitOrderApi { */ @GET("api/jst/benefit/order/status/{orderNo}") suspend fun status(@Path("orderNo") orderNo: String): NetResponse + + /** + * 查询门店在售套餐列表(家用版订购中心列表数据源)。 + * + *

envelope 约定:code == 0 成功(jsapi 系列专用,区别于其他 AS 接口的 200), + * 调用方需手判 resp.code == 0 后取 data.items,不能用 .map<T>()(其解包 code==200)。

+ * + * @return data.items 套餐列表(cardType / priceFen / durationHours / goodsName / description) + */ + @POST("api/jst/benefit/order/price") + suspend fun price(): NetResponse + + /** + * 创建/复用 H5 扫码订购订单(D-02,Redis 锁键 jsapi:benefit:order:device:{deviceId},独立于 Native 流程)。 + * + *

请求体 cardType 取自套餐列表选中项;响应 h5Url 生成二维码供用户扫码进入 H5 订购页; + * expireTime 为 ISO-8601 字符串(订单 15 分钟后过期),到期需重新 create(可复用未过期订单)。

+ * + *

envelope 约定:code == 0 成功;业务错误码 400(STORE_NOT_BOUND / DEVICE_NOT_FOUND / + * CARD_TYPE_DISABLED)、500(BENEFIT_CARD_OUT_OF_STOCK / ORDER_CREATE_RETRY),msg 携带描述。

+ */ + @POST("api/jst/benefit/order/jsapi/create") + suspend fun jsapiCreate(@Body req: BenefitJsapiOrderReq): NetResponse + + /** + * 轮询订单状态(家用版 H5 扫码订购 → 阶段 3)。 + * + *

create 成功取得 orderNo 后,由 [com.pukka.jiecheng.order.JsapiOrderHelper] + * 周期调用(默认 10s 延迟 + 5s 周期,参见 §3.1 阶段 3);命中 + * {PAID, ACTIVATING, ACTIVE} 进入阶段 4(check 卡密轮询),命中 CANCEL 清缓存重 create。

+ * + *

envelope 约定:code == 0 成功;404 ORDER_NOT_FOUND(订单不存在)/ 403 PERMISSION_DENIED + * (设备不匹配)等错误码在 data.status 缺失时由调用方按 envelope.code 判定。

+ * + * @param orderNo 来自 jsapiCreate 响应的 orderNo + */ + @GET("api/jst/benefit/order/jsapi/status/{orderNo}") + suspend fun jsapiStatus(@Path("orderNo") orderNo: String): NetResponse } \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/network/api/StoreVersionApi.kt b/app/src/main/java/com/pukka/jiecheng/network/api/StoreVersionApi.kt new file mode 100644 index 0000000..5bf3962 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/network/api/StoreVersionApi.kt @@ -0,0 +1,28 @@ +package com.pukka.jiecheng.network.api + +import com.pukka.jiecheng.network.dto.StoreVersionDto +import com.pukka.jiecheng.network.dto.StoreVersionReq +import com.pukka.jiecheng.network.infra.NetResponse +import retrofit2.http.Body +import retrofit2.http.POST + +/** + * App 内应用更新接口(domain="AS")。 + * + * 用途:酒店版"我的"页面手动触发升级检查。 + * baseUrl 已为 https://jst.ainetv.cn/api/,故 path 仅写 api/jst/device/store-version。 + * + * Header 注入:HeaderInterceptor 会自动注入 sign / X-Channel-Id / + * X-Client-Version / X-Device-Id;该端点不在 SignRequestPolicy 黑名单中, + * 因此会带 sign 头。 + */ +interface StoreVersionApi { + + /** + * 手动触发升级检查:服务端返回最新版本信息(含下载 URL)。 + * + * 响应 data.version 块结构由 [StoreVersionDto] 承载。 + */ + @POST("api/jst/device/store-version") + suspend fun query(@Body req: StoreVersionReq): NetResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/network/auth/HeaderInterceptor.kt b/app/src/main/java/com/pukka/jiecheng/network/auth/HeaderInterceptor.kt old mode 100755 new mode 100644 index a20ed2a..5b3ad32 --- a/app/src/main/java/com/pukka/jiecheng/network/auth/HeaderInterceptor.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/auth/HeaderInterceptor.kt @@ -26,8 +26,7 @@ class HeaderInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val original = chain.request() - // OkHttp 3.12 兼容:Java 版字段访问需用方法调用(url()),4.x 的 Kotlin val 语法不可用 - val path = SignRequestPolicy.relativePath(original.url().toString()) + val path = SignRequestPolicy.relativePath(original.url.toString()) val builder = original.newBuilder() diff --git a/app/src/main/java/com/pukka/jiecheng/network/auth/SignAuthenticator.kt b/app/src/main/java/com/pukka/jiecheng/network/auth/SignAuthenticator.kt old mode 100755 new mode 100644 index 9465172..7362c35 --- a/app/src/main/java/com/pukka/jiecheng/network/auth/SignAuthenticator.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/auth/SignAuthenticator.kt @@ -1,62 +1,32 @@ package com.pukka.jiecheng.network.auth -import com.pukka.jiecheng.network.api.AsApi -import com.pukka.jiecheng.network.dto.RefreshSignReq -import com.pukka.jiecheng.network.infra.ApiException -import com.pukka.jiecheng.network.infra.EnvProvider -import com.pukka.jiecheng.network.infra.Network -import com.pukka.jiecheng.network.infra.RetrofitFactory -import kotlinx.coroutines.runBlocking import okhttp3.Authenticator import okhttp3.Request import okhttp3.Response import okhttp3.Route /** - * 401 单飞刷新 Authenticator。 + * HTTP 401 单飞刷新 Authenticator。 * - * 五层保护(与原 AsApiRequester.java 行为等价,但用协程重构): - * 1. **首签豁免**(SignRequestPolicy.shouldTriggerRefresh):首签/刷新接口自身 401 直接放行,避免无限重试 + * 职责:响应 OkHttp 在收到 HTTP 401/407 时回调,给原 Request 注入新 sign 并重发。 + * + * 与业务码 401 的关系: + * - HTTP 401 由本 Authenticator 处理(OkHttp 内部机制) + * - 业务码 401(HTTP 200 + envelope.code=401)由 [BusinessCodeRefreshInterceptor] 处理 + * - 两条路径共享 [SignRefresher] 的单飞 / 熔断 / 凭据级失败状态,避免分叉 + * + * 三层自身判定(首签豁免 / X-Refresh-Call / priorResponse): + * 1. **首签豁免**([SignRequestPolicy.shouldTriggerRefresh]):首签/刷新接口自身 401 直接放行 * 2. **递归保护 A**(X-Refresh-Call 标记):refreshSign 同步调用发出的请求带 X-Refresh-Call=1,Authenticator 见到此标记直接放行 * 3. **递归保护 B**(priorResponse 链):OkHttp 在重试链上设置 priorResponse,若 priorResponse 已带 X-Refresh-Marker 则放弃二次刷新 - * 4. **凭据级失败**(credentialBroken):refreshSign 自身 401 时置 true,会话内所有 401 放行(让业务感知 401 走登录流程) - * 5. **冷却熔断**(failedUntilMs):瞬时刷新失败后置时间戳,30s 冷却期满后才能再次尝试刷新,避免任何一次 500/超时永久锁死 401 刷新 * * 关于 CancellationException:runBlocking 包裹同步调用,内部不会触发协程取消。 * 如调用方处于已 cancel 的协程中,OkHttp Authenticator 链路同步执行,不会受外部协程取消影响(语义正确)。 */ class SignAuthenticator : Authenticator { - /** - * 单飞锁:替代原 AsApiRequester.sIsRefreshingSign。 - * 用 synchronized 保证多 OkHttp 线程同时进入 Authenticator 时只有一个去刷新,其余等待。 - */ - private val refreshLock = Any() - - /** - * 冷却窗口截止时间戳(epoch millis)。0 = 未熔断。 - * - * 设计:原实现 (failedRefresh: Boolean) 任意一次刷新失败都置 true 后永不重试; - * 改为时间窗口后,瞬时故障(500/超时)只会让冷却期内 401 放行,30s 后即可重新尝试刷新。 - */ - @Volatile - private var failedUntilMs: Long = 0L - - /** - * 熔断冷却窗口长度(毫秒)。30s 足以避开短时服务端故障,又能在用户重新触发业务时给一次重试机会。 - */ - private val failCooldownMs: Long = 30_000L - - /** - * 凭据级失败:refreshSign 接口自身返回 401 或明确的"凭据失效"错误。 - * 与冷却熔断的区别:这是服务端明确判定 sign 不能再用,应让业务立即走登录流程,不应再尝试。 - */ - @Volatile - private var credentialBroken: Boolean = false - override fun authenticate(route: Route?, response: Response): Request? { - // OkHttp 3.12 兼容:Java 版 request()/priorResponse() 方法调用(4.x Kotlin val 语法不可用) - val path = SignRequestPolicy.relativePath(response.request().url().toString()) + val path = SignRequestPolicy.relativePath(response.request.url.toString()) // 保护 1:首签/刷新接口 401 直接放行 if (!SignRequestPolicy.shouldTriggerRefresh(path)) { @@ -64,112 +34,28 @@ class SignAuthenticator : Authenticator { } // 保护 2:刷新调用本身带 X-Refresh-Call=1 标记,Authenticator 见到放行 - if (response.request().header(HEADER_REFRESH_CALL) == "1") { + if (response.request.header(HEADER_REFRESH_CALL) == "1") { return null } // 保护 3:priorResponse 链上已有 X-Refresh-Marker,说明已刷新过一次仍 401,放弃 - val prior = response.priorResponse() + val prior = response.priorResponse if (prior != null && prior.header(HEADER_REFRESH_MARKER) != null) { return null } - // 保护 4:凭据级失败标记 — 不再刷新,让业务感知 401 走登录流程 - if (credentialBroken) { - return null - } - - // 保护 5:冷却熔断 — 冷却期内直接放行(避免每次 401 都重启刷新流程) - if (failedUntilMs > 0L && System.currentTimeMillis() < failedUntilMs) { - return null - } - - // 单飞:抢到锁的线程执行刷新,其余线程直接重试原 request(即使 sign 过期大概率仍 401,但避免并发刷新) - synchronized(refreshLock) { - // 双重检查:进入临界区后再判熔断与 prior marker - if (credentialBroken) return null - if (failedUntilMs > 0L && System.currentTimeMillis() < failedUntilMs) return null - - return try { - val newSign = doRefreshSign() - if (newSign.isNullOrEmpty()) { - // 刷新返回空 sign:进入冷却窗口,避免连续击穿 - failedUntilMs = System.currentTimeMillis() + failCooldownMs - null - } else { - // 成功:清熔断 - failedUntilMs = 0L - // 双写:TokenStore + 同步 SharedPreferences - // 旧 AsApiRequester 路径与 Java 兼容层可能直读 SharedPreferenceUtils.get(AS_SIGN_KEY), - // 仅写 SpTokenStore.apply() 异步落盘会导致冷启动读到过期 sign,引发持续 401。 - Network.tokenStore.refreshAndPersist(newSign) - - // 重建原 Request:保留所有原有 headers,注入新 sign 头 + X-Refresh-Marker 标记 - response.request().newBuilder() - .header("sign", newSign) - .header(HEADER_REFRESH_MARKER, "1") - .build() - } - } catch (e: CredentialBrokenException) { - // doRefreshSign 抛凭据级错误 → 整会话不再刷新,让业务走登录 - credentialBroken = true - failedUntilMs = 0L - null - } catch (e: Exception) { - // 其他异常(IO/解析)→ 冷却窗口,避免每次 401 重复刷新 - failedUntilMs = System.currentTimeMillis() + failCooldownMs - null - } + // 委托 SignRefresher:单飞锁 + 熔断 + 凭据级失败状态由其统一管理 + return when (val result = SignRefresher.refresh()) { + is SignRefresher.Result.Success -> response.request.newBuilder() + .header("sign", result.newSign) + .header(HEADER_REFRESH_MARKER, "1") + .build() + SignRefresher.Result.CredentialBroken, + SignRefresher.Result.Cooldown, + SignRefresher.Result.Failed -> null } } - /** - * 同步调用 refreshSign(OkHttp Authenticator 接口本身同步)。 - * - * 使用专用 Retrofit 实例 + 给请求加 X-Refresh-Call=1 头(HeaderInterceptor 见到此标记不注入旧 sign, - * 但 SignRequestPolicy.shouldTriggerRefresh 已把 refreshSign 排除刷新路径,故 Authenticator 也不再拦截)。 - * - * 凭据级失败(refreshSign 自身 401 / 业务码 401)抛 [CredentialBrokenException],调用方应让用户去登录。 - * 其它失败(IO/解析)返回 null 走冷却熔断路径。 - */ - private fun doRefreshSign(): String? { - return runBlocking { - try { - val api = RetrofitFactory.createApi(EnvProvider.AS_DOMAIN) - val resp = api.refreshSign(RefreshSignReq()) - val sign = resp.data?.sign - if (sign.isNullOrEmpty()) { - // refreshSign 返回 200 但 data 为空:极少见,按凭据级失败处理 - throw CredentialBrokenException("refreshSign returned empty sign") - } - sign - } catch (e: ApiException.BusinessException) { - // 业务码判定:401 系列代表服务端明确拒绝当前 sign - if (e.code in 401..499) { - throw CredentialBrokenException("refreshSign business code=${e.code}", e) - } - null - } catch (e: retrofit2.HttpException) { - // 401/403 这种服务端身份拒绝,抛凭据级错误 - if (e.code() in 401..403) { - throw CredentialBrokenException("refreshSign http=${e.code()}", e) - } - null - } catch (e: Exception) { - null - } - } - } - - /** - * 凭据级失败异常:refreshSign 自身被服务端拒绝(401/403/业务 401)时抛出。 - * Authenticator 捕获后置 credentialBroken = true 不再重试,让业务走登录流程。 - */ - private class CredentialBrokenException( - message: String, - cause: Throwable? = null - ) : RuntimeException(message, cause) - companion object { /** * 内部标记头,用于 Authenticator 内部传递状态,不应被业务感知。 @@ -177,4 +63,4 @@ class SignAuthenticator : Authenticator { internal const val HEADER_REFRESH_CALL = "X-Refresh-Call" internal const val HEADER_REFRESH_MARKER = "X-Refresh-Marker" } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/pukka/jiecheng/network/auth/SignRefresher.kt b/app/src/main/java/com/pukka/jiecheng/network/auth/SignRefresher.kt new file mode 100644 index 0000000..485e140 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/network/auth/SignRefresher.kt @@ -0,0 +1,149 @@ +package com.pukka.jiecheng.network.auth + +import com.pukka.jiecheng.network.api.AsApi +import com.pukka.jiecheng.network.dto.RefreshSignReq +import com.pukka.jiecheng.network.infra.ApiException +import com.pukka.jiecheng.network.infra.EnvProvider +import com.pukka.jiecheng.network.infra.Network +import com.pukka.jiecheng.network.infra.RetrofitFactory +import kotlinx.coroutines.runBlocking + +/** + * sign 同步刷新器(从 SignAuthenticator 抽出,供 HTTP 401 与业务码 401 两条路径共享)。 + * + * 五层保护: + * 1. **首签豁免**:refreshSign 自身不参与刷新(路径白名单已在外层处理) + * 2. **递归保护 A**:同进程内多请求并发只刷新一次(synchronized 单飞) + * 3. **递归保护 B**:refreshSign 自身 401/403/业务 401 → 凭据级失败,会话内不再刷新 + * 4. **凭据级失败**:credentialBroken = true 后所有刷新请求直接返回 CredentialBroken + * 5. **冷却熔断**:IO/解析/500 失败后置 30s 冷却窗口,期间内请求直接返回 Cooldown + * + * 调用方契约: + * - HTTP 401 由 [SignAuthenticator] 调用 → 拿 Success 时构造带 X-Refresh-Marker=1 的 Request + * - 业务码 401 由 [BusinessCodeRefreshInterceptor] 调用 → 拿 Success 时构造带 X-Business-Retry=1 的 Request + * - 两种调用方共享同一份状态(refreshLock / credentialBroken / failedUntilMs),避免分叉 + */ +object SignRefresher { + + sealed class Result { + data class Success(val newSign: String) : Result() + object CredentialBroken : Result() + object Cooldown : Result() + object Failed : Result() + } + + /** + * 单飞锁:替代原 SignAuthenticator.sIsRefreshingSign。 + * 用 synchronized 保证多 OkHttp 线程同时进入时只有一个去刷新,其余立刻拿到当前结果。 + */ + private val refreshLock = Any() + + /** + * 冷却窗口截止时间戳(epoch millis)。0 = 未熔断。 + * + * 设计:原实现 (failedRefresh: Boolean) 任意一次刷新失败都置 true 后永不重试; + * 改为时间窗口后,瞬时故障(500/超时)只会让冷却期内刷新请求放行,30s 后即可重新尝试。 + */ + @Volatile + private var failedUntilMs: Long = 0L + + /** + * 熔断冷却窗口长度(毫秒)。30s 足以避开短时服务端故障,又能在用户重新触发业务时给一次重试机会。 + */ + private val failCooldownMs: Long = 30_000L + + /** + * 凭据级失败:refreshSign 接口自身返回 401 或明确的"凭据失效"错误。 + * 与冷却熔断的区别:这是服务端明确判定 sign 不能再用,应让业务立即走登录流程,不应再尝试。 + */ + @Volatile + private var credentialBroken: Boolean = false + + /** + * 同步刷新入口。返回 [Result] 让调用方根据状态决定下一步(重发 / 透传 / 走登录)。 + * + * 线程安全:整个方法在 [refreshLock] 内串行执行。 + */ + fun refresh(): Result = synchronized(refreshLock) { + // 双重检查:进入临界区后再判熔断 + if (credentialBroken) return Result.CredentialBroken + if (failedUntilMs > 0L && System.currentTimeMillis() < failedUntilMs) { + return Result.Cooldown + } + + return try { + val newSign = doRefreshSign() + if (newSign.isNullOrEmpty()) { + // 刷新返回空 sign:进入冷却窗口,避免连续击穿 + failedUntilMs = System.currentTimeMillis() + failCooldownMs + Result.Failed + } else { + // 成功:清熔断 + failedUntilMs = 0L + // 双写:TokenStore + 同步 SharedPreferences + // 旧 AsApiRequester 路径与 Java 兼容层可能直读 SharedPreferenceUtils.get(AS_SIGN_KEY), + // 仅写 SpTokenStore.apply() 异步落盘会导致冷启动读到过期 sign,引发持续 401。 + Network.tokenStore.refreshAndPersist(newSign) + Result.Success(newSign) + } + } catch (e: CredentialBrokenException) { + // refreshSign 抛凭据级错误 → 整会话不再刷新,让业务走登录 + credentialBroken = true + failedUntilMs = 0L + Result.CredentialBroken + } catch (e: Exception) { + // 其他异常(IO/解析)→ 冷却窗口,避免每次 401 重复刷新 + failedUntilMs = System.currentTimeMillis() + failCooldownMs + Result.Failed + } + } + + /** + * 同步调用 refreshSign(OkHttp Authenticator 与 NetworkInterceptor 链路均同步)。 + * + * HeaderInterceptor 注入旧 sign 头供服务端鉴权(refreshSign 已在 SignRequestPolicy 中排除刷新触发, + * 故 Authenticator 与 BusinessCodeRefreshInterceptor 均不拦截 refreshSign 自身,不会形成死循环)。 + * + * 凭据级失败(refreshSign 自身 401 / 业务码 401)抛 [CredentialBrokenException],调用方应让用户去登录。 + * 其它失败(IO/解析)返回 null 走冷却熔断路径。 + */ + private fun doRefreshSign(): String? { + return runBlocking { + try { + val api = RetrofitFactory.createApi(EnvProvider.AS_DOMAIN) + val resp = api.refreshSign(RefreshSignReq()) + val sign = resp.data?.sign + if (sign.isNullOrEmpty()) { + // refreshSign 返回 200 但 data 为空:极少见,按凭据级失败处理 + throw CredentialBrokenException("refreshSign returned empty sign") + } + sign + } catch (e: ApiException.BusinessException) { + // 业务码判定:401 系列代表服务端明确拒绝当前 sign + if (e.code in 401..499) { + throw CredentialBrokenException("refreshSign business code=${e.code}", e) + } + null + } catch (e: retrofit2.HttpException) { + // 401/403 这种服务端身份拒绝,抛凭据级错误 + if (e.code() in 401..403) { + throw CredentialBrokenException("refreshSign http=${e.code()}", e) + } + null + } catch (e: Exception) { + null + } + } + } + + /** + * 凭据级失败异常:refreshSign 自身被服务端拒绝(401/403/业务 401)时抛出。 + * 调用方捕获后置 credentialBroken = true 不再重试,让业务走登录流程。 + * + * 公开为 internal(包内可见):供模块内调试或日志使用,但禁止业务侧捕获。 + */ + internal class CredentialBrokenException( + message: String, + cause: Throwable? = null + ) : RuntimeException(message, cause) +} diff --git a/app/src/main/java/com/pukka/jiecheng/network/auth/SignRequestPolicy.kt b/app/src/main/java/com/pukka/jiecheng/network/auth/SignRequestPolicy.kt old mode 100755 new mode 100644 index 750466e..d744997 --- a/app/src/main/java/com/pukka/jiecheng/network/auth/SignRequestPolicy.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/auth/SignRequestPolicy.kt @@ -1,31 +1,20 @@ package com.pukka.jiecheng.network.auth -import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull + + -/** - * sign 头注入 / 401 刷新触发策略。 - * - * 排除原则: - * - /api/jst/init:首签接口,自身产出 sign,不能依赖已有 sign 注入(否则首次启动注入空 sign 触发 401) - * - /api/jst/refreshSign:刷新接口自身,如不排除,OkHttp 重试时仍会注入旧 sign 形成死循环 - * - * 为什么不复用 Authorization/Bearer:本项目历史协议使用自定义 "sign" 头(与既后端约定一致)。 - */ object SignRequestPolicy { - /** - * 不自动注入 sign 头的接口 path(相对 AS baseUrl)。 - */ val excludeFromAutoInject: Set = setOf( + "jst/init" + ) + + val excludeFromRefresh: Set = setOf( "jst/init", "jst/refreshSign" ) - /** - * 不触发 401 刷新的接口(与 excludeFromAutoInject 一致:首签/刷新自身) - */ - val excludeFromRefresh: Set = excludeFromAutoInject - fun shouldInjectSign(path: String): Boolean { return path !in excludeFromAutoInject } @@ -35,16 +24,12 @@ object SignRequestPolicy { } /** - * 提取相对 baseUrl 的 path(去掉 query/fragment),用于上述匹配。 - * - * 注意:HttpUrl.parse 是 OkHttp 3.12 的 API(4.x 才有 toHttpUrlOrNull 扩展), - * 项目已为 Android 4.2 兼容全局锁定 OkHttp 3.12.0,勿改回扩展函数。 + * 提取相对 baseUrl 的 path(去掉 query/fragment),用于白名单匹配。 + * 去掉 /api/ 前缀,匹配 Set 中的相对路径。 */ fun relativePath(absoluteUrl: String): String { - val url = HttpUrl.parse(absoluteUrl) ?: return absoluteUrl - // pathSegments.joinToString("/") 自动处理首尾斜杠 - return url.encodedPath().trimStart('/').let { - // 去掉 /api/ 前缀,匹配 Set 中的相对路径 + val url = absoluteUrl.toHttpUrlOrNull() ?: return absoluteUrl + return url.encodedPath.trimStart('/').let { if (it.startsWith("api/")) it.removePrefix("api/") else it } } diff --git a/app/src/main/java/com/pukka/jiecheng/network/auth/TokenStore.kt b/app/src/main/java/com/pukka/jiecheng/network/auth/TokenStore.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/ActivateDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/ActivateDto.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/ActivateReq.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/ActivateReq.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderJsapiDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderJsapiDto.kt new file mode 100644 index 0000000..e35c496 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderJsapiDto.kt @@ -0,0 +1,96 @@ +package com.pukka.jiecheng.network.dto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * /jst/benefit/order/price 响应 data:门店在售套餐列表(家用版订购中心)。 + * + *

envelope 约定:code == 0 成功(jsapi 系列接口专用,区别于其他 AS 接口的 200), + * 调用方需手判 resp.code == 0 后再取 data。

+ * + * @author ainetv + * @date 2026-09-04 + */ +@JsonClass(generateAdapter = true) +data class BenefitPriceDto( + @field:Json(name = "items") val items: List? = null +) + +/** + * 套餐项。 + * + * - cardType: 卡种标识("DAY" 天卡 / "MONTH" 月卡 / "YEAR" 年卡),jsapi/create 的请求入参 + * - priceFen: 售价(单位:分),精度敏感计算场景使用(如 ¥4 = 4 分) + * - price: 售价(单位:元,字符串),后端友好展示字段,如 "0.04"、"29.90",列表直接展示用 + * - originalPriceFen: 原价(单位:分),如有则为划线原价展示(如 ¥48 = 4800 分);null 表示无折扣 + * - originalPrice: 原价(单位:元,字符串),同上展示用,如 "48.00" + * - pricingId: 套餐定价 ID,下单/续费时作为业务主键传给后端(区别于 cardType 的卡种维度) + * - durationHours: 时长(小时),如天卡 24、月卡 720 + * - goodsName: 商品名(如 "橘视厅-天卡"),列表展示用 + * - description: 商品描述(如 "24小时不限次观看"),列表展示用 + * + * @author ainetv + * @date 2026-09-08 + */ +@JsonClass(generateAdapter = true) +data class BenefitPriceItemDto( + @field:Json(name = "cardType") val cardType: String? = null, + @field:Json(name = "priceFen") val priceFen: Int? = null, + @field:Json(name = "price") val price: String? = null, + @field:Json(name = "originalPriceFen") val originalPriceFen: Int? = null, + @field:Json(name = "originalPrice") val originalPrice: String? = null, + @field:Json(name = "pricingId") val pricingId: Int? = null, + @field:Json(name = "durationHours") val durationHours: Int? = null, + @field:Json(name = "goodsName") val goodsName: String? = null, + @field:Json(name = "description") val description: String? = null +) + +/** + * /jst/benefit/order/jsapi/create 请求体。 + * + * cardType 取自套餐列表选中项;后端校验 cardType 白名单(不在白名单返回 400 CARD_TYPE_DISABLED)。 + * + * @author ainetv + * @date 2026-09-04 + */ +@JsonClass(generateAdapter = true) +data class BenefitJsapiOrderReq( + @field:Json(name = "cardType") val cardType: String? = null +) + +/** + * /jst/benefit/order/jsapi/create 响应 data。 + * + * - orderNo: 订单号,格式 BO + yyyyMMddHHmmssSSS + 6 位随机 + * - h5Url: H5 订购页链接,前端生成二维码供用户扫码跳转 + * - expireTime: 订单过期时间(ISO-8601 字符串,创建后 15 分钟),到期需重新 create + * - reused: true = 复用既有未过期未支付订单;false = 新建 + * + * @author ainetv + * @date 2026-09-04 + */ +@JsonClass(generateAdapter = true) +data class BenefitJsapiOrderDto( + @field:Json(name = "orderNo") val orderNo: String? = null, + @field:Json(name = "h5Url") val h5Url: String? = null, + @field:Json(name = "expireTime") val expireTime: String? = null, + @field:Json(name = "reused") val reused: Boolean? = null +) + +/** + * /jst/benefit/order/jsapi/status/{orderNo} 响应 data。 + * + * 字段约定参见 docs/家庭会员套餐扫码.md §2.4: + * - status: 状态机(PENDING / PAID / ACTIVATING / ACTIVE / CANCEL);命中 PAID/ACTIVATING/ACTIVE + * → 进入阶段 4(check 轮询);命中 CANCEL → 清缓存 + 重新 create + * - msg: 后端携带的用户提示文案(如"支付成功"、"订单已取消"等),非空时前端 Toast 展示 + * + * @author ainetv + * @date 2026-09-09 + */ +@JsonClass(generateAdapter = true) +data class BenefitJsapiStatusDto( + @field:Json(name = "status") val status: String? = null, + @field:Json(name = "msg") val msg: String? = null +) diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderReq.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderReq.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderRespDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderRespDto.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderStatusDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/BenefitOrderStatusDto.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/CheckBenefitsDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/CheckBenefitsDto.kt old mode 100755 new mode 100644 index 62a4d53..d1520cf --- a/app/src/main/java/com/pukka/jiecheng/network/dto/CheckBenefitsDto.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/CheckBenefitsDto.kt @@ -15,10 +15,6 @@ data class CheckBenefitsDto( @field:Json(name = "status") val status: String? = null, @field:Json(name = "expireTime") val expireTime: String? = null, @field:Json(name = "thirdCardNo") val thirdCardNo: String? = null, - // 第三方后端(58.220.83.58:8084)字段:cardNo=卡号,expireAt=过期时间(原版为 expireTime)。 - // 解析层会将 expireAt 映射到 expireTime,业务侧无感。 - @field:Json(name = "cardNo") val cardNo: String? = null, - @field:Json(name = "expireAt") val expireAt: String? = null, @field:Json(name = "usedCardCount") val usedCardCount: String? = null, @field:Json(name = "totalCardCount") val totalCardCount: String? = null, @field:Json(name = "failCardCount") val failCardCount: String? = null, diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/CheckBenefitsReq.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/CheckBenefitsReq.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/CheckDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/CheckDto.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/CheckReq.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/CheckReq.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/DeviceBenefitsDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/DeviceBenefitsDto.kt new file mode 100644 index 0000000..e484b8e --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/DeviceBenefitsDto.kt @@ -0,0 +1,49 @@ +package com.pukka.jiecheng.network.dto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * /api/jst/benefit/device/me/benefits 响应 data。 + * + * 字段映射自 device-integration.md §5.4: + * - quotaLeft: 月度换绑剩余配额(本期单服务号,实际不会归零) + * - user: 扫码用户(DeviceUserDto) + * - benefits: ACTIVE 权益列表(空时为 []) + * + * 注意:本 DTO 字段结构由原 ActiveBenefitsDto(/active-benefits)演进而来, + * 多 quotaLeft 字段,专用于登录后的权益查询(/me/benefits); + * 内部 benefit item 已统一为 DeviceBenefitItemDto。 + */ +@JsonClass(generateAdapter = true) +data class DeviceBenefitsDto( + @field:Json(name = "quotaLeft") val quotaLeft: Int? = null, + @field:Json(name = "user") val user: DeviceUserDto? = null, + @field:Json(name = "benefits") val benefits: List? = null +) + +/** + * 单条权益记录(device-integration.md §5.4): + * - pricingName: 产品名称,「我的订购」列表标题列 + * - cardType / cardLabel: 订购类型(代码 / 展示名) + * - status / validFrom / validUntil: 状态展示用 + * - payAmountYuan: 支付金额(单位:元),「我的订购」列表金额列 + * - deviceId / deviceSn / deviceUid / deviceName / roomNo / mac: 绑定设备核对(本期仅展示) + */ +@JsonClass(generateAdapter = true) +data class DeviceBenefitItemDto( + @field:Json(name = "orderNo") val orderNo: String? = null, + @field:Json(name = "pricingName") val pricingName: String? = null, + @field:Json(name = "cardType") val cardType: String? = null, + @field:Json(name = "cardLabel") val cardLabel: String? = null, + @field:Json(name = "status") val status: String? = null, + @field:Json(name = "validFrom") val validFrom: String? = null, + @field:Json(name = "validUntil") val validUntil: String? = null, + @field:Json(name = "payAmountYuan") val payAmountYuan: String? = null, + @field:Json(name = "deviceId") val deviceId: Long? = null, + @field:Json(name = "deviceSn") val deviceSn: String? = null, + @field:Json(name = "deviceUid") val deviceUid: String? = null, + @field:Json(name = "deviceName") val deviceName: String? = null, + @field:Json(name = "roomNo") val roomNo: String? = null, + @field:Json(name = "mac") val mac: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/DeviceMeDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/DeviceMeDto.kt new file mode 100644 index 0000000..eaa920f --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/DeviceMeDto.kt @@ -0,0 +1,24 @@ +package com.pukka.jiecheng.network.dto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * 设备侧扫码用户身份(device-integration.md §1.1 device_session 表达)。 + * + * /me 响应的 data 字段直接是本 DTO(后端无 user 包装层); + * /me/benefits 响应的 data.user 字段同样使用本 DTO。 + * 字段全部可空(spec §4.3:用户走 snsapi_base 或 wx_user 缺失时部分字段为 null)。 + * + * - openid / unionid: 微信开放平台/unionid + * - appId: 公众号 appId + * - nickname / avatarUrl: 昵称/头像(用于 HomeMyFragment 已登录态头部展示) + */ +@JsonClass(generateAdapter = true) +data class DeviceUserDto( + @field:Json(name = "openid") val openid: String? = null, + @field:Json(name = "appId") val appId: String? = null, + @field:Json(name = "nickname") val nickname: String? = null, + @field:Json(name = "avatarUrl") val avatarUrl: String? = null, + @field:Json(name = "unionid") val unionid: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatActionDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatActionDto.kt new file mode 100644 index 0000000..fdbacf8 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatActionDto.kt @@ -0,0 +1,34 @@ +package com.pukka.jiecheng.network.dto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * /jst/heartBeat 响应 data.action[] 元素:服务端下发的控制指令。 + * + * 服务端可通过心跳通道下发一个或多个动作,由 [commandType] 决定客户端如何处理。 + * 当前已支持的 commandType: + * - "revokeCardSecret":撤销当前权益码,客户端将本地缓存的 sUid 置为随机数并 + * 重新调用 JetsenxSdkUtil.globalInit(),随后弹出"权益已取消"对话框让用户确认退出。 + * + * 未下发命令时 [commandType] 为 null,分派器应忽略。 + * + * 字段映射自服务端心跳响应示例: + * ```json + * { + * "id": 12345, + * "commandType": "revokeCardSecret", + * "params": { "orderNo": "...", "refundNo": "...", "refundTime": "..." }, + * "sendTime": "2026-08-24T15:31:23+08:00" + * } + * ``` + */ +@JsonClass(generateAdapter = true) +data class HeartBeatActionDto( + @field:Json(name = "id") val id: Long? = null, + @field:Json(name = "commandType") val commandType: String? = null, + // params 是动态 JSON 对象(key=string, value=string/number/null), + // 当前未在客户端消费,留可空 Map 以兼容 Moshi 反序列化 + 未来扩展 + @field:Json(name = "params") val params: Map? = null, + @field:Json(name = "sendTime") val sendTime: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatDto.kt old mode 100755 new mode 100644 index c33d02d..2685e6c --- a/app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatDto.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatDto.kt @@ -6,10 +6,16 @@ import com.squareup.moshi.JsonClass /** * /jst/heartBeat 响应 data。 * - * 原 Java 只关心 code==200,data 内容不消费;保留可空字段应对未来扩展。 + * 服务端可通过 [action] 数组下发一组控制指令,每条 action 包含 [HeartBeatActionDto.commandType], + * 客户端按 commandType 分派处理。当前已支持的命令: + * - "revokeCardSecret":撤销当前权益码,客户端须将本地缓存的 sUid 置为随机数后 + * 重新调用 JetsenxSdkUtil.globalInit(),并弹出"权益已取消"对话框让用户确认退出。 + * + * 未下发命令时 [action] 为 null 或空数组,调用方应忽略。 */ @JsonClass(generateAdapter = true) data class HeartBeatDto( - @field:Json(name = "msg") val msg: String? = null, - @field:Json(name = "timestamp") val timestamp: Long? = null + @field:Json(name = "msg") val msg: String? = null, + @field:Json(name = "timestamp") val timestamp: Long? = null, + @field:Json(name = "action") val action: List? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatReq.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/HeartBeatReq.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/InitDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/InitDto.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/InitReq.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/InitReq.kt old mode 100755 new mode 100644 index 95e1015..33fafbf --- a/app/src/main/java/com/pukka/jiecheng/network/dto/InitReq.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/InitReq.kt @@ -15,12 +15,6 @@ data class InitReq( @field:Json(name = "wifi_mac") val wifiMac: String? = null, @field:Json(name = "device_sn") val deviceSn: String? = null, @field:Json(name = "device_uid") val deviceUid: String? = null, - // 第三方后端(58.220.83.58:8084)文档字段名为 camelCase,原版后端为 snake_case。 - // thirdparty 变体双写两种命名(服务端各自忽略不认识的字段);原版变体保持 null 不参与序列化, - // 请求体与历史行为完全一致。 - @field:Json(name = "wifiMac") val wifiMacAlias: String? = null, - @field:Json(name = "deviceSn") val deviceSnAlias: String? = null, - @field:Json(name = "deviceUid") val deviceUidAlias: String? = null, @field:Json(name = "ver_code") val verCode: String? = null, @field:Json(name = "ver_name") val verName: String? = null, @field:Json(name = "org_code") val orgCode: String? = null, diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/LoginUrlDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/LoginUrlDto.kt new file mode 100644 index 0000000..470fbbb --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/LoginUrlDto.kt @@ -0,0 +1,24 @@ +package com.pukka.jiecheng.network.dto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * /api/jst/benefit/device/login-url 响应 data。 + * + * 字段映射自 device-integration.md §3.3.1: + * - loginUrl: 完整 H5 登录 URL,直接喂给 QRCodeUtil.createQRCodeBitmap 即可 + * - appId: 微信公众号 AppID(可空串,但 login-url 失败时 service 层会抛 500,正常不会走到这里) + * - deviceLoginToken: UUID v4,本次邀请标识(不要持久化) + * - isNew: true=本次新建;false=复用了上次的 PENDING + * + * 调用方决策:isNew=false 时 QR 内容不变,设备无需重渲染; + * isNew=true 时若屏幕已在显示旧 QR,应立即刷新。 + */ +@JsonClass(generateAdapter = true) +data class LoginUrlDto( + @field:Json(name = "loginUrl") val loginUrl: String? = null, + @field:Json(name = "appId") val appId: String? = null, + @field:Json(name = "deviceLoginToken") val deviceLoginToken: String? = null, + @field:Json(name = "isNew") val isNew: Boolean? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/LogoutDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/LogoutDto.kt new file mode 100644 index 0000000..d8710b1 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/LogoutDto.kt @@ -0,0 +1,13 @@ +package com.pukka.jiecheng.network.dto + +import com.squareup.moshi.JsonClass + +/** + * /api/jst/benefit/device/logout 响应 data。 + * + * 登出接口成功响应的 data 恒为 null;定义此空类仅为满足 NetResponse<T> 泛型 + * (Moshi 代码生成需要具体类型参数,data 为 null 时不会触发反序列化)。 + * Kotlin 不允许无参 data class,故为普通空类。 + */ +@JsonClass(generateAdapter = true) +class LogoutDto diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/RefreshSignDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/RefreshSignDto.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/RefreshSignReq.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/RefreshSignReq.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/StoreVersionDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/StoreVersionDto.kt new file mode 100644 index 0000000..0c6762a --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/StoreVersionDto.kt @@ -0,0 +1,36 @@ +package com.pukka.jiecheng.network.dto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * /api/jst/device/store-version 响应体。 + * + * 服务端把最新版本信息放在 data.version 中。 + * 外层 [StoreVersionDto] 与 [InitDto] 形态一致——都包一层 { version: ... }, + * 这样 NetResponse<StoreVersionDto> 直接走 .map<T>() 即可,无需特殊解析。 + * + * 内层字段命名按服务端实际下发采用 camelCase(保持与后端契约一致): + * compulsory / touchUrl / orgCode / verName / keyUrl / verCode + * + * 注意:服务端字段名若后续调整为 snake_case,只需改这里 [field:Json] 的 + * name 即可,业务调用方不感知。 + */ +@JsonClass(generateAdapter = true) +data class StoreVersionInfoDto( + @field:Json(name = "compulsory") val compulsory: String? = null, + @field:Json(name = "touchUrl") val touchUrl: String? = null, + @field:Json(name = "orgCode") val orgCode: String? = null, + @field:Json(name = "verName") val verName: String? = null, + @field:Json(name = "keyUrl") val keyUrl: String? = null, + @field:Json(name = "verCode") val verCode: String? = null +) + +/** + * /api/jst/device/store-version 响应的 data 字段。 + * 仅承载一个嵌套的 version 块——与服务端 JSON 结构保持一致。 + */ +@JsonClass(generateAdapter = true) +data class StoreVersionDto( + @field:Json(name = "version") val version: StoreVersionInfoDto? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/StoreVersionReq.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/StoreVersionReq.kt new file mode 100644 index 0000000..5175e6e --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/network/dto/StoreVersionReq.kt @@ -0,0 +1,24 @@ +package com.pukka.jiecheng.network.dto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * /api/jst/device/store-version 请求体。 + * + * 用于酒店版"我的"页面手动触发升级检查:用户点击升级入口后, + * 客户端把设备标识 + 渠道/控制类型发给服务端,服务端返回最新版本信息。 + * + * 字段命名风格:snake_case,与 [InitReq] 保持一致。 + * 所有字段可空 + 默认 null:服务端字段扩展不会让客户端崩溃。 + */ +@JsonClass(generateAdapter = true) +data class StoreVersionReq( + @field:Json(name = "mac") val mac: String? = null, + @field:Json(name = "wifiMac") val wifiMac: String? = null, + @field:Json(name = "deviceSn") val deviceSn: String? = null, + @field:Json(name = "deviceUid") val deviceUid: String? = null, + @field:Json(name = "orgCode") val orgCode: String? = null, + @field:Json(name = "deviceType") val deviceType: String? = null, + @field:Json(name = "controlType") val controlType: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/TrackingDto.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/TrackingDto.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/dto/TrackingReq.kt b/app/src/main/java/com/pukka/jiecheng/network/dto/TrackingReq.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/infra/ApiResult.kt b/app/src/main/java/com/pukka/jiecheng/network/infra/ApiResult.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/infra/EnvProvider.kt b/app/src/main/java/com/pukka/jiecheng/network/infra/EnvProvider.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/infra/MoshiProvider.kt b/app/src/main/java/com/pukka/jiecheng/network/infra/MoshiProvider.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/infra/NetResponse.kt b/app/src/main/java/com/pukka/jiecheng/network/infra/NetResponse.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/infra/Network.kt b/app/src/main/java/com/pukka/jiecheng/network/infra/Network.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/infra/NetworkConfig.kt b/app/src/main/java/com/pukka/jiecheng/network/infra/NetworkConfig.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/infra/NetworkEnv.kt b/app/src/main/java/com/pukka/jiecheng/network/infra/NetworkEnv.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/network/infra/OkHttpProvider.kt b/app/src/main/java/com/pukka/jiecheng/network/infra/OkHttpProvider.kt old mode 100755 new mode 100644 index 388f36a..da69740 --- a/app/src/main/java/com/pukka/jiecheng/network/infra/OkHttpProvider.kt +++ b/app/src/main/java/com/pukka/jiecheng/network/infra/OkHttpProvider.kt @@ -19,8 +19,9 @@ import javax.net.ssl.X509TrustManager * 2. SSL 全信任:复用 com.pukka.libcore.fresco.TrustAllManager(与 MyApplication.installTrustAllSsl 保持一致)。 * 机顶盒场景下系统 CA 库不全,全信任是历史兼容性方案;保留注释以便后续替换为正式 CA。 * 3. Tls12SocketFactory:低版本机顶盒(Android 4.x)需强制启用 TLSv1.2,否则 HTTPS 握手失败。 - * 4. 拦截器链:Logging -> Header(注入 sign/业务头) -> NetworkEvents -> Authenticator(401 刷新) - * Authenticator 必须在 HeaderInterceptor 之后,以便刷新后的 sign 能注入重试请求。 + * 4. 拦截器链:Header(注入 sign/业务头) -> Authenticator(HTTP 401 刷新)。 + * HTTP 401 由 [SignAuthenticator] 在 OkHttp 网络层处理; + * envelope code(HTTP 200 + body.code=任意数字)由业务侧自行 switch 判定,不再走拦截器层。 */ object OkHttpProvider { @@ -54,7 +55,7 @@ object OkHttpProvider { // 业务拦截器:注入 sign 头 / X-Channel-Id / X-Client-Version / X-Device-Id builder.addInterceptor(HeaderInterceptor()) - // 401 单飞刷新:Authenticator 在 OkHttp 收到 401 后回调,给原 request 注入新 sign 并重试 + // HTTP 401 单飞刷新:Authenticator 在 OkHttp 收到 401 后回调,给原 request 注入新 sign 并重试 builder.authenticator(SignAuthenticator()) // 日志拦截器(可选):Release 关闭以避免泄漏 header diff --git a/app/src/main/java/com/pukka/jiecheng/network/infra/RetrofitFactory.kt b/app/src/main/java/com/pukka/jiecheng/network/infra/RetrofitFactory.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/newtv/NewTvListner.java b/app/src/main/java/com/pukka/jiecheng/newtv/NewTvListner.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/newtv/NewTvManager.java b/app/src/main/java/com/pukka/jiecheng/newtv/NewTvManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/newtv/NewTvUtils.java b/app/src/main/java/com/pukka/jiecheng/newtv/NewTvUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/newtv/NewTvView.java b/app/src/main/java/com/pukka/jiecheng/newtv/NewTvView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/newtv/SensorsPlayBean.java b/app/src/main/java/com/pukka/jiecheng/newtv/SensorsPlayBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/JsapiOrderHelper.kt b/app/src/main/java/com/pukka/jiecheng/order/JsapiOrderHelper.kt new file mode 100644 index 0000000..f44d931 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/order/JsapiOrderHelper.kt @@ -0,0 +1,160 @@ +package com.pukka.jiecheng.order + +import com.pukka.jiecheng.network.ApiRequesters +import com.pukka.jiecheng.network.api.BenefitOrderApi +import com.pukka.jiecheng.network.dto.BenefitJsapiOrderDto +import com.pukka.jiecheng.network.dto.BenefitJsapiOrderReq +import com.pukka.jiecheng.network.dto.BenefitPriceItemDto +import com.pukka.jiecheng.network.from +import com.pukka.libcore.utils.LogUtils +import kotlinx.coroutines.CoroutineScope + +/** + * 订购中心 H5 扫码订购助手(家用版 jsapi 链路,精简版)。 + * + *

封装两个接口的调用,供 Java 侧 + * ([com.pukka.jiecheng.order.fragment.OrderCenterFragment]) 使用:

+ *
    + *
  • [loadPrices]:POST /api/jst/benefit/order/price 拉取门店在售套餐列表
  • + *
  • [createOrder]:POST /api/jst/benefit/order/jsapi/create 创建/复用扫码订单
  • + *
+ * + *

职责收敛(详见 docs/superpowers/specs/2026-09-12-jsapi-order-monitor-design.md §3.1): + * status 轮询 / check 轮询 / 状态机推进 全部移交 [JsapiOrderMonitor], + * helper 不再持有任何协程 Job,Fragment destroy 时无需手动 cancel。

+ * + *

envelope 约定:code == 0 成功(jsapi 系列,非 200),故不走 .map(), + * 在 onSuccess 中手判 code == 0。

+ * + * @param scope 必须为宿主 Fragment/Activity 的 lifecycleScope(本次仅用于单次网络请求,不再挂载轮询) + */ +class JsapiOrderHelper(private val scope: CoroutineScope) { + + companion object { + private const val TAG = "JsapiOrderHelper" + } + + /** 缓存最近一次 loadPrices 成功的套餐列表;供 [switchItem] 兜底查找 pricingId */ + @Volatile var cachedItems: List? = null + private set + + /** 缓存最近一次 createOrder 成功的 h5Url;Fragment 用作 QR 拼接;失败/null 时清空 */ + @Volatile var cachedH5Url: String? = null + private set + + /** 缓存最近一次 createOrder 成功的 orderNo;供 Fragment 同步给 Monitor.arm() */ + @Volatile var cachedOrderNo: String? = null + private set + + /** 套餐列表查询回调(fun interface,Java 侧可用 lambda) */ + fun interface PriceCallback { + fun onResult(items: List?, code: Int?, msg: String?) + } + + /** 扫码订单创建回调(fun interface,Java 侧可用 lambda) */ + fun interface CreateCallback { + fun onResult(dto: BenefitJsapiOrderDto?, code: Int?, msg: String?) + } + + /** + * 拉取门店在售套餐列表。 + * + *

成功后写入 [cachedItems],供 [switchItem] 兜底查找 pricingId。

+ */ + fun loadPrices(callback: PriceCallback) { + ApiRequesters.ui(scope) + .from() + .action { it.price() } + .onSuccess { resp -> + if (resp.code == 0) { + val items = resp.data?.items.orEmpty() + cachedItems = items + callback.onResult(items, resp.code, resp.msg) + } else { + LogUtils.e(TAG, "price business error code=${resp.code} msg=${resp.msg}") + callback.onResult(null, resp.code, resp.msg) + } + } + .onFailure { err -> + LogUtils.e(TAG, "price request error: ${err.message}") + callback.onResult(null, err.httpCode, err.message) + } + .launch() + } + + /** + * 是否已创建过订单([cachedH5Url] 非空)。 + * + *

供宿主判断阶段 1 选中套餐后应走 [createOrder](首次 / CANCEL 重建) + * 还是 [switchItem](切套餐,零网络请求)。

+ */ + fun hasCachedOrder(): Boolean = !cachedH5Url.isNullOrBlank() + + /** + * 拼接 QR URL:h5Url + ?pricingId=xxx(或 h5Url 已含 ? 时用 &)。 + * + *

无 pricingId 时原样返回 h5Url(不破坏现有 URL 结构)。

+ */ + fun composeQrUrl(h5Url: String?, pricingId: Int?): String? { + if (h5Url.isNullOrBlank()) return null + if (pricingId == null) { + LogUtils.w(TAG, "composeQrUrl: pricingId is null, return h5Url unchanged") + return h5Url + } + val separator = if (h5Url.contains("?")) "&" else "?" + return "$h5Url${separator}pricingId=$pricingId" + } + + /** + * 切换选中套餐:本地拼 [pricingId] 刷新二维码,不发起任何网络请求。 + * + *

仅在 [hasCachedOrder] 返回 true 时调用;否则返回 null,由宿主回退到 [createOrder]。

+ * + *

pricingId 优先取 [item.pricingId],兜底从 [cachedItems] 按 cardType 查找。

+ */ + fun switchItem(item: BenefitPriceItemDto): String? { + val h5Url = cachedH5Url + if (h5Url.isNullOrBlank()) { + LogUtils.w(TAG, "switchItem skipped: cachedH5Url is null, should call createOrder first") + return null + } + val pricingId = item.pricingId + ?: cachedItems?.firstOrNull { it.cardType == item.cardType }?.pricingId + return composeQrUrl(h5Url, pricingId) + } + + /** + * 创建/复用 H5 扫码订购订单。 + * + *

成功时同时写入 [cachedH5Url] / [cachedOrderNo],由宿主 Fragment 调 + * [JsapiOrderMonitor.arm] + [JsapiOrderMonitor.startStatusJobBg] 启动轮询。

+ * + *

失败只回调一次,由宿主决定重试/提示。

+ */ + fun createOrder(cardType: String?, callback: CreateCallback) { + ApiRequesters.ui(scope) + .from() + .action { it.jsapiCreate(BenefitJsapiOrderReq(cardType = cardType)) } + .onSuccess { resp -> + val dto = resp.data + if (resp.code == 0 && dto != null) { + LogUtils.d(TAG, "jsapi create success orderNo=${dto.orderNo} reused=${dto.reused}") + cachedOrderNo = dto.orderNo + cachedH5Url = dto.h5Url + callback.onResult(dto, resp.code, resp.msg) + } else { + LogUtils.e(TAG, "jsapi create business error code=${resp.code} msg=${resp.msg}") + cachedOrderNo = null + cachedH5Url = null + callback.onResult(null, resp.code, resp.msg ?: "订单创建失败,请稍后重试") + } + } + .onFailure { err -> + LogUtils.e(TAG, "jsapi create request error: ${err.message}") + cachedOrderNo = null + cachedH5Url = null + callback.onResult(null, err.httpCode, err.message) + } + .launch() + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/order/JsapiOrderMonitor.kt b/app/src/main/java/com/pukka/jiecheng/order/JsapiOrderMonitor.kt new file mode 100644 index 0000000..7c6efcc --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/order/JsapiOrderMonitor.kt @@ -0,0 +1,511 @@ +package com.pukka.jiecheng.order + +import android.content.Context +import android.os.Handler +import android.os.Looper +import com.pukka.jiecheng.network.ApiRequesters +import com.pukka.jiecheng.network.api.AsApi +import com.pukka.jiecheng.network.api.BenefitOrderApi +import com.pukka.jiecheng.network.dto.BenefitJsapiStatusDto +import com.pukka.jiecheng.network.dto.CheckReq +import com.pukka.jiecheng.network.from +import com.pukka.jiecheng.network.infra.ApiResult +import com.pukka.jiecheng.network.infra.NetResponse +import com.pukka.jiecheng.utils.JetsenxSdkUtil +import com.pukka.libcore.utils.ContextUtils +import com.pukka.libcore.utils.LogUtils +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.util.concurrent.CopyOnWriteArraySet + +/** + * 家用版 H5 扫码订购 status 轮询监控器(单例)。 + * + *

持有 [statusJobBg](AppScope CoroutineScope, 5s 周期)跨 OrderActivity 存活, + * 通过 [JsapiOrderMonitorCallback] 把状态推进推给 [com.pukka.jiecheng.order.fragment.OrderCenterFragment]。

+ * + *

关键设计:

+ *
    + *
  • Monitor 不持有 JsapiOrderHelper 引用,不直接调 createOrder;
  • + *
  • CANCELLED 重建由回调方 Fragment 完成;ACTIVE 启动 check 由 Monitor 直接 + * [startCheckJob](不依赖 Fragment 是否注册,保证用户退出 OrderActivity 后 check 仍触发);
  • + *
+ * + *

详见 docs/superpowers/specs/2026-09-12-jsapi-order-monitor-design.md §3 / §5.3。

+ * + * @param appScope AppScope CoroutineScope,生命周期 = 进程生命周期 + */ +class JsapiOrderMonitor private constructor(private val appScope: CoroutineScope) { + + /** 阶段 3 status 轮询 Job;Activity destroy 不停,终态时停 */ + private var statusJobBg: Job? = null + + /** 阶段 4 check 轮询 Job;仅 status 命中 ACTIVE 时由 Fragment 调 startCheckJob() 启动 */ + private var checkJob: Job? = null + + /** 缓存最近一次 arm 的 orderNo;statusJobBg 用作轮询 key */ + @Volatile private var currentOrderNo: String? = null + + /** 缓存最近一次 arm 的 cardType;超时自愈 recreate() 用 */ + @Volatile private var currentCardType: String? = null + + /** 缓存最近一次 status 响应里的 expireTime(Unix 毫秒);超时自愈判定用 */ + @Volatile private var currentExpireTime: Long? = null + + /** 回调订阅列表(线程安全);Monitor 状态推进时遍历回调 */ + private val callbacks = CopyOnWriteArraySet() + + /** OrderActivity 退出后剩余轮询次数(>0 表示处于"销毁后兜底窗口"模式) */ + @Volatile private var postExitIterationsRemaining: Int = 0 + + /** 主线程 Handler:回调实现方(OrderCenterFragment)需在主线程更新 UI */ + private val mainHandler = Handler(Looper.getMainLooper()) + + /** 是否已 arm(orderNo 非空);供 OrderCenterFragment.onResume 判断是否已有未完结订单 */ + val isArmed: Boolean + get() = !currentOrderNo.isNullOrBlank() + + /** 供 OrderCenterFragment.onResume 同步 helper 缓存;与 arm() 写入同步 */ + val armedOrderNo: String? get() = currentOrderNo + + /** 供 recreate 内部用 */ + internal val armedCardType: String? get() = currentCardType + + /** 供 recreate 内部用 */ + internal val armedExpireTime: Long? get() = currentExpireTime + + companion object { + private const val TAG = "JsapiOrderMonitor" + + /** status 轮询初始延迟(ms):刚进入页面给用户聚焦套餐 + 触发 createOrder 留出时间窗 */ + const val DEFAULT_STATUS_INITIAL_DELAY_MS = 0L + + /** status 轮询周期(ms):与新协议 §10.6 推荐 5s 对齐 */ + const val DEFAULT_STATUS_PERIOD_MS = 8_000L + + /** check 轮询总预算(ms):30s 内未命中则超时失败,与 BenefitOrderController 对齐 */ + private const val CHECK_TOTAL_BUDGET_MS = 30_000L + + /** check 轮询周期(ms):3s/次(30s 内最多 10 次) */ + private const val CHECK_INTERVAL_MS = 3_000L + + /** check 轮询最大尝试次数:与 BenefitOrderController.CHECK_MAX_ATTEMPTS 对齐 */ + private const val CHECK_MAX_ATTEMPTS = 10 + + /** check 业务码 */ + private const val CHECK_BIZ_CODE_ACTIVATED = 10200 + private const val CHECK_BIZ_CODE_CODE_ACTIVATED = 10900 + private const val CHECK_STATUS_ACTIVATED = "ACTIVATED" + private const val CHECK_STATUS_ACTIVE = "ACTIVE" + + /** OrderActivity 退出后额外轮询次数(约 80s 兜底窗口,按 8s/次) */ + const val POST_EXIT_POLL_ITERATIONS = 10 + + @Volatile private var INSTANCE: JsapiOrderMonitor? = null + + /** + * 获取单例(lazy-init)。 + * + *

调用时机:[com.pukka.jiecheng.order.fragment.OrderCenterFragment.onCreateView]。 + * 不传 Activity Context,仅持有 applicationContext 防止内存泄漏。

+ * + * @param context 任意 Context,方法内部仅取 applicationContext + * @return 全局唯一的 [JsapiOrderMonitor] 实例 + */ + @Suppress("UNUSED_PARAMETER") + fun get(context: Context): JsapiOrderMonitor { + // 参数 context 在本骨架中尚未使用(Task 2 不创建任何依赖 Context 的资源), + // 保留入参是因未来 Task(N) 可能需要 applicationContext 做网络层初始化 + // (如注册 DeviceIdProvider、ChannelIdProvider 等)。 + // 此处约定调用方传 Application Context,不持有 Activity Context。 + return INSTANCE ?: synchronized(this) { + INSTANCE ?: JsapiOrderMonitor( + // AppScope CoroutineScope:SupervisorJob + Dispatchers.IO,进程级存活 + CoroutineScope(SupervisorJob() + Dispatchers.IO) + ).also { INSTANCE = it } + } + } + } + + /** + * 注册状态回调。 + * + *

调用时机:OrderCenterFragment.onResume。

+ *

线程安全:CopyOnWriteArraySet 保证并发 add/remove 不抛 ConcurrentModificationException。

+ * + * @param callback 状态回调实现方(典型为 OrderCenterFragment) + */ + fun registerCallback(callback: JsapiOrderMonitorCallback) { + callbacks.add(callback) + LogUtils.d(TAG, "registerCallback total=${callbacks.size}") + } + + /** + * 注销状态回调。 + * + *

调用时机:OrderCenterFragment.onPause / onDestroyView。 + * 注销后 Monitor 状态推进不会再触达该 callback,但 statusJobBg 不会因此停止。

+ * + * @param callback 之前注册的状态回调实现方 + */ + fun unregisterCallback(callback: JsapiOrderMonitorCallback) { + callbacks.remove(callback) + LogUtils.d(TAG, "unregisterCallback total=${callbacks.size}") + } + + /** + * 通知 Monitor:OrderActivity.onStart 已调(用户回到页面)。 + * + *

重置 [postExitIterationsRemaining] 为 0,取消"销毁后兜底倒计"。 + * 若 statusJobBg 已被 post-exit 倒计取消,OrderCenterFragment 在 onResume + * 注册回调后,后续 helper.createOrder / 重新 arm 路径会再次调 [startStatusJobBg] + * 拉起轮询,Monitor 不在此方法内主动重启。

+ */ + fun notifyOrderActivityResumed() { + if (postExitIterationsRemaining > 0) { + LogUtils.d(TAG, "notifyOrderActivityResumed: reset post-exit counter from $postExitIterationsRemaining to 0") + postExitIterationsRemaining = 0 + } + } + + /** + * 通知 Monitor:OrderActivity.onDestroy 已调(用户退出页面)。 + * + *

无未完结订单([isArmed] == false)时直接 return,不进入倒计(statusJobBg 本就没跑)。

+ *

有 armed 订单时把 [postExitIterationsRemaining] 置为 [POST_EXIT_POLL_ITERATIONS], + * 下一次 [statusJobBg] 迭代结束后倒计 --1,达到 0 时只取消 [statusJobBg] + * (保留 [checkJob] 让激活路径独立完成,即便用户已离开也能让新权益即时生效)。

+ */ + fun notifyOrderActivityDestroyed() { + if (!isArmed) { + LogUtils.d(TAG, "notifyOrderActivityDestroyed: no armed order, skip") + return + } + postExitIterationsRemaining = POST_EXIT_POLL_ITERATIONS + LogUtils.d(TAG, "notifyOrderActivityDestroyed: schedule stop after $POST_EXIT_POLL_ITERATIONS more polls") + } + + /** + * 在主线程投递状态回调。 + * + *

[OrderCenterFragment] 等实现方在回调里直接操作 View(setText / Toast / 二维码刷新), + * 而 statusJobBg / checkJob 运行在 [Dispatchers.IO]——统一在这里切回主线程, + * 避免 CalledFromWrongThreadException(Only the original thread 可触碰 View)。

+ * + *

采用 [mainHandler] 的 [Handler.post](fire-and-forget)而非 withContext(Main): + * FAILED / check 命中路径会先调 [stop] 取消当前协程,若此时再进 withContext(Main) + * 会因协程已取消直接抛 CancellationException、回调丢失;post 不依赖协程存活态。

+ */ + private fun dispatchToCallbacks(block: (JsapiOrderMonitorCallback) -> Unit) { + mainHandler.post { callbacks.forEach(block) } + } + + /** + * 装备(arm)轮询目标。 + * + *

记录元数据供 [startStatusJobBg] 启动轮询用。仅当 orderNo != currentOrderNo 时才真正启动/重启 + * statusJobBg;orderNo == currentOrderNo 时仅更新 cardType/expireTime(如有变化),不重启协程(幂等保护)。

+ * + *

调用时机:OrderCenterFragment.helper.createOrder 成功回调里 / OrderCenterFragment.onResume + * 同步 Monitor.armed 状态后调。

+ * + * @param orderNo 订单号(status 轮询 path 参数) + * @param cardType 卡种(超时自愈 recreate 用);可空 + * @param expireTime Unix 毫秒(超时自愈判定用);可空表示无过期(ORDER_CREATED 阶段) + */ + fun arm(orderNo: String, cardType: String?, expireTime: Long?) { + // 防御:orderNo 必须非空非空白,否则后续 status 轮询 key 无法构建 + require(orderNo.isNotBlank()) { "arm: orderNo must not be blank" } + synchronized(this) { + if (currentOrderNo == orderNo) { + // 幂等:仅更新 cardType/expireTime(若有变化),不重启协程 + currentCardType = cardType + currentExpireTime = expireTime + LogUtils.d(TAG, "arm idempotent orderNo=$orderNo cardType=$cardType expireTime=$expireTime") + return + } + currentOrderNo = orderNo + currentCardType = cardType + currentExpireTime = expireTime + LogUtils.d(TAG, "arm orderNo=$orderNo cardType=$cardType expireTime=$expireTime") + } + } + + /** + * 启动 statusJobBg 后台轮询。 + * + *

幂等:若 statusJobBg?.isActive == true,直接 return。 + * 必须先调 [arm] 写入 currentOrderNo。

+ * + *

轮询逻辑(详见 docs/superpowers/specs/2026-09-12-jsapi-order-monitor-design.md §4):

+ *
    + *
  • status ∈ {ORDER_CREATED, SCANNED, PENDING, PAID, ACTIVATING} → 回调 onStatusUpdate,继续轮询
  • + *
  • status == ACTIVE → 回调 onStatusUpdate + onOrderActive(由 Fragment 决定是否 startCheckJob)
  • + *
  • status ∈ {CANCELLED, TIMEOUT_CANCELED} → 回调 onOrderCancelled(reason, expired)
  • + *
  • status == FAILED → stop() + 回调 onOrderFailed
  • + *
  • 当前时间 > expireTime 且 status ∈ {SCANNED, PENDING} → 触发超时自愈 recreate(Task 4)
  • + *
+ */ + fun startStatusJobBg() { + synchronized(this) { + if (statusJobBg?.isActive == true) { + LogUtils.w(TAG, "startStatusJobBg ignored: already running") + return + } + val orderNo = currentOrderNo + if (orderNo.isNullOrBlank()) { + LogUtils.e(TAG, "startStatusJobBg skipped: orderNo is null, call arm() first") + return + } + LogUtils.d(TAG, "startStatusJobBg orderNo=$orderNo") + statusJobBg = appScope.launch { + // 立即发起首轮请求(无延迟),后续按 periodMs 周期 + while (isActive) { + val current = currentOrderNo + if (current.isNullOrBlank()) { + LogUtils.w(TAG, "statusJobBg skipped: currentOrderNo is null") + delay(DEFAULT_STATUS_PERIOD_MS) + continue + } + + // 超时自愈判定:now > expireTime 且 status 是 SCANNED/PENDING(详见 Task 4) + // Task 3 占位,Task 4 实现 recreate 触发逻辑 + + val reqStartMs = System.currentTimeMillis() + LogUtils.d(TAG, "jsapiStatus request start orderNo=$current") + val apiResult: ApiResult>? = try { + ApiRequesters.app() + .from() + .action { it.jsapiStatus(current) } + .execute() + } catch (ce: CancellationException) { + LogUtils.e(TAG, "statusJobBg request CancellationException: ${ce.message}") + throw ce + } catch (e: Throwable) { + LogUtils.e(TAG, "statusJobBg request exception: ${e.message}") + null + } + val reqElapsedMs = System.currentTimeMillis() - reqStartMs + LogUtils.d(TAG, "jsapiStatus request end orderNo=$current elapsedMs=$reqElapsedMs") + + if (apiResult == null) { + delay(DEFAULT_STATUS_PERIOD_MS) + continue + } + val resp = when (apiResult) { + is ApiResult.Success -> apiResult.value + is ApiResult.Failure -> { + LogUtils.w(TAG, "statusJobBg failure: ${apiResult.error}") + null + } + } + if (resp == null) { + delay(DEFAULT_STATUS_PERIOD_MS) + continue + } + + // envelope 业务失败(非 0)→ 继续轮询,避免临时抖动退出 + if (resp.code != 0 || resp.data == null) { + LogUtils.w(TAG, "statusJobBg envelope failure code=${resp.code} msg=${resp.msg}") + delay(DEFAULT_STATUS_PERIOD_MS) + continue + } + + val status = resp.data.status.orEmpty().uppercase() + val msg = resp.data.msg + LogUtils.d(TAG, "statusJobBg orderNo=$current status=$status msg=$msg") + + // expireTime 缓存由 arm() 阶段从 jsapiCreate 响应写入, + // Task 4 的 recreate 路径会在此读 currentExpireTime 判断是否超时自愈。 + + when (status) { + "ORDER_CREATED", "SCANNED", "PENDING", "PAID", "ACTIVATING" -> { + dispatchToCallbacks { it.onStatusUpdate(status, msg) } + } + "ACTIVE" -> { + dispatchToCallbacks { it.onStatusUpdate(status, msg) } + dispatchToCallbacks { it.onOrderActive() } + // 命中 ACTIVE 后 Monitor 直接启动 check 轮询,不依赖 Fragment 回调: + // 用户可能在 OrderActivity 之外(onPause 已注销回调),仅靠 onOrderActive + // 转发会导致 /api/jst/check 永不触发。startCheckJob 幂等,重复触发安全; + // check 命中 thirdCardNo 后内部 stop() 会同时取消 statusJobBg(见下方 startCheckJob)。 + startCheckJob() + } + "CANCELLED" -> { + dispatchToCallbacks { it.onOrderCancelled(msg, false) } + } + "TIMEOUT_CANCELED" -> { + dispatchToCallbacks { it.onOrderCancelled(msg, true) } + } + "FAILED" -> { + stop() + dispatchToCallbacks { it.onOrderFailed(msg) } + } + else -> { + LogUtils.w(TAG, "statusJobBg unknown status=$status, continue polling") + } + } + + // OrderActivity 已销毁,达到额外轮询上限则退出 statusJobBg(保留 checkJob 独立完成激活) + if (postExitIterationsRemaining > 0) { + val remaining = --postExitIterationsRemaining + LogUtils.d(TAG, "statusJobBg post-exit iteration remaining=$remaining") + if (remaining <= 0) { + LogUtils.d(TAG, "statusJobBg post-exit polling exhausted, cancel statusJobBg only") + statusJobBg?.cancel() + statusJobBg = null + return@launch + } + } + + delay(DEFAULT_STATUS_PERIOD_MS) + } + } + } + } + + /** + * 停止 statusJobBg + checkJob(终态时调,如 FAILED / check 命中 / check 超时)。 + */ + fun stop() { + synchronized(this) { + statusJobBg?.cancel() + statusJobBg = null + checkJob?.cancel() + checkJob = null + LogUtils.d(TAG, "stop statusJobBg + checkJob cancelled") + } + } + + /** + * 启动 checkJob 轮询(卡密激活轮询)。 + * + *

调用方有两处:

+ *
    + *
  1. [statusJobBg] 命中 ACTIVE 时由 Monitor 直接调起(主路径,不依赖 Fragment 是否注册);
  2. + *
  3. [JsapiOrderMonitorCallback.onOrderActive] 回调(OrderCenterFragment 收到后调,幂等,重复触发安全)。
  4. + *
+ * + *

30s 内最多 10 次轮询 `/api/jst/check`,命中 10200 或 thirdCardNo 非空即调 + * [stop] 同时取消 statusJobBg + checkJob 并回调 onCheckActivated;超时回调 onCheckTimeout。

+ * + *

幂等:若 checkJob?.isActive == true,直接 return。

+ */ + fun startCheckJob() { + synchronized(this) { + if (checkJob?.isActive == true) { + LogUtils.w(TAG, "startCheckJob ignored: already running") + return + } + LogUtils.d(TAG, "startCheckJob") + checkJob = appScope.launch { + val deadline = System.currentTimeMillis() + CHECK_TOTAL_BUDGET_MS + var attempt = 0 + + while (attempt < CHECK_MAX_ATTEMPTS + && System.currentTimeMillis() < deadline + && isActive) { + attempt++ + if (attempt > 1) delay(CHECK_INTERVAL_MS) + if (!isActive) break + + LogUtils.d(TAG, "check poll attempt=$attempt") + val apiResult: ApiResult>? = try { + ApiRequesters.app() + .from() + .action { it.check(CheckReq()) } + .execute() + } catch (ce: CancellationException) { + LogUtils.e(TAG, "check poll CancellationException: ${ce.message}") + throw ce + } catch (e: Throwable) { + LogUtils.e(TAG, "check poll exception: ${e.message}") + null + } + + if (apiResult == null) continue + val resp = when (apiResult) { + is ApiResult.Success -> apiResult.value + is ApiResult.Failure -> { + LogUtils.w(TAG, "check poll failure: ${apiResult.error}") + null + } + } + if (resp == null) continue + + val bizCode = resp.code ?: -1 + val dto = resp.data + val cardNo = dto?.thirdCardNo.orEmpty() + val statusUpper = dto?.status.orEmpty().uppercase() + LogUtils.d(TAG, "check poll attempt=$attempt bizCode=$bizCode status=$statusUpper cardNo=${if (cardNo.isNotEmpty()) cardNo.take(4) + "***" else "null"}") + + if (cardNo.isNotEmpty() && (bizCode == CHECK_BIZ_CODE_ACTIVATED + || bizCode == CHECK_BIZ_CODE_CODE_ACTIVATED + || statusUpper == CHECK_STATUS_ACTIVATED + || statusUpper == CHECK_STATUS_ACTIVE)) { + // 命中激活:写 SDK uid + 重初始化 SDK(新权益即时生效)+ stop + 回调 + JetsenxSdkUtil.cacheReusableUid(cardNo) + // 新卡密已写入 sUid,重新初始化 SDK 让新权益立即生效; + // ensureGlobalInitAsync 发现 sUid 非空会直接走 globalInit(含串行化/排队保护)。 + // 回调仅打日志、不阻断主流程:即便 init 失败,sUid 已缓存,后续播放路径可自愈重试。 + JetsenxSdkUtil.ensureGlobalInitAsync( + ContextUtils.getApplicationContext(), + object : JetsenxSdkUtil.FallbackInitCallback { + override fun onReady(memberCode: String) { + LogUtils.i(TAG, "order center re-init SDK success, memberCode=$memberCode") + } + override fun onNoEntitlement(businessCode: Int, message: String) { + LogUtils.w(TAG, "order center re-init: no entitlement code=$businessCode msg=$message") + } + override fun onNetworkError(businessCode: Int, message: String) { + LogUtils.w(TAG, "order center re-init: network error code=$businessCode msg=$message") + } + } + ) + stop() + dispatchToCallbacks { it.onCheckActivated(cardNo) } + return@launch + } + } + + if (isActive) { + LogUtils.w(TAG, "check poll timeout after ${CHECK_TOTAL_BUDGET_MS}ms") + stop() + dispatchToCallbacks { it.onCheckTimeout() } + } + } + } + } + + /** + * 解析 ISO-8601 时间为 Unix 毫秒(兼容带/不带时区偏移)。 + * + *

从原 JsapiOrderHelper.parseIso8601 复制,仅用于 statusJobBg 内部超时自愈判定。 + * 解析失败返回 null,不影响主流程。

+ */ + private fun parseIso8601ToMillis(ts: String?): Long? { + if (ts.isNullOrBlank()) return null + val patterns = listOf( + "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + "yyyy-MM-dd'T'HH:mm:ssXXX", + "yyyy-MM-dd'T'HH:mm:ss.SSS", + "yyyy-MM-dd'T'HH:mm:ss" + ) + for (pattern in patterns) { + try { + val sdf = java.text.SimpleDateFormat(pattern, java.util.Locale.US) + sdf.isLenient = false + return sdf.parse(ts)?.time + } catch (e: Exception) { + // 尝试下一种格式 + } + } + return null + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/order/JsapiOrderMonitorCallback.kt b/app/src/main/java/com/pukka/jiecheng/order/JsapiOrderMonitorCallback.kt new file mode 100644 index 0000000..c2535d5 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/order/JsapiOrderMonitorCallback.kt @@ -0,0 +1,59 @@ +package com.pukka.jiecheng.order + +/** + * JsapiOrderMonitor 状态回调接口(家用版 H5 扫码订购链路)。 + * + *

由 [JsapiOrderMonitor] 在 statusJobBg / checkJob 推进时回调, + * 由 [com.pukka.jiecheng.order.fragment.OrderCenterFragment] 实现。

+ * + *

回调线程:主线程(由 [JsapiOrderMonitor.dispatchToCallbacks] 统一切到 + * [android.os.Looper.getMainLooper] 后投递)。实现方可直接操作 View,无需自行切线程。

+ * + *

详见 docs/superpowers/specs/2026-09-12-jsapi-order-monitor-design.md §5.2。

+ */ +interface JsapiOrderMonitorCallback { + + /** + * status 状态更新(含 ORDER_CREATED / SCANNED / PENDING / PAID / ACTIVATING / ACTIVE 等中间态)。 + * + * @param status 当前订单状态字面量,与新协议 §3.1 状态机表对齐 + * @param msg 后端携带的用户提示文案(如"支付成功");可空 + */ + fun onStatusUpdate(status: String, msg: String?) + + /** + * status 命中 CANCELLED 或 TIMEOUT_CANCELED(终态,由 Quartz BenefitOrderTimeoutTask 推进)。 + * + *

由回调方决定是否重建订单:典型实现是 OrderCenterFragment 收到回调后调 + * helper.createOrder(currentCardType),成功后再 Monitor.arm(newOrderNo, ...) 续接。

+ * + * @param reason 后端 msg(如"订单已取消");可空 + * @param expired true = TIMEOUT_CANCELED(15min 过期),false = CANCELLED(用户主动 / Quartz 推进) + */ + fun onOrderCancelled(reason: String?, expired: Boolean) + + /** + * status 命中 ACTIVE(终态前置,需启动 check 轮询拿 thirdCardNo)。 + * + *

由回调方决定何时启动 check:典型实现是 OrderCenterFragment 收到回调后调 + * Monitor.startCheckJob()。Monitor 仅暴露 startCheckJob,不依赖 Helper。

+ */ + fun onOrderActive() + + /** + * check 命中 10200 或 thirdCardNo 非空 —— 卡密已下发,已写入 JetsenxSdkUtil.sUid。 + * + * @param cardNo 第三方卡号(非空) + */ + fun onCheckActivated(cardNo: String) + + /** check 30s/10 次超时,未拿到 thirdCardNo */ + fun onCheckTimeout() + + /** + * status 终态 FAILED(订单异常,如第三方激活失败等)。 + * + * @param reason 后端 msg 或异常描述 + */ + fun onOrderFailed(reason: String?) +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/order/OrderActivity.java b/app/src/main/java/com/pukka/jiecheng/order/OrderActivity.java deleted file mode 100755 index abb10a2..0000000 --- a/app/src/main/java/com/pukka/jiecheng/order/OrderActivity.java +++ /dev/null @@ -1,643 +0,0 @@ -package com.pukka.jiecheng.order; - -import static com.pukka.libcore.base.BaseConst.agentVendorCode; -import static com.pukka.libcore.base.BaseConst.businessLine; -import static com.pukka.libcore.base.BaseConst.serviceComboCode; -import static com.pukka.libcore.base.BaseConst.serviceGroupCode; - -import android.content.ComponentName; -import android.content.Intent; -import android.graphics.Bitmap; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.KeyEvent; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.leanback.widget.BrowseFrameLayout; - -import com.alibaba.fastjson.JSONObject; -import com.migu.bean.GetsystemBean; -import com.migu.callback.AijiaCallback; -import com.migu.httputils.HomeHttpUtils; -import com.pukka.jiecheng.JcConfig; -import com.pukka.jiecheng.R; -import com.pukka.jiecheng.base.BaseActivity; -import com.pukka.jiecheng.login.viewmodel.IsLoginViewModel; -import com.pukka.jiecheng.order.fragment.OrderCenterFragment; -import com.pukka.jiecheng.order.fragment.OrderDuihuanFragment; -import com.pukka.jiecheng.order.fragment.OrderListFragment; -import com.pukka.jiecheng.order.fragment.OrderXufeiFragment; -import com.pukka.jiecheng.utils.IsLoginTimerUtil; -import com.pukka.jiecheng.utils.PayResultTimerUtil; -import com.pukka.jiecheng.utils.QRCodeUtil; -import com.pukka.libcore.boxShared.BoxSharedParameters; -import com.pukka.libcore.boxShared.BoxSharedParamsUtils; -import com.pukka.libcore.boxShared.MacUtils; -import com.pukka.libcore.fresco.PkSimpleDraweeView; -import com.pukka.libcore.threading.DispatchQueue; -import com.pukka.libcore.utils.ContextUtils; -import com.pukka.libcore.utils.LogUtils; -import com.pukka.libcore.utils.StringUtils; -import com.pukka.libcore.viewmodel.BaseVM; - -import java.net.URLEncoder; - -public class OrderActivity extends BaseActivity { - - public static final String media_classify_param = "media_classify";//媒资分类 例如:电影 - public static final String content_id_param = "content_id"; - public static final String content_name_param = "content_name"; - public static final String refer_page_path_param = "refer_page_path"; - //1=订购中心 2=我的订购 - public static final String go_type_param = "go_type_param"; - - private TextView mTvOrderCenter, mTvMyOrder, mTvDuihuan, mTvXufei; - private ConstraintLayout mClOrderCenter, mClMyOrder, mClLoginQrCode, mClDuihuan, mClXufei; - private ImageView mImgLoginQrCode; - private TextView mTvUserId; - private boolean orderCenterGetFocus; - private boolean firstFocus; - private BrowseFrameLayout mBrowseFrameLayout; - private OrderListFragment orderListFragment; - private OrderCenterFragment orderCenterFragment; - private OrderDuihuanFragment orderDuihuanFragment; - private IsLoginViewModel mIsLoginViewModel; - //埋点 - String media_classify, content_id, content_name, refer_page_path; - String go_type; - private PkSimpleDraweeView clbg; - private ImageView ivGame; - private String policyCode; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_order); - initView(); - getIntentParam(); - initListener(); - topFocusSearchListener(); - if (StringUtils.isEmpty(BoxSharedParameters.getInstance().getUserId())) { - //用户没登录,只能跳转到订购中心界面 - go_type = ""; - } - if (!StringUtils.isEmpty(go_type) && go_type.equals("2")) { - mClMyOrder.requestFocus(); - mClOrderCenter.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mClMyOrder.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focus_bg)); - mTvOrderCenter.setTextColor(mContext.getResources().getColor(R.color.white30)); - mTvMyOrder.setTextColor(mContext.getResources().getColor(R.color.order_btn_text_color)); - showOrderListFragment(); - } else { - showOrderCenterFragment(); - } - } - - //从上个页面获取值 - private void getIntentParam() { - Intent intent = getIntent(); - if (intent != null) { - media_classify = intent.getStringExtra(media_classify_param); - content_id = intent.getStringExtra(content_id_param); - content_name = intent.getStringExtra(content_name_param); - refer_page_path = intent.getStringExtra(refer_page_path_param); - go_type = intent.getStringExtra(go_type_param); - LogUtils.d("media_classify=" + media_classify + " content_id=" + content_id + " content_name=" + content_name + " go_type=" + go_type); - policyCode = intent.getStringExtra("policyCode"); - LogUtils.d("policyCode=" + policyCode); - } - } - - private void initView() { - ivGame = findViewById(R.id.ivGame); - clbg = findViewById(R.id.clbg); - mImgLoginQrCode = findViewById(R.id.id_img_login_qrcode); - mClLoginQrCode = findViewById(R.id.id_cl_login_qrcode); - mTvOrderCenter = findViewById(R.id.id_tv_orderCenter); - mTvMyOrder = findViewById(R.id.id_tv_myOrder); - mClOrderCenter = findViewById(R.id.id_cl_orderCenter); - mTvXufei = findViewById(R.id.id_tv_xufei); - mClXufei = findViewById(R.id.id_cl_xufei); - mTvDuihuan = findViewById(R.id.id_tv_duihuan); - mClDuihuan = findViewById(R.id.id_cl_duihuan); - mClMyOrder = findViewById(R.id.id_cl_myOrder); - mTvUserId = findViewById(R.id.id_tv_userId); - mTvUserId.setText("账号:" + BoxSharedParameters.getInstance().getUserId()); - if (StringUtils.isEmpty(BoxSharedParameters.getInstance().getUserId())) { - LogUtils.d("用户没有登录,显示登录二维码,轮询是否登录接口"); - mTvUserId.setText(""); - mClLoginQrCode.setVisibility(View.VISIBLE); - createQRCode("KMHX"); - initSever(); - } else { - mClLoginQrCode.setVisibility(View.GONE); - } - mBrowseFrameLayout = findViewById(R.id.id_nav_bfl); - mBrowseFrameLayout.getViewTreeObserver().addOnGlobalFocusChangeListener((oldFocus, newFocus) -> { - LogUtils.d("oldFocus:" + oldFocus + " newFocus:" + newFocus); - }); - orderListFragment = OrderListFragment.newInstance(); - orderCenterFragment = OrderCenterFragment.newInstance(); - orderDuihuanFragment = OrderDuihuanFragment.newInstance(); - initGamePad(); - changeBg(); - showGameIcon(); - } - - private void initSever() { - mIsLoginViewModel = BaseVM.getSafeViewModel(this, IsLoginViewModel.class); - mIsLoginViewModel.getIsLoginData().observe(this, response -> { - LogUtils.d("轮询结果: isLogin==" + response); - if ("success".equals(response)) { - try { - mClLoginQrCode.setVisibility(View.GONE); - mTvUserId.setText("账号:" + BoxSharedParameters.getInstance().getUserId()); - LogUtils.d("显示会员卡核销输入面板"); - if (null != orderDuihuanFragment) { - orderDuihuanFragment.showVipPanel(); - } - } catch (Exception ex) { - ex.printStackTrace(); - } - } - }); - IsLoginTimerUtil.getInstance().startTimer(mIsLoginViewModel); - } - - private boolean isFirstShowOrderCenter; - - private void initListener() { - ivGame.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - intentGame(); - } - }); - mClXufei.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean b) { - if (b) { - showOrderXufeiFragment(); - } - if (b) { - mTvXufei.setTextColor(0xff4c3709); - - mClMyOrder.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mTvMyOrder.setTextColor(mContext.getResources().getColor(R.color.white30)); - } else { - mTvXufei.setTextColor(0x80ffffff); - } - } - }); - mClOrderCenter.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean b) { - LogUtils.d("mClOrderCenter onFocusChange b=" + b + " orderCenterGetFocus=" + orderCenterGetFocus); - if (orderCenterGetFocus) { - orderCenterGetFocus = false; - DispatchQueue.main.after(100, () -> { - mClMyOrder.setFocusable(true); - }); - } - if (b) { - showOrderCenterFragment(); - } - if (b) { - mClDuihuan.setFocusable(true); - mClXufei.setFocusable(false); - if (isFirstShowOrderCenter) { - mClOrderCenter.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focused_bg)); - mTvOrderCenter.setTextColor(mContext.getResources().getColor(R.color.order_list_focus_text_color)); - } else { - isFirstShowOrderCenter = true; - mClOrderCenter.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focus_bg)); - mTvOrderCenter.setTextColor(mContext.getResources().getColor(R.color.order_btn_text_color)); - } - - mClDuihuan.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mTvDuihuan.setTextColor(mContext.getResources().getColor(R.color.white30)); - mClMyOrder.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mTvMyOrder.setTextColor(mContext.getResources().getColor(R.color.white30)); - } else { - mClOrderCenter.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focus_bg)); - mTvOrderCenter.setTextColor(mContext.getResources().getColor(R.color.order_btn_text_color)); - } - } - }); - mClMyOrder.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean b) { - LogUtils.d("mClMyOrder onFocusChange b=" + b); - firstFocus = true; - if (b) { - showOrderListFragment(); - } - if (b) { - mClOrderCenter.setFocusable(true); - mClXufei.setFocusable(true); - mClMyOrder.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focused_bg)); - mTvMyOrder.setTextColor(mContext.getResources().getColor(R.color.order_list_focus_text_color)); - - mClDuihuan.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mTvDuihuan.setTextColor(mContext.getResources().getColor(R.color.white30)); - mClOrderCenter.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mTvOrderCenter.setTextColor(mContext.getResources().getColor(R.color.white30)); - } else { - mClMyOrder.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focus_bg)); - mTvMyOrder.setTextColor(mContext.getResources().getColor(R.color.order_btn_text_color)); - } - } - }); - mClDuihuan.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean b) { - LogUtils.d("mClDuihuan onFocusChange b=" + b); - if (b) { - mClOrderCenter.setFocusable(true); - showOrderDuihuanFragment(); - mClDuihuan.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focused_bg)); - mTvDuihuan.setTextColor(mContext.getResources().getColor(R.color.order_list_focus_text_color)); - - mClMyOrder.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mTvMyOrder.setTextColor(mContext.getResources().getColor(R.color.white30)); - mClOrderCenter.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mTvOrderCenter.setTextColor(mContext.getResources().getColor(R.color.white30)); - } else { - mClDuihuan.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focus_bg)); - mTvDuihuan.setTextColor(mContext.getResources().getColor(R.color.order_btn_text_color)); - } - } - }); - } - - public String getPolicyCode() { - return policyCode; - } - - public void setDuihuanFoucs() { - mClDuihuan.setFocusable(false); - } - - public void showGameIcon() { - if (BoxSharedParamsUtils.isJcCloud()) { - HomeHttpUtils.getsystemData(new AijiaCallback.GetsystemCallBack() { - @Override - public void onResponse(GetsystemBean response) { - try { - if (response == null) { - return; - } - if (response.getResult() == null - || response.getResult().isEmpty()) { - return; - } - for (GetsystemBean.ResultBean bean : response.getResult() - ) { - if (bean.getType()==5) { - int status = bean.getStatus(); - if (status == 1) { - ivGameSetVisibility(true); - } else { - ivGameSetVisibility(false); - } - break; - } - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void netFail() { - - } - - @Override - public void fail() { - - } - }); - } else { - ivGameSetVisibility(false); - } - } - - private void ivGameSetVisibility(boolean isVISIBLE) { - LogUtils.d("ivGameSetVisibility:" + isVISIBLE); - ivGame.post(new Runnable() { - @Override - public void run() { - ivGame.setVisibility(isVISIBLE ? View.VISIBLE : View.GONE); - } - }); - } - - public void mClXufeiSetFocusableNot() { - mClXufei.setFocusable(false); - } - - /** - * 我的订购按下焦点处理 - */ - public void mClMyOrderSetNextFocusDownId(boolean isTuiDingBtn) { - if (!isTuiDingBtn) { - LogUtils.d("没有退订按钮,焦点聚焦到我的订购按钮"); - mClMyOrder.setNextFocusDownId(mClMyOrder.getId()); - } else { - mClMyOrder.setNextFocusDownId(View.NO_ID); - } - } - - public void requestFocusXufei() { - mClMyOrder.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mTvMyOrder.setTextColor(mContext.getResources().getColor(R.color.white30)); - mClOrderCenter.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mTvOrderCenter.setTextColor(mContext.getResources().getColor(R.color.white30)); - mClXufei.setFocusable(true); - mClXufei.requestFocus(); - } - - public void changeBg() { - HomeHttpUtils.getsystemData(new AijiaCallback.GetsystemCallBack() { - @Override - public void onResponse(GetsystemBean response) { - try { - if (response == null) { - return; - } - if (response.getResult() == null - || response.getResult().isEmpty()) { - return; - } - for (GetsystemBean.ResultBean bean : response.getResult() - ) { - if (bean.getType() == 4) { - String corner_url = bean.getCorner_url(); - LogUtils.d("changeBg corner_url:" + corner_url); - if (!TextUtils.isEmpty(corner_url)) { - clbg.post(new Runnable() { - @Override - public void run() { - if (TextUtils.isEmpty(corner_url)) { - return; - } - clbg.setImageURI(corner_url); - } - }); - } - break; - } - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void netFail() { - - } - - @Override - public void fail() { - - } - }); - } - - public void intentGame() { - try { - LogUtils.d("跳转UserRechargeActivity 开始"); - String packageName = "com.anning.game"; - String activityFullName = "com.anning.game.ui.activity.UserRechargeActivity"; // 目标Activity完整类名 - Intent intent = new Intent(); - ComponentName comp = new ComponentName(packageName, activityFullName); - intent.setComponent(comp); - startActivity(intent); - } catch (Exception e) { - LogUtils.d("跳转UserRechargeActivity 出错"); - e.printStackTrace(); - } - } - - public void isShowClXufei(int subscription) { - LogUtils.d("wxf isShowClXufei=" + subscription); - if (subscription > 0) { - mClXufei.setVisibility(View.VISIBLE); - } else { - mClXufei.setVisibility(View.GONE); - } - } - - // TOP的按钮 焦点控制 - private void topFocusSearchListener() { - mBrowseFrameLayout.setOnFocusSearchListener((focused, direction) -> { - LogUtils.d("wxf focused=" + focused + " direction=" + direction); - if (direction == View.FOCUS_DOWN) { - LogUtils.d("wxf, topFocusSearchListener FOCUS_DOWN"); - if (focused.getId() == R.id.id_cl_orderCenter) { - mClOrderCenter.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focus_bg)); - } - } else if (direction == View.FOCUS_UP) { - LogUtils.d("wxf, topFocusSearchListener FOCUS_UP"); - - } else if (direction == View.FOCUS_RIGHT) { - LogUtils.d("wxf, topFocusSearchListener FOCUS_RIGHT"); - if (focused.getId() == R.id.id_cl_orderCenter) { - mClOrderCenter.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mClMyOrder.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focus_bg)); - mTvOrderCenter.setTextColor(mContext.getResources().getColor(R.color.white30)); - mTvMyOrder.setTextColor(mContext.getResources().getColor(R.color.order_btn_text_color)); - showOrderListFragment(); - } - } else if (direction == View.FOCUS_LEFT) { - LogUtils.d("wxf, topFocusSearchListener FOCUS_LEFT"); - if (focused.getId() == R.id.id_cl_myOrder) { - mClOrderCenter.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_focus_bg)); - mClMyOrder.setBackground(mContext.getResources().getDrawable(R.drawable.order_btn_unfocus_bg)); - mTvMyOrder.setTextColor(mContext.getResources().getColor(R.color.white30)); - mTvOrderCenter.setTextColor(mContext.getResources().getColor(R.color.order_btn_text_color)); - showOrderCenterFragment(); - } else if (focused.getId() == R.id.id_cl_xufei_ok) { - if (orderCenterFragment != null) { - orderCenterFragment.restDirection(); - } - } else if (focused.getId() == R.id.id_cl_update_ok) { - if (orderCenterFragment != null) { - orderCenterFragment.restDirection(); - } - } - } - return null; - }); - } - - private void showOrderXufeiFragment() { - LogUtils.d("showOrderCenterFragment"); - try { - hideClLoginQrCode(); - getSupportFragmentManager().beginTransaction() - .replace(R.id.id_fragment_container, OrderXufeiFragment.newInstance()) - .commit(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - private void showOrderCenterFragment() { - LogUtils.d("showOrderCenterFragment"); - try { - hideClLoginQrCode(); - getSupportFragmentManager().beginTransaction() - .replace(R.id.id_fragment_container, orderCenterFragment) - .commit(); - OrderAmberManager.orderView(refer_page_path, media_classify, content_id, content_name); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - private void showOrderDuihuanFragment() { - LogUtils.d("showOrderDuihuanFragment"); - try { - showClLoginQrCode(); - getSupportFragmentManager().beginTransaction() - .replace(R.id.id_fragment_container, orderDuihuanFragment) - .commit(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - private void showOrderListFragment() { - LogUtils.d("showOrderListFragment"); - try { - hideClLoginQrCode(); - getSupportFragmentManager().beginTransaction() - .replace(R.id.id_fragment_container, orderListFragment) - .commit(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - public void requestFocusOrderCenter() { - if (null != mClOrderCenter) { - LogUtils.d("mClOrderCenter request focus"); - orderCenterGetFocus = true; - mClMyOrder.setFocusable(false); - mClOrderCenter.setFocusable(true); - mClOrderCenter.requestFocus(); - } - } - - public void requestFocusOrderList() { - if (null != mClMyOrder) { - LogUtils.d("mClOrderCenter request focus"); - orderCenterGetFocus = false; - mClMyOrder.setFocusable(true); - mClMyOrder.requestFocus(); - } - } - - public void setOrderCenterFocusable(boolean b) { - if (null != mClOrderCenter) { - mClOrderCenter.setFocusable(b); - } - } - - - public boolean isFirstFocus() { - return firstFocus; - } - - public void setFirstFocus(boolean firstFocus1) { - this.firstFocus = firstFocus1; - } - - @Override - protected void onPause() { - super.onPause(); - PayResultTimerUtil.getInstance().stopTimer(); - IsLoginTimerUtil.getInstance().stopTimer(); - } - - /** - * 获取登录二维码 - */ - public void createQRCode(String selectedPolicyCode) { - //分别是 hid(mac地址), appId、channelId(渠道id)、serviceComboCode(业务组合标识)、serviceGroupCode(业务分组标识)、businessLine(业务线标识) - //prefix 0代表测试环境,1代表正式环境 - JSONObject jsonObject = new JSONObject(); - jsonObject.put("hid", MacUtils.getMac(ContextUtils.getApplicationContext())); - jsonObject.put("channelId", agentVendorCode); - jsonObject.put("serviceComboCode", serviceComboCode); - jsonObject.put("serviceGroupCode", serviceGroupCode); - jsonObject.put("businessLine", businessLine); - jsonObject.put("prefix", JcConfig.prefix); - jsonObject.put("genToken", "1"); - jsonObject.put("selectedPolicyCode", selectedPolicyCode); - jsonObject.put("redirect", "pages/orderCenter/orderCenter"); - try { - // 将JSON转为字符串并进行URL编码 - String encodedParams = URLEncoder.encode(jsonObject.toString(), "UTF-8"); - String codeUrl = JcConfig.weiXinURL + "?params=" + encodedParams; - LogUtils.d("codeUrl==" + codeUrl); - if (!TextUtils.isEmpty(codeUrl)) { - int smallHeight = this.getResources().getDimensionPixelSize(R.dimen.qb_px_318); - int smallWidth = this.getResources().getDimensionPixelSize(R.dimen.qb_px_318); - Bitmap imgurl = QRCodeUtil.createQRCodeBitmap(codeUrl, smallWidth, smallHeight); - if (imgurl != null) { - mImgLoginQrCode.setImageBitmap(imgurl); - } - } - } catch (Exception ex) { - ex.printStackTrace(); - } - - } - - /** - * 隐藏微信扫一扫登录二维码UI - */ - public void hideClLoginQrCode() { - if (null != mClLoginQrCode) { - LogUtils.d("隐藏微信登录二维码"); - mClLoginQrCode.setVisibility(View.GONE); - } - } - - /** - * 显示微信登录二维码 - */ - public void showClLoginQrCode() { - if (StringUtils.isEmpty(BoxSharedParameters.getInstance().getUserId())) { - if (null != mClLoginQrCode) { - LogUtils.d("显示微信登录二维码"); - mClLoginQrCode.setVisibility(View.VISIBLE); - } - } - } - - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - return super.onKeyDown(keyCode, event); - } - - private void clearFragment() { - orderListFragment = null; - orderCenterFragment = null; - orderDuihuanFragment = null; - } - - @Override - protected void onDestroy() { - super.onDestroy(); - clearFragment(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/order/OrderActivity.kt b/app/src/main/java/com/pukka/jiecheng/order/OrderActivity.kt new file mode 100644 index 0000000..d6eceaf --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/order/OrderActivity.kt @@ -0,0 +1,228 @@ +/** + * @file OrderActivity.kt + * @description 订购中心入口 Activity,包含"订购中心/兑换码/我的订购/自动续费"等 Tab 切换 + * 同时支持遥控(DPAD 焦点导航)与触摸点击两种交互模式 + * @author ainetv + * @date 2026-09-01 + */ +package com.pukka.jiecheng.order + +import android.os.Bundle +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.Fragment +import androidx.leanback.widget.BrowseFrameLayout +import com.pukka.jiecheng.R +import com.pukka.jiecheng.base.BaseActivity +import com.pukka.jiecheng.config.AppConfiguration +import com.pukka.jiecheng.order.fragment.OrderCenterFragment +import com.pukka.jiecheng.order.fragment.OrderDuihuanFragment +import com.pukka.jiecheng.order.fragment.OrderListFragment +import com.pukka.jiecheng.order.fragment.OrderXufeiFragment +import com.pukka.jiecheng.utils.JetsenxSdkUtil +import com.pukka.libcore.utils.LogUtils + +/** + * 订购中心 Activity + * + * 双模式交互策略: + * - 触摸模式:以 OnClickListener 为入口,点击 Tab 直接切换 Fragment;不绑定焦点监听,避免触摸点击触发焦点变化导致的二次切换 + * - 遥控模式:同时绑定 OnClickListener(DPAD 中键)与 OnFocusChangeListener(DPAD 方向键导航),通过 currentTab 防重入保证不会重复切换 + */ +class OrderActivity : BaseActivity() { + + // Tab 标识常量 + private companion object { + const val TAB_NONE = -1 + const val TAB_ORDER_CENTER = 0 + const val TAB_DUIHUAN = 1 + const val TAB_MY_ORDER = 2 + const val TAB_XUFEI = 3 + } + + private var mBrowseFrameLayout: BrowseFrameLayout? = null + + // ----------------------------- Tab View 引用 ----------------------------- + private var mClOrderCenter: ConstraintLayout? = null + private var mClDuihuan: ConstraintLayout? = null + private var mClMyOrder: ConstraintLayout? = null + private var mClXufei: ConstraintLayout? = null + private var mTvOrderCenter: android.widget.TextView? = null + private var mTvDuihuan: android.widget.TextView? = null + private var mTvMyOrder: android.widget.TextView? = null + private var mTvXufei: android.widget.TextView? = null + + // ----------------------------- Fragment 实例 ----------------------------- + private var orderListFragment: OrderListFragment? = null + private var orderCenterFragment: OrderCenterFragment? = null + private var orderDuihuanFragment: OrderDuihuanFragment? = null + private var orderXufeiFragment: OrderXufeiFragment? = null + + // 当前选中的 Tab。初始值设为 TAB_NONE(无效 Tab 标识), + // 让 onCreate 中首次调用 selectTab(TAB_ORDER_CENTER) 能真正进入 showOrderCenterFragment() 分支, + // 避免 selectTab 的幂等保护 `if (currentTab == tabId) return` 吞掉首次切换 + // (之前初始值=TAB_ORDER_CENTER 时 currentTab==tabId 直接 return,导致 Fragment 永远不显示)。 + private var currentTab: Int = TAB_NONE + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_order) + initView() + initListener() + // 默认展示订购中心 + selectTab(TAB_ORDER_CENTER) + // 触摸模式启用左上角悬浮返回按钮;BaseActivity 内部已自行 isTouchMode 判断 + enableBackButton() + } + + private fun initView() { + mBrowseFrameLayout = findViewById(R.id.id_nav_bfl) + mBrowseFrameLayout?.viewTreeObserver?.addOnGlobalFocusChangeListener { oldFocus, newFocus -> + LogUtils.d("oldFocus:$oldFocus newFocus:$newFocus") + } + mClOrderCenter = findViewById(R.id.id_cl_orderCenter) + mClDuihuan = findViewById(R.id.id_cl_duihuan) + mClMyOrder = findViewById(R.id.id_cl_myOrder) + mClXufei = findViewById(R.id.id_cl_xufei) + mTvOrderCenter = findViewById(R.id.id_tv_orderCenter) + mTvDuihuan = findViewById(R.id.id_tv_duihuan) + mTvMyOrder = findViewById(R.id.id_tv_myOrder) + mTvXufei = findViewById(R.id.id_tv_xufei) + + orderListFragment = OrderListFragment.newInstance() + orderCenterFragment = OrderCenterFragment.newInstance() + orderDuihuanFragment = OrderDuihuanFragment.newInstance() + orderXufeiFragment = OrderXufeiFragment.newInstance() + } + + /** + * 初始化顶部 Tab 的遥控 + 触摸事件 + * + * 触摸模式:仅绑定 OnClickListener + * 遥控模式:同时绑定 OnClickListener(DPAD 中键)与 OnFocusChangeListener(方向键) + */ + private fun initListener() { + val isTouchMode = AppConfiguration.getInstance().isTouchMode() + // 触摸模式仅 click;遥控模式额外绑 focus 监听以响应方向键 + listOf( + mClOrderCenter to TAB_ORDER_CENTER, + mClDuihuan to TAB_DUIHUAN, + mClMyOrder to TAB_MY_ORDER, + mClXufei to TAB_XUFEI + ).forEach { (cl, id) -> + cl?.setOnClickListener { selectTab(id) } + if (!isTouchMode) { + cl?.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) selectTab(id) } + } + } + } + + /** + * 切换 Tab:切换 Fragment + 刷新按钮视觉态 + * + * @param tabId 目标 Tab 标识([TAB_ORDER_CENTER] / [TAB_DUIHUAN] / [TAB_MY_ORDER] / [TAB_XUFEI]) + */ + private fun selectTab(tabId: Int) { + // 同一 Tab 多次触发(OnClick + OnFocusChange)做幂等保护 + if (currentTab == tabId) return + currentTab = tabId + updateTabVisualState(tabId) + when (tabId) { + TAB_ORDER_CENTER -> showOrderCenterFragment() + TAB_DUIHUAN -> showOrderDuihuanFragment() + TAB_MY_ORDER -> showOrderListFragment() + TAB_XUFEI -> showOrderXufeiFragment() + } + } + + /** + * 刷新顶部 Tab 按钮的背景与文字色: + * - 选中 Tab 使用 order_btn_focus_bg + order_btn_text_color + * - 未选中 Tab 使用 order_btn_unfocus_bg + white30 + */ + private fun updateTabVisualState(activeTabId: Int) { + val focusBg = R.drawable.order_btn_focus_bg + val unfocusBg = R.drawable.order_btn_unfocus_bg + val focusTextColor = resources.getColor(R.color.order_btn_text_color, theme) + val unfocusTextColor = resources.getColor(R.color.white30, theme) + + mClOrderCenter?.setBackgroundResource(if (activeTabId == TAB_ORDER_CENTER) focusBg else unfocusBg) + mClDuihuan?.setBackgroundResource(if (activeTabId == TAB_DUIHUAN) focusBg else unfocusBg) + mClMyOrder?.setBackgroundResource(if (activeTabId == TAB_MY_ORDER) focusBg else unfocusBg) + mClXufei?.setBackgroundResource(if (activeTabId == TAB_XUFEI) focusBg else unfocusBg) + + mTvOrderCenter?.setTextColor(if (activeTabId == TAB_ORDER_CENTER) focusTextColor else unfocusTextColor) + mTvDuihuan?.setTextColor(if (activeTabId == TAB_DUIHUAN) focusTextColor else unfocusTextColor) + mTvMyOrder?.setTextColor(if (activeTabId == TAB_MY_ORDER) focusTextColor else unfocusTextColor) + mTvXufei?.setTextColor(if (activeTabId == TAB_XUFEI) focusTextColor else unfocusTextColor) + } + + private fun showOrderXufeiFragment() { + showFragment(orderXufeiFragment ?: OrderXufeiFragment.newInstance().also { + orderXufeiFragment = it + }) + } + + private fun showOrderCenterFragment() { + showFragment(orderCenterFragment ?: OrderCenterFragment.newInstance().also { + orderCenterFragment = it + }) + } + + private fun showOrderDuihuanFragment() { + showFragment(orderDuihuanFragment ?: OrderDuihuanFragment.newInstance().also { + orderDuihuanFragment = it + }) + } + + private fun showOrderListFragment() { + showFragment(orderListFragment ?: OrderListFragment.newInstance().also { + orderListFragment = it + }) + } + + /** + * 使用 FragmentManager.replace 切换内容区 Fragment + */ + private fun showFragment(fragment: Fragment) { + supportFragmentManager.beginTransaction() + .replace(R.id.id_fragment_container, fragment) + .commit() + } + + /** + * 清理 Fragment 引用(onDestroy 时调用,防止内存泄漏) + */ + private fun clearFragment() { + orderListFragment = null + orderCenterFragment = null + orderDuihuanFragment = null + orderXufeiFragment = null + } + + override fun onStart() { + super.onStart() + // 用户回到 OrderActivity:取消 statusJobBg 的"销毁后兜底倒计", + // 避免在用户重新进入后被前置的倒计提前停掉(详见 JsapiOrderMonitor.notifyOrderActivityResumed) + JsapiOrderMonitor.get(this).notifyOrderActivityResumed() + } + + /** + * onResume 时若 JetsenxSdkUtil.sUid 已缓存(用户已开通/激活),自动切到"我的订购" Tab, + * 跳过默认的"订购中心"引导,直达用户当前订单状态。 + * selectTab 自带 currentTab 幂等保护,重复触发同一 Tab 不会重建 Fragment。 + */ + override fun onResume() { + super.onResume() + if (JetsenxSdkUtil.getSuid().isNotEmpty()) { + selectTab(TAB_MY_ORDER) + } + } + + override fun onDestroy() { + // 用户退出 OrderActivity:进入 statusJobBg 的"销毁后兜底倒计", + // 允许额外轮询 POST_EXIT_POLL_ITERATIONS 次后再停,checkJob 不受影响独立完成激活 + JsapiOrderMonitor.get(this).notifyOrderActivityDestroyed() + super.onDestroy() + clearFragment() + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/order/OrderAmberManager.java b/app/src/main/java/com/pukka/jiecheng/order/OrderAmberManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/OrderStyleUtils.java b/app/src/main/java/com/pukka/jiecheng/order/OrderStyleUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/adapter/OrderCenterAdapter.java b/app/src/main/java/com/pukka/jiecheng/order/adapter/OrderCenterAdapter.java old mode 100755 new mode 100644 index eb40210..280433e --- a/app/src/main/java/com/pukka/jiecheng/order/adapter/OrderCenterAdapter.java +++ b/app/src/main/java/com/pukka/jiecheng/order/adapter/OrderCenterAdapter.java @@ -14,8 +14,9 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.gson.reflect.TypeToken; import com.pukka.jiecheng.R; +import com.pukka.jiecheng.config.AppConfiguration; import com.pukka.jiecheng.filter.adapter.TopAdapter; -import com.pukka.jiecheng.order.OrderActivity; +import com.pukka.jiecheng.network.dto.BenefitPriceItemDto; import com.pukka.jiecheng.order.adapter.viewholder.CenterViewHolder; import com.pukka.jiecheng.order.adapter.viewholder.CenterVipViewHolder; import com.pukka.jiecheng.order.bean.OrderListBean; @@ -39,12 +40,21 @@ import java.util.Map; * @since V0.1.0.1 */ public class OrderCenterAdapter extends RecyclerView.Adapter { + // 部分刷新 payload:仅刷新选中态视觉,不重新绑定数据 + private static final String PAYLOAD_SELECTION_STATE = "payload_selection_state"; private static final int TYPE_HEADER = 0; private static final int TYPE_ITEM = 1; private final OrderViewModel mOrderViewModel; + // 触摸模式标记:在构造时从 AppConfiguration.isTouchMode() 捕获,运行时只走一条路径 + private final boolean isTouchMode; + // 触摸模式专用:当前选中项位置,-1 表示无选中;驱动视觉态切换 + private int selectedPosition = -1; + List> assetPrice = new ArrayList<>(); List pricingBeanList = new ArrayList<>(); + //jsapi 链路套餐列表(家用版 H5 扫码订购),非空时优先于 assetPrice 旧数据渲染 + List priceItems = new ArrayList<>(); String packageCode; boolean isDuihuan; @@ -52,7 +62,6 @@ public class OrderCenterAdapter extends RecyclerView.Adapter getPricingBeanList() { return pricingBeanList; @@ -62,28 +71,71 @@ public class OrderCenterAdapter extends RecyclerView.Adapter items) { + LogUtils.d("setPriceItems=" + (items == null ? 0 : items.size())); + priceItems.clear(); + if (items != null) { + priceItems.addAll(items); + } + // 数据刷新后重置触摸模式选中态,避免旧 position 残留导致错位视觉 + selectedPosition = -1; + notifyDataSetChanged(); + // 触摸模式:默认选中第一项 + selectFirstItem(); } @SuppressLint("NotifyDataSetChanged") public void setAssetPriceData(List> assetPrices) { LogUtils.d("setAssetPriceData="); isSubscription=0; + // 数据刷新后重置触摸模式选中态,避免旧 position 残留导致错位视觉 + selectedPosition = -1; mOrderViewModel.requestOrderList(new OrderViewModel.getOrderListCallBack() { @Override public void getOrderListCallBack(OrderListBean orderListBean) { @@ -112,7 +164,9 @@ public class OrderCenterAdapter extends RecyclerView.Adapter list = new ArrayList<>(); - for (PricingBean bean : pricingBeanList - ) { - //已定包月移除包月 - if (isSubscription==1){ - if (bean.getPolicyType() == 2 - && bean.getChargeType2() == 2){ - continue; - } + // 已订月移除月卡连续包;已订年移除月卡/年卡连续包(其余保留) + pricingBeanList.removeIf(bean -> { + if (bean.getPolicyType() != 2) { + return false; } - //已定包年移除包年和包月 - else if (isSubscription==2){ - if (bean.getPolicyType() == 2 - && bean.getChargeType2() == 2){ - continue; - }else if (bean.getPolicyType() == 2 - && bean.getChargeType2() == 5){ - continue; - } + int chargeType2 = bean.getChargeType2(); + if (isSubscription == 1) { + return chargeType2 == 2; } - list.add(bean); - } - pricingBeanList.clear(); - pricingBeanList.addAll(list); + return isSubscription == 2 && (chargeType2 == 2 || chargeType2 == 5); + }); } @Override @@ -187,46 +230,198 @@ public class OrderCenterAdapter extends RecyclerView.Adapter当 payloads 包含 [PAYLOAD_SELECTION_STATE] 时只刷视觉态,不重绑数据; + * 触摸模式下被 [updateSelectedPosition] 通过 notifyItemChanged(pos, PAYLOAD) 触发。

+ */ + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull java.util.List payloads) { + if (!payloads.isEmpty()) { + applySelectionState(holder, position); + return; + } + super.onBindViewHolder(holder, position, payloads); + } + + /** + * 应用选中态视觉(仅视觉,不触发任何业务逻辑)。 + * 触摸模式下驱动卡片聚焦/失焦;遥控模式下 selectedPosition 恒为 -1,等效 no-op。 + */ + private void applySelectionState(RecyclerView.ViewHolder vHolder, int position) { + if (vHolder instanceof CenterViewHolder) { + boolean isSelected = (position == selectedPosition); + CenterViewHolder holder = (CenterViewHolder) vHolder; + holder.itemView.setSelected(isSelected); + holder.applyItemStyle(isSelected); + } else if (vHolder instanceof CenterVipViewHolder && position == 0) { + // VIP 卡:驻留效果仍由原 OnFocusChangeListener 的 getDirection()==FOCUS_RIGHT 处理 + vipStyle(vHolder.itemView, position == selectedPosition, vHolder); + } + } + + /** + * 触摸模式专用:更新选中位置,触发旧/新位置卡片视觉态的部分刷新。 + * 与 ListNavAdapter 模板一致。 + */ + private void updateSelectedPosition(int newPosition) { + int oldPosition = selectedPosition; + if (oldPosition == newPosition) { + return; + } + selectedPosition = newPosition; + if (oldPosition > -1) { + notifyItemChanged(oldPosition, PAYLOAD_SELECTION_STATE); + } + notifyItemChanged(newPosition, PAYLOAD_SELECTION_STATE); + } + + /** + * 触摸模式专用:默认选中第一项(仅当数据已加载且当前无选中时)。 + *

由 [upData] / [setPriceItems] / [setDuihuanList] 在 notifyDataSetChanged 之后调用; + * 通过 [RecyclerView.post] 延迟到布局完成,复用点击流程触发视觉态与业务逻辑。

+ */ + public void selectFirstItem() { + if (!isTouchMode) { + return; + } + if (selectedPosition != -1) { + return; + } + int itemCount = getItemCount(); + if (itemCount == 0) { + return; + } + if (mgRecyclerView == null) { + return; + } + final int firstItemPosition = 0; + mgRecyclerView.post(new Runnable() { + @Override + public void run() { + RecyclerView.ViewHolder vh = mgRecyclerView.findViewHolderForAdapterPosition(firstItemPosition); + if (vh == null) { + return; + } + updateSelectedPosition(firstItemPosition); + if (vh instanceof CenterViewHolder) { + final CenterViewHolder ch = (CenterViewHolder) vh; + // getItemCount() 已保证 firstItemPosition 落在合法区间 + if (!priceItems.isEmpty()) { + ch.performJsapiItemFocus(priceItems.get(firstItemPosition), mOrderViewModel, onJsapiItemFocus); + } else { + ch.performItemFocus(firstItemPosition, isSubscription, packageCode, pricingBeanList, mOrderViewModel); + } + } else if (vh instanceof CenterVipViewHolder) { + // isDuihuan 模式:VIP 卡直接显示 + if (mOrderViewModel != null) { + mOrderViewModel.showOrHideVipPanel(true); + } + vipStyle(vh.itemView, true, vh); + } + } + }); + } + @NonNull private void vipStyle(View view, boolean b, RecyclerView.ViewHolder holder) { @@ -237,9 +432,9 @@ public class OrderCenterAdapter extends RecyclerView.Adapter { - private List list = new ArrayList<>(); // 当前显示的数据 + private List list = new ArrayList<>(); // 当前显示的数据 private Context context; - public OrderListAdapter(List list, Context context) { + public OrderListAdapter(List list, Context context) { this.list = list; this.context = context; } @@ -41,146 +36,51 @@ public class OrderListAdapter extends RecyclerView.Adapter= 10 ? v.substring(0, 10) : v; } - /** - * 是否在7天以内 - * */ - public boolean isIn7Day(String payTime){ - //当前时间和支付时间间隔多少秒 - int tempSecond = DateUtils.getSecondCompareStringDate(payTime,DateUtils.getStringNowDate()); - //一天多少秒 - long seconds7Days = TimeUnit.DAYS.toSeconds(7); - LogUtils.d("tempSecond==="+tempSecond+" seconds7Days="+seconds7Days+" "); - if(tempSecond - seconds7Days <= 0){ - return true; - } - return false; - } - - public void setList(List list) { - this.list = list; + public void setList(List list) { + this.list = list == null ? new ArrayList<>() : list; notifyDataSetChanged(); } - @Override public int getItemCount() { return list == null ? 0 : list.size(); @@ -188,13 +88,12 @@ public class OrderListAdapter extends RecyclerView.Adapter pricingBeanList, OrderViewModel mOrderViewMode,int isSubscription) { + /** + * 绑定 jsapi 链路套餐数据(家用版 H5 扫码订购,POST /api/jst/benefit/order/price)。 + * + * 选中(获取焦点)时通过 [onJsapiItemFocus] 通知宿主 Fragment 调 jsapi/create 创建扫码订单, + * 并复用 showOrHideQrCodePanel 控制右侧二维码面板显隐。 + */ + public void bindJsapiItemData(BenefitPriceItemDto item, OrderViewModel mOrderViewMode, + OrderCenterAdapter.OnJsapiItemFocus onJsapiItemFocus) { + if (item == null) { + return; + } + try { + //售价:优先用后端友好字符串 price(如 "29.90"),空时 priceFen/100 兜底 + String priceStr = formatJsapiPrice(item.getPrice(), item.getPriceFen()); + SpannableString spannableString = new SpannableString(priceStr + "元"); + int endSize = spannableString.length(); + spannableString.setSpan( + new AbsoluteSizeSpan(24, true), // 24sp + endSize - 1, endSize, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ); + mTvFees.setText(spannableString); + //原价(如有):显示删除线 "(原价xx元)";null/0 时隐藏 + String originalPriceStr = formatJsapiPrice(item.getOriginalPrice(), item.getOriginalPriceFen()); + if (originalPriceStr != null) { + mTvPrice.setText("(原价" + originalPriceStr + "元)"); + } else { + mTvPrice.setText(""); + } +// String typeName = getJsapiCardTypeName(item.getCardType()); +// mTvTypeName.setText(typeName); +// mTyTypeName2.setText(typeName); + //商品名(焦点态/非焦点态两处均要刷新) + String goodsName = item.getGoodsName() != null ? item.getGoodsName() : ""; + mTvProductName.setText(goodsName); + mTyProductName2.setText(goodsName); + //商品描述(仅焦点态展示) + mTvProductInfo.setText(item.getDescription() != null ? item.getDescription() : ""); + tvXufei.setVisibility(View.INVISIBLE); + + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + /** + * 格式化 jsapi 价格展示:优先用后端字符串(如 "29.90"),空时 priceFen/100 兜底。 + * 返回 null 表示无价格信息。 + */ + private String formatJsapiPrice(String priceStr, Integer priceFen) { + if (priceStr != null && !priceStr.isEmpty()) { + return priceStr; + } + if (priceFen != null && priceFen > 0) { + return (priceFen / 100.0) + ""; + } + return null; + } + + /** + * jsapi 卡种标识转中文名(cardType: DAY/MONTH/YEAR) + */ + private String getJsapiCardTypeName(String cardType) { + if (cardType == null) { + return ""; + } + switch (cardType) { + case "DAY": + return "天卡"; + case "MONTH": + return "月卡"; + case "YEAR": + return "年卡"; + default: + return cardType; + } + } + + /** + * 遥控模式专用:绑定 [OnFocusChangeListener],由焦点变化驱动视觉态与业务逻辑。 + * 视觉态、二维码面板显隐、isLianXunBao 由本方法内联处理; + * 业务逻辑(createPreOrder、年卡升级、埋点等)委托给 [performItemFocus]。 + */ + public void setFocusListener(int position, String productCode, List pricingBeanList, OrderViewModel mOrderViewMode, int isSubscription) { mClOrderCenterItem.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View view, boolean b) { @@ -131,97 +215,108 @@ public class CenterViewHolder extends RecyclerView.ViewHolder { } } //0 显示 1隐藏 2当前选中年续并已订月续 - int isHideQrCodePanel = b?0:1; + int isHideQrCodePanel = b ? 0 : 1; //这里判断是否是 年续并已订月续 - if (b&& isSubscription==1 - &&pricingBeanList.get(position).getChargeType2()==5 - && pricingBeanList.get(position).getPolicyType()==2){ + if (b && isSubscription == 1 + && pricingBeanList.get(position).getChargeType2() == 5 + && pricingBeanList.get(position).getPolicyType() == 2) { isHideQrCodePanel = 2; } itemStyle(view, b); mOrderViewMode.showOrHideQrCodePanel(isHideQrCodePanel); //是否是连续包产品 - boolean isLianXu ; - if(b && mTvTypeName.getText().toString().contains("连续")){ + boolean isLianXu; + if (b && mTvTypeName.getText().toString().contains("连续")) { isLianXu = true; - }else{ + } else { isLianXu = false; } mOrderViewMode.isLianXunBao(isLianXu); - try { - if (b) { - mOrderViewMode.mCurrentPolicyCode = pricingBeanList.get(position).getPolicyCode(); - PayResultTimerUtil.getInstance().stopTimer(); - //判断是否是年卡选项 - int chargeType = pricingBeanList.get(position).getChargeType(); - int chargeType2 = pricingBeanList.get(position).getChargeType2(); - boolean isYearCard = isYearCard(chargeType, chargeType2); - if (pricingBeanList.get(position).getPolicyType()==2){ - //续费的话不执行升级 - isYearCard = false; - } - //如果订购了连续包月、包年就不升级 - if (isSubscription==1||isSubscription==2){ - isYearCard = false; - } - if (!isYearCard) { - //非年卡,显示二维码实现订购 - String policyCode = pricingBeanList.get(position).getPolicyCode(); - mOrderViewMode.createPreOrder(productCode, policyCode, "",pricingBeanList.get(position).getPolicyType()==2?3:0); - String policyName = pricingBeanList.get(position).getPolicyName(); - OrderAmberManager.orderProductClick(policyCode, policyName, chargeType); - OrderAmberManager.orderPrice = pricingBeanList.get(position).getDiscountPrice() / 100.0 + ""; - } else { - //年卡 - LogUtils.d("is year card , go to request order list"); - OrderAmberManager.orderProductClick(pricingBeanList.get(position).getPolicyCode(), pricingBeanList.get(position).getPolicyName(), chargeType); - OrderAmberManager.orderPrice = pricingBeanList.get(position).getDiscountPrice() / 100.0 + ""; - mOrderViewMode.requestOrderList(orderListBean -> { - try { - if (null != orderListBean) { - if (orderListBean.getRetCode().equals("00000000")) { - int orderchargeType2 = orderListBean.getRetInfo().getRows().get(0).getChargeType2(); - LogUtils.d("orderchargeType2:" + orderchargeType2); - if (orderchargeType2 != 5) { - //不是年卡,可以升级 - //expireTime 2025-07-07 15:04:24 订单权益失效时间 - String expireTime = orderListBean.getRetInfo().getRows().get(0).getExpireTime(); - //订单状态:1-已订购、2-退订、6-升级年 - int status = orderListBean.getRetInfo().getRows().get(0).getStatus(); - LogUtils.d("status:" + status + " expireTime:" + expireTime); - int temp = DateUtils.compareDate(DateUtils.getNowDate(), DateUtils.convertToDate(expireTime, "yyyy-MM-dd HH:mm:ss")); - if (temp < 0 && status == 1) { - //仍在生效期 且 已订购 可以升级 supportUpgrade:订单是否支持升级,true表示可以升级,false表示不可升级 - LogUtils.d("have not expire temp=" + temp); - LogUtils.d("==" + orderListBean.getRetInfo().getRows().get(0).toString()); - - String yearPolicyName = pricingBeanList.get(position).getPolicyName(); - orderListBean.getRetInfo().getRows().get(0).setYearPolicyName(yearPolicyName); - int yearDiscountPrice = pricingBeanList.get(position).getDiscountPrice(); - orderListBean.getRetInfo().getRows().get(0).setYearDiscountPrice(yearDiscountPrice); - mOrderViewMode.rowsBeanData.postValue(orderListBean.getRetInfo().getRows().get(0)); - return; - } - } - } - } - } catch (Exception ex) { - ex.printStackTrace(); - } - //没有任何订购订单 - String policyCode = pricingBeanList.get(position).getPolicyCode(); - mOrderViewMode.createPreOrder(productCode, policyCode, "",pricingBeanList.get(position).getPolicyType()==2?3:0); - - }); - } - } - } catch (Exception ex) { - ex.printStackTrace(); + if (b) { + // 焦点获得时执行下单业务逻辑(与触摸模式点击 onItemClicked 共用) + performItemFocus(position, isSubscription, productCode, pricingBeanList, mOrderViewMode); } } }); } + /** + * 焦点获得时的下单业务逻辑。 + *

遥控模式由 [setFocusListener] 的 OnFocusChangeListener (b=true) 调用; + * 触摸模式由 OrderCenterAdapter 的点击回调直接调用,与遥控模式行为等价。

+ */ + public void performItemFocus(int position, int isSubscription, String productCode, + List pricingBeanList, OrderViewModel mOrderViewMode) { + try { + mOrderViewMode.mCurrentPolicyCode = pricingBeanList.get(position).getPolicyCode(); + PayResultTimerUtil.getInstance().stopTimer(); + //判断是否是年卡选项 + int chargeType = pricingBeanList.get(position).getChargeType(); + int chargeType2 = pricingBeanList.get(position).getChargeType2(); + boolean isYearCard = isYearCard(chargeType, chargeType2); + if (pricingBeanList.get(position).getPolicyType() == 2) { + //续费的话不执行升级 + isYearCard = false; + } + //如果订购了连续包月、包年就不升级 + if (isSubscription == 1 || isSubscription == 2) { + isYearCard = false; + } + if (!isYearCard) { + //非年卡,显示二维码实现订购 + String policyCode = pricingBeanList.get(position).getPolicyCode(); + mOrderViewMode.createPreOrder(productCode, policyCode, "", pricingBeanList.get(position).getPolicyType() == 2 ? 3 : 0); + String policyName = pricingBeanList.get(position).getPolicyName(); + OrderAmberManager.orderProductClick(policyCode, policyName, chargeType); + OrderAmberManager.orderPrice = pricingBeanList.get(position).getDiscountPrice() / 100.0 + ""; + } else { + //年卡 + LogUtils.d("is year card , go to request order list"); + OrderAmberManager.orderProductClick(pricingBeanList.get(position).getPolicyCode(), pricingBeanList.get(position).getPolicyName(), chargeType); + OrderAmberManager.orderPrice = pricingBeanList.get(position).getDiscountPrice() / 100.0 + ""; + mOrderViewMode.requestOrderList(orderListBean -> { + try { + if (null != orderListBean) { + if (orderListBean.getRetCode().equals("00000000")) { + int orderchargeType2 = orderListBean.getRetInfo().getRows().get(0).getChargeType2(); + LogUtils.d("orderchargeType2:" + orderchargeType2); + if (orderchargeType2 != 5) { + //不是年卡,可以升级 + //expireTime 2025-07-07 15:04:24 订单权益失效时间 + String expireTime = orderListBean.getRetInfo().getRows().get(0).getExpireTime(); + //订单状态:1-已订购、2-退订、6-升级年 + int status = orderListBean.getRetInfo().getRows().get(0).getStatus(); + LogUtils.d("status:" + status + " expireTime:" + expireTime); + int temp = DateUtils.compareDate(DateUtils.getNowDate(), DateUtils.convertToDate(expireTime, "yyyy-MM-dd HH:mm:ss")); + if (temp < 0 && status == 1) { + //仍在生效期 且 已订购 可以升级 supportUpgrade:订单是否支持升级,true表示可以升级,false表示不可升级 + LogUtils.d("have not expire temp=" + temp); + LogUtils.d("==" + orderListBean.getRetInfo().getRows().get(0).toString()); + + String yearPolicyName = pricingBeanList.get(position).getPolicyName(); + orderListBean.getRetInfo().getRows().get(0).setYearPolicyName(yearPolicyName); + int yearDiscountPrice = pricingBeanList.get(position).getDiscountPrice(); + orderListBean.getRetInfo().getRows().get(0).setYearDiscountPrice(yearDiscountPrice); + mOrderViewMode.rowsBeanData.postValue(orderListBean.getRetInfo().getRows().get(0)); + return; + } + } + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } + //没有任何订购订单 + String policyCode = pricingBeanList.get(position).getPolicyCode(); + mOrderViewMode.createPreOrder(productCode, policyCode, "", pricingBeanList.get(position).getPolicyType() == 2 ? 3 : 0); + + }); + } + } catch (Exception ex) { + ex.printStackTrace(); + } + } + /** * 样式切换 */ @@ -242,7 +337,7 @@ public class CenterViewHolder extends RecyclerView.ViewHolder { } ViewGroup.LayoutParams params = mClOrderCenterItem.getLayoutParams(); // 修改宽高 - params.width = (int) view.getContext().getResources().getDimension(R.dimen.qb_px_741); +// params.width = (int) view.getContext().getResources().getDimension(R.dimen.qb_px_741); params.height = height; mClOrderCenterItem.setLayoutParams(params); @@ -261,7 +356,7 @@ public class CenterViewHolder extends RecyclerView.ViewHolder { ViewGroup.LayoutParams params = mClOrderCenterItem.getLayoutParams(); // 修改宽高 - params.width = (int) view.getContext().getResources().getDimension(R.dimen.qb_px_741); +// params.width = (int) view.getContext().getResources().getDimension(R.dimen.qb_px_741); params.height = (int) view.getContext().getResources().getDimension(R.dimen.qb_px_108); mClOrderCenterItem.setLayoutParams(params); @@ -317,5 +412,50 @@ public class CenterViewHolder extends RecyclerView.ViewHolder { return isYearCard; } + /** + * 遥控模式专用(jsapi 链路):绑定 [OnFocusChangeListener],由焦点变化驱动视觉态与业务回调。 + * 由 OrderCenterAdapter 在遥控模式下调用。 + */ + public void setJsapiFocusListener(BenefitPriceItemDto item, OrderViewModel mOrderViewModel, + OrderCenterAdapter.OnJsapiItemFocus onJsapiItemFocus) { + mClOrderCenterItem.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View view, boolean b) { + itemStyle(view, b); + //0 显示 1隐藏:焦点在套餐上显示二维码面板 + if (mOrderViewModel != null) { + mOrderViewModel.showOrHideQrCodePanel(b ? 0 : 1); + } + if (b && onJsapiItemFocus != null) { + onJsapiItemFocus.onItemFocused(item); + } + } + }); + } + + /** + * 触摸模式专用(jsapi 链路):触发卡片获得焦点的视觉态与业务回调。 + * 由 OrderCenterAdapter 的点击回调直接调用,与遥控模式 OnFocusChangeListener (b=true) 行为等价。 + */ + public void performJsapiItemFocus(BenefitPriceItemDto item, OrderViewModel mOrderViewModel, + OrderCenterAdapter.OnJsapiItemFocus onJsapiItemFocus) { + itemStyle(mClOrderCenterItem, true); + if (mOrderViewModel != null) { + mOrderViewModel.showOrHideQrCodePanel(0); + } + if (onJsapiItemFocus != null) { + onJsapiItemFocus.onItemFocused(item); + } + } + + /** + * 触摸模式专用:仅切换卡片视觉态(不触发任何业务逻辑)。 + * 由 OrderCenterAdapter 在 PAYLOAD 部分刷新选中状态时调用。 + * + * @param isSelected true 聚焦态,false 非聚焦态 + */ + public void applyItemStyle(boolean isSelected) { + itemStyle(mClOrderCenterItem, isSelected); + } } diff --git a/app/src/main/java/com/pukka/jiecheng/order/adapter/viewholder/CenterVipViewHolder.java b/app/src/main/java/com/pukka/jiecheng/order/adapter/viewholder/CenterVipViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/bean/ActivateBean.java b/app/src/main/java/com/pukka/jiecheng/order/bean/ActivateBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/bean/GetPayResultCycle.java b/app/src/main/java/com/pukka/jiecheng/order/bean/GetPayResultCycle.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/bean/OrderListBean.java b/app/src/main/java/com/pukka/jiecheng/order/bean/OrderListBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/bean/PricingBean.java b/app/src/main/java/com/pukka/jiecheng/order/bean/PricingBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/bean/QrCodeBean.java b/app/src/main/java/com/pukka/jiecheng/order/bean/QrCodeBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/dialog/OrderCancelDialog.java b/app/src/main/java/com/pukka/jiecheng/order/dialog/OrderCancelDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/dialog/OrderDialog.java b/app/src/main/java/com/pukka/jiecheng/order/dialog/OrderDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderCenterFragment.java b/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderCenterFragment.java old mode 100755 new mode 100644 index b42fb98..12d29ff --- a/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderCenterFragment.java +++ b/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderCenterFragment.java @@ -5,12 +5,14 @@ import static com.pukka.libcore.base.BaseConst.businessLine; import static com.pukka.libcore.base.BaseConst.serviceComboCode; import static com.pukka.libcore.base.BaseConst.serviceGroupCode; +import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; import android.util.Base64; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -23,23 +25,26 @@ import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.widget.NestedScrollView; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import com.alibaba.fastjson.JSONObject; import com.pukka.jiecheng.JcConfig; import com.pukka.jiecheng.R; +import com.pukka.jiecheng.config.AppConfiguration; +import com.pukka.jiecheng.network.dto.BenefitPriceItemDto; +import com.pukka.jiecheng.order.JsapiOrderHelper; +import com.pukka.jiecheng.order.JsapiOrderMonitor; +import com.pukka.jiecheng.order.JsapiOrderMonitorCallback; import com.pukka.jiecheng.order.OrderActivity; import com.pukka.jiecheng.order.OrderAmberManager; import com.pukka.jiecheng.order.adapter.OrderCenterAdapter; import com.pukka.jiecheng.order.viewmodel.OrderViewModel; +import com.pukka.jiecheng.utils.JetsenxSdkUtil; import com.pukka.jiecheng.utils.PayResultTimerUtil; import com.pukka.jiecheng.utils.QRCodeUtil; import com.pukka.jiecheng.utils.ToastUtils; import com.pukka.jiecheng.widget.SpaceItemDecoration; -import com.pukka.libcore.boxShared.BoxSharedParameters; import com.pukka.libcore.boxShared.MacUtils; import com.pukka.libcore.fresco.PkSimpleDraweeView; -import com.pukka.libcore.threading.DispatchQueue; import com.pukka.libcore.utils.ContextUtils; import com.pukka.libcore.utils.DateUtils; import com.pukka.libcore.utils.LogUtils; @@ -50,17 +55,25 @@ import com.pukka.libcore.widget.VerticalRecyclerViewTv; import java.io.ByteArrayOutputStream; import java.net.URLEncoder; +import java.util.List; /** * 订购中心 **/ -public class OrderCenterFragment extends Fragment { +public class OrderCenterFragment extends Fragment implements JsapiOrderMonitorCallback { + + private static final String TAG = "OrderCenterFragment"; private VerticalRecyclerViewTv mVerticalRecyclerViewTv; private Context mContext; private OrderViewModel mOrderViewModel; private OrderCenterAdapter mOrderCenterAdapter; private VerticalLinearLayoutManagerTv mOrderLayoutManager; + //jsapi 链路助手(家用版 H5 扫码订购:套餐列表 + 创建扫码订单) + private JsapiOrderHelper mJsapiHelper; + //是否处于家庭版展示模式(依据 AppConfiguration.isHomeEdition()), + //true 时仅渲染 jsapi priceItems、隐藏连续包月/年升级、VipPanel、续费二维码 Tip 等商用专属 UI + private final boolean isHomeEdition = AppConfiguration.getInstance().isHomeEdition(); private TextView mTvMoney, mTvTemp; // private TextView mTvWeixinTip; @@ -81,6 +94,18 @@ public class OrderCenterFragment extends Fragment { private NestedScrollView nsv; private ImageView mImgQrcodeTip; + //jsapi 链路持久提示文案(spec §6.1 #14:阶段 3/4 状态机 UI) + // 复用 fragment_order_center.xml 的 id_tv_temp(默认「请扫码支付」),按状态机切换 + private static final String JSAPI_HINT_SCANNING = "请扫码支付"; + private static final String JSAPI_HINT_PAID = "支付成功,正在激活..."; + private static final String JSAPI_HINT_ACTIVATING = "正在激活..."; + private static final String JSAPI_HINT_ACTIVE = "已激活,正在下发权益..."; + private static final String JSAPI_HINT_SUCCESS = "订购成功"; + private static final String JSAPI_HINT_CHECK_FAILED = "权益下发超时,请稍后再试"; + + /** 当前选中套餐 cardType,recreate(CANCELLED/TIMEOUT_CANCELED 重建)时使用 */ + private String mCurrentCardType = null; + public OrderCenterFragment() { } @@ -103,13 +128,299 @@ public class OrderCenterFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mContext = getActivity(); + //家庭版展示模式使用 BaseVM 仅作 LiveData 桥,避免误触发商用 JetsenxSdk 链路 mOrderViewModel = BaseVM.getSafeViewModel(this, OrderViewModel.class); + // 单例 lazy-init(仅持有 applicationContext,不创建任何 Job) + JsapiOrderMonitor.Companion.get(ContextUtils.getContext()); initView(view); initListener(); - LogUtils.d("setAssetPriceData= start"); - mOrderViewModel.getAllAssetPrices(); - registerObserver(); + //家庭版模式:仅初始化 jsapi 链路(拉套餐 + 选中创建订单),不走 getAllAssetPrices/升级等观察者 + initJsapiOrder(); + if (!isHomeEdition) { + registerObserver(); + } + } + /** + * jsapi 链路初始化(家用版 H5 扫码订购): + * 1. 拉取门店在售套餐列表(POST /api/jst/benefit/order/price); + * 2. 选中套餐(列表项获焦)后创建/复用扫码订单(POST /api/jst/benefit/order/jsapi/create), + * 用返回的 h5Url 生成二维码展示,15 分钟过期后由助手自动重新创建; + * 3. 阶段 3 status 轮询不在此处启动:orderNo 由 create 返回,必须等 create 成功 + * 后才有意义,故轮询在 [createJsapiOrder] 成功回调内启动。 + */ + private void initJsapiOrder() { + //绑定视图生命周期:Fragment 视图销毁时自动取消在途请求与到期刷新协程 + mJsapiHelper = new JsapiOrderHelper( + androidx.lifecycle.LifecycleOwnerKt.getLifecycleScope(getViewLifecycleOwner())); + loadJsapiPrices(); + + //选中套餐 → 统一走 onJsapiItemSelected(内部按 cachedH5Url 分流 create / switchItem) + mOrderCenterAdapter.setOnJsapiItemFocus(item -> onJsapiItemSelected(item)); + } + + /** + * 拉取门店在售套餐列表并渲染。 + * + *

加载成功后自动触发第一个套餐的选中逻辑(遥控模式下由 [OrderCenterAdapter.selectFirstItem] + * 兜底,但仅触摸模式生效),保证开屏即可见二维码面板,符合家用版"开屏即扫码"交互预期。

+ */ + private void loadJsapiPrices() { + mJsapiHelper.loadPrices((items, code, msg) -> { + if (items == null) { + LogUtils.d("jsapi loadPrices fail code=" + code + " msg=" + msg); + ToastUtils.showLongToast(TextUtils.isEmpty(msg) ? "套餐获取失败,请稍后重试" : msg); + return; + } + LogUtils.d("jsapi loadPrices success size=" + items.size()); + mOrderCenterAdapter.setPriceItems(items); + // 修复:遥控模式下 OrderCenterAdapter.selectFirstItem() 不生效(仅触摸模式生效), + // 这里兜底手动触发首个套餐选中;触摸模式 selectFirstItem 内部已触发,避免双 create + autoTriggerFirstItemSelection(items); + }); + } + + /** + * 遥控模式专用:默认选中第一个套餐并触发 [onJsapiItemSelected]。 + * + *

触摸模式下 [OrderCenterAdapter.selectFirstItem] 已在 [OrderCenterAdapter.setPriceItems] + * 内部 post 触发回调,此处直接跳过避免双 create。

+ */ + private void autoTriggerFirstItemSelection(List items) { + if (items == null || items.isEmpty()) { + return; + } + if (AppConfiguration.getInstance().isTouchMode()) { + // 触摸模式:selectFirstItem 已触发回调 + return; + } + if (mVerticalRecyclerViewTv == null) { + return; + } + final BenefitPriceItemDto first = items.get(0); + // post 等 RecyclerView 完成首帧布局后触发,避免 itemView 未 attach 时回调异常 + mVerticalRecyclerViewTv.post(() -> { + LogUtils.d("jsapi auto-select first item cardType=" + first.getCardType() + + " pricingId=" + first.getPricingId()); + onJsapiItemSelected(first); + }); + } + + /** + * 选中套餐的统一处理入口(spec §3.1 阶段 1 → 阶段 2 / 阶段 2.5): + *
    + *
  • 首次或 CANCEL 重建后([JsapiOrderHelper.hasCachedOrder] 返回 false)→ 走 + * [createJsapiOrder] 真正发起 /benefit/order/jsapi/create 网络请求
  • + *
  • 已 create 过([JsapiOrderHelper.hasCachedOrder] 返回 true)→ 走 + * [JsapiOrderHelper.switchItem] 本地拼 pricingId 刷新二维码,**零网络请求**
  • + *
+ * + *

触摸模式 click、遥控模式 focus、加载完成后自动选中 都收敛到本方法, + * 与 [OrderCenterAdapter.OnJsapiItemFocus] 回调完全等价。

+ */ + private void onJsapiItemSelected(BenefitPriceItemDto item) { + if (item == null || item.getCardType() == null) { + return; + } + LogUtils.d("jsapi item selected cardType=" + item.getCardType() + + " pricingId=" + item.getPricingId()); + // 记录当前选中套餐的 cardType,recreate(CANCELLED/TIMEOUT_CANCELED 重建)时使用 + mCurrentCardType = item.getCardType(); + // 更新价格展示(与原逻辑一致:qrCodeDate 观察者也会读 OrderAmberManager.orderPrice) + int priceFen = item.getPriceFen() != null ? item.getPriceFen() : 0; + String price = priceFen / 100.0 + ""; + OrderAmberManager.orderPrice = price; + mTvMoney.setText("¥" + price); + + // 切换套餐 / 重新聚焦时,把持久提示重置为「请扫码支付」: + // 上一次 status 命中 PAID/ACTIVATING/ACTIVE 的提示需清除,避免误导用户 + // (CANCEL 重建也会走此路径,因为 helper.cancelCreateCache() 把 cachedH5Url 清空后 + // hasCachedOrder() 返回 false,最终落到 createJsapiOrder → showJsapiQrCode,hint 兜底重置) + updateJsapiStatusHint(JSAPI_HINT_SCANNING); + + // 分流(spec §6.1 #12):cachedH5Url 决定 create vs switchItem + if (mJsapiHelper.hasCachedOrder()) { + String qrUrl = mJsapiHelper.switchItem(item); + if (qrUrl != null) { + showJsapiQrCode(qrUrl); + } else { + // 兜底:缓存存在但 switchItem 拼不出来(缺 pricingId),回退 create + LogUtils.w("jsapi switchItem returned null, fallback to createOrder"); + createJsapiOrder(item); + } + } else { + createJsapiOrder(item); + } + } + + /** + * 创建/复用 H5 扫码订单并展示二维码。 + * + *

阶段 3 status 轮询在此回调的"成功分支"启动(orderNo 在此响应里返回); + * 助手内部已有幂等保护([com.pukka.jiecheng.order.JsapiOrderHelper.startStatusPolling] + * 对已运行的 statusJob 直接 return),后续切套餐 / CANCEL 重 create 不会重复挂载。

+ * + *

[修复] 入参改为 [BenefitPriceItemDto],create 接口要求 cardType 必传; + * QR URL 拼接需要 [BenefitPriceItemDto.getPricingId],见 spec §2.3 备注。

+ */ + private void createJsapiOrder(BenefitPriceItemDto item) { + mJsapiHelper.createOrder(item.getCardType(), (dto, code, msg) -> { + if (dto == null || TextUtils.isEmpty(dto.getH5Url())) { + LogUtils.d("jsapi createOrder fail code=" + code + " msg=" + msg); + ToastUtils.showLongToast(TextUtils.isEmpty(msg) ? "订单创建失败,请稍后重试" : msg); + return; + } + LogUtils.d("jsapi createOrder success orderNo=" + dto.getOrderNo() + + " reused=" + dto.getReused() + " expireTime=" + dto.getExpireTime()); + // 阶段 2.5:h5Url + pricingId 拼 QR URL(spec §3.1 阶段 2.5 + §6.1 #6) + String qrUrl = mJsapiHelper.composeQrUrl(dto.getH5Url(), item.getPricingId()); + if (qrUrl != null) { + showJsapiQrCode(qrUrl); + } else { + // 兜底:理论上 h5Url 非空时会返回拼接结果 + showJsapiQrCode(dto.getH5Url()); + } + + // 阶段 3:把元数据交给 Monitor,启动 statusJobBg 后台轮询(AppScope 跨 Activity 存活) + // 注意:Monitor.startStatusJobBg() 是幂等的,若已 running 直接 return + JsapiOrderMonitor monitor = JsapiOrderMonitor.Companion.get(ContextUtils.getContext()); + // expireTime 为 null(ORDER_CREATED 时设备永久,Monitor 仅记录不据此重建) + monitor.arm(dto.getOrderNo(), item.getCardType(), null); + monitor.startStatusJobBg(); + }); + } + + /** + * 更新 jsapi 链路持久提示文案(复用 id_tv_temp)。 + * + *

spec §6.1 #14:阶段 3/4 状态机 UI 在右侧二维码面板下方展示; + * 共用 [id_tv_temp] TextView(layout 默认「请扫码支付」),避免新增 View 引发布局抖动。

+ * + *

调用时机:

+ *
    + *
  • [onJsapiItemSelected]:切换套餐 / 重新聚焦时 → 重置为 SCANNING
  • + *
  • create 成功:[showJsapiQrCode] 已通过 [JSAPI_HINT_SCANNING] 兜底
  • + *
  • status 回调:→ PAID / ACTIVATING / ACTIVE
  • + *
  • check 回调:→ SUCCESS / CHECK_FAILED
  • + *
+ * + * @param hint 待展示文案(不允许为空;为空走兜底「请扫码支付」) + */ + private void updateJsapiStatusHint(String hint) { +// if (mTvTemp == null) { +// return; +// } +// mTvTemp.setText(TextUtils.isEmpty(hint) ? JSAPI_HINT_SCANNING : hint); + } + + /** + * 将 H5 订购链接生成二维码并展示(用户扫码后进入 H5 完成支付) + */ + private void showJsapiQrCode(String h5Url) { + Bitmap bitmap = null; + try { + int size = getResources().getDimensionPixelSize(R.dimen.qb_px_318); + bitmap = QRCodeUtil.createQRCodeBitmap(h5Url, size, size); + if (bitmap == null) { + LogUtils.e("jsapi qr code bitmap is null"); + return; + } + //Fresco 视图不支持 setImageBitmap,沿用 base64 data URI 方案(与登录二维码一致) + //拼 base64 后立即回收 bitmap,避免切换套餐时 318x318 PNG (~400KB) 双份内存占用 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); + String base64 = Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT); + mImgQrcode.setImageURI("data:image/png;base64," + base64); + } finally { + baos.close(); + } + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + if (bitmap != null && !bitmap.isRecycled()) { + bitmap.recycle(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + // 注册 Monitor 状态回调(Fragment 可见时订阅,不可见时注销) + JsapiOrderMonitor.Companion.get(ContextUtils.getContext()).registerCallback(this); + // 同步当前权益显示(spec §5.1 ④) + fetchActiveBenefits(); + } + + /** + * 调 /me/benefits 拉当前权益(spec §5.1 ④,端点原 active-benefits 已废弃)。 + * + *

权益非空时刷新"已激活"UI;权益空时保持当前 Fragment 状态。 + * 用 Java 直接调 Retrofit DSL(项目内已有 JsapiOrderHelper.loadPrices 同款写法)。

+ */ + private void fetchActiveBenefits() { + // Kotlin DSL 顶层 object/扩展函数 + suspend lambda 不能被 Java 链式调用, + // 走 [OrderNetworkBridge.fetchActiveBenefits] 这层薄封装(原 Kt 包装内已 cancel-safe)。 + // 本页只关心"权益 ACTIVE 信号",忽略 onSuccess 入参(权益明细由其他回调渲染)。 + OrderNetworkBridge.INSTANCE.fetchActiveBenefits( + androidx.lifecycle.LifecycleOwnerKt.getLifecycleScope(getViewLifecycleOwner()), + benefits -> { + updateJsapiStatusHint(JSAPI_HINT_SUCCESS); + return kotlin.Unit.INSTANCE; + }, + err -> { + // 订购中心页保持原行为:权益空 / 网络失败时不主动弹错,保持当前 Fragment 状态。 + return kotlin.Unit.INSTANCE; + }); + } + + @Override + public void onPause() { + super.onPause(); + // 注销 Monitor 状态回调(statusJobBg 继续跑,不暂停) + JsapiOrderMonitor.Companion.get(ContextUtils.getContext()).unregisterCallback(this); + } + + @Override + public void onDestroyView() { + // 注销 Monitor 状态订阅(statusJobBg 不停,等用户回 OrderActivity 再订阅) + JsapiOrderMonitor.Companion.get(ContextUtils.getContext()).unregisterCallback(this); + //lifecycleScope 已自动取消协程;此处显式释放字段并解绑 adapter 回调,避免视图销毁后仍持有旧引用 + if (mJsapiHelper != null) { + // helper 不再持有协程 Job (Task 6 移交给 Monitor),此处直接置空 + mJsapiHelper = null; + } + if (mOrderCenterAdapter != null) { + mOrderCenterAdapter.setOnJsapiItemFocus(null); + mOrderCenterAdapter.setOnXufeiVisibilityListener(null); + mOrderCenterAdapter.setOnSubscription(null); + mOrderCenterAdapter.setMgRecyclerView(null); + mOrderCenterAdapter = null; + } + if (mVerticalRecyclerViewTv != null) { + //解绑 RecyclerView 与 Adapter/LayoutManager 的强引用,允许 Fragment 被 FragmentManager 复用 + mVerticalRecyclerViewTv.setAdapter(null); + mVerticalRecyclerViewTv.setLayoutManager(null); + mVerticalRecyclerViewTv.setInFocusLostListener(null); + } + mOrderLayoutManager = null; + mImgQrcodeTip = null; + nsv = null; + clGoxufei = null; + id_cl_xufei_ok = null; + mClMoneyTip = null; + mTvTemp = null; + mTvMoney = null; + mClUpdateBtn = null; + mTvUpdateInfo = null; + mClUpdate = null; + mImgQrcode = null; + mClTabQrCode = null; + mVerticalRecyclerViewTv = null; + mContext = null; + mOrderViewModel = null; + super.onDestroyView(); } private void initView(View view) { @@ -133,18 +444,53 @@ public class OrderCenterFragment extends Fragment { int spacingInPixel2 = getResources().getDimensionPixelSize(R.dimen.qb_px_1); mVerticalRecyclerViewTv.addItemDecoration(new SpaceItemDecoration(spacingInPixel2, 5)); - mOrderCenterAdapter = new OrderCenterAdapter(mContext, mOrderViewModel, (OrderActivity) getActivity()); - mOrderCenterAdapter.setMgRecyclerView(mVerticalRecyclerViewTv); - mOrderCenterAdapter.setOnSubscription(new OrderCenterAdapter.OnSubscription() { - @Override - public void subscription(int isSubscription) { - requestItemFocus(); - } + mOrderCenterAdapter = new OrderCenterAdapter(mContext, mOrderViewModel); + //Adapter 不再直接持有 Activity:通过 listener 接收订阅状态变化, + //宿主 Activity 当前未暴露 isShowClXufei 入口,订阅保留作为未来扩展点。 + mOrderCenterAdapter.setOnXufeiVisibilityListener(subscription -> { + LogUtils.d("jsapi subscription changed: " + subscription); }); + mOrderCenterAdapter.setMgRecyclerView(mVerticalRecyclerViewTv); + mOrderCenterAdapter.setOnSubscription(isSubscription -> requestItemFocus()); + //RecyclerView 绑定 Adapter 必须在 setPriceItems 触发 notifyDataSetChanged 之前, + //否则在跳过 registerObserver 的家庭版模式下,notifyDataSetChanged 会因无 observer 而不渲染。 + //原代码将 setAdapter 放在 registerObserver 第一行;为兼容 jsapi-only 路径(home)前移到 initView。 + mVerticalRecyclerViewTv.setAdapter(mOrderCenterAdapter); + + //家庭版展示模式:隐藏连续包月/年升级、VipPanel、续费二维码 Tip 等商用专属 UI, + //避免视图占位但无数据导致右侧面板闪烁或空指针 + if (isHomeEdition) { + hideCommercialOnlyViews(); + } + } + + /** + * 隐藏商用专属 UI:连续包升级面板、续费二维码面板、扫码提示图。 + * + *

家庭版用户不走 JetsenxSdk 预订单/升级链路,没有连续包月/年卡概念, + * 只需保留 jsapi H5 扫码订购的二维码面板([mClTabQrCode])与基础金额展示。

+ */ + private void hideCommercialOnlyViews() { + if (mClUpdate != null) { + mClUpdate.setVisibility(View.GONE); + } + if (clGoxufei != null) { + clGoxufei.setVisibility(View.GONE); + } + if (mClUpdateBtn != null) { + mClUpdateBtn.setVisibility(View.GONE); + } + if (id_cl_xufei_ok != null) { + id_cl_xufei_ok.setVisibility(View.GONE); + } + if (mImgQrcodeTip != null) { + mImgQrcodeTip.setVisibility(View.GONE); + } } private void registerObserver() { - mVerticalRecyclerViewTv.setAdapter(mOrderCenterAdapter); + //setAdapter 已前移到 initView(确保家庭版 jsapi-only 路径下也能渲染), + //此处只注册 LiveData 观察者(仅商用版需要) if (null == mOrderViewModel) { return; } @@ -203,61 +549,26 @@ public class OrderCenterFragment extends Fragment { qrcodeTipShow(View.GONE); } }); - //二维码 - mOrderViewModel.getPayQrcodeUrlData.observe(getViewLifecycleOwner(), qrCodeBean -> { - if (null == qrCodeBean) { - return; - } - if (StringUtils.isEmpty(BoxSharedParameters.getInstance().getUserId())) { - LogUtils.d("用户没有登录,显示登录二维码"); - if (mContext != null) { - OrderActivity activity = (OrderActivity) mContext; - activity.hideClLoginQrCode(); - String selectedPolicyCode = mOrderViewModel.mCurrentPolicyCode; - LogUtils.d("显示登录二维码 selectedPolicyCode=" + selectedPolicyCode); - Bitmap bitmap = createQRCode(selectedPolicyCode); - if (null != bitmap) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos); - byte[] byteArray = baos.toByteArray(); - String base64String = Base64.encodeToString(byteArray, Base64.DEFAULT); - mImgQrcode.setImageURI("data:image/png;base64," + base64String); - LogUtils.d("保存支付二维码地址,方便登录成功之后,显示支付二维码"); - payQrCodeUrl = qrCodeBean.getQrcodeUrl(); - payOrderCode = qrCodeBean.getOrderCode(); - } - } - return; - } - - String qrCodeUrl = qrCodeBean.getQrcodeUrl(); - if (!StringUtils.isEmpty(qrCodeUrl)) { - mOrderViewModel.showOrHideUpdateUI(false); - mImgQrcode.setImageURI(qrCodeUrl); - //启动定时器 - PayResultTimerUtil.getInstance().startTimer(mOrderViewModel, qrCodeBean.getOrderCode()); - } - }); //轮询订购结果 - mOrderViewModel.getPayResultCycleData.observe(getViewLifecycleOwner(), getPayResultCycle -> { - if (null == getPayResultCycle) { - LogUtils.d("getPayResultCycle is null, so stop timer"); - PayResultTimerUtil.getInstance().stopTimer(); - } else { - if (null != getPayResultCycle.getRetInfo()) { - int payStatus = getPayResultCycle.getRetInfo().getPayStatus(); - LogUtils.d("getPayResultCycle payStatus=" + payStatus); - if (payStatus == 0) { - ToastUtils.showLongToast("订购成功"); - OrderAmberManager.orderPay(); - PayResultTimerUtil.getInstance().stopTimer(); - } - } else { - PayResultTimerUtil.getInstance().stopTimer(); - } - } - - }); +// mOrderViewModel.getPayResultCycleData.observe(getViewLifecycleOwner(), getPayResultCycle -> { +// if (null == getPayResultCycle) { +// LogUtils.d("getPayResultCycle is null, so stop timer"); +// PayResultTimerUtil.getInstance().stopTimer(); +// } else { +// if (null != getPayResultCycle.getRetInfo()) { +// int payStatus = getPayResultCycle.getRetInfo().getPayStatus(); +// LogUtils.d("getPayResultCycle payStatus=" + payStatus); +// if (payStatus == 0) { +// ToastUtils.showLongToast("订购成功"); +// OrderAmberManager.orderPay(); +// PayResultTimerUtil.getInstance().stopTimer(); +// } +// } else { +// PayResultTimerUtil.getInstance().stopTimer(); +// } +// } +// +// }); //待升级 mOrderViewModel.rowsBeanData.observe(getViewLifecycleOwner(), rowsBean -> { try { @@ -318,39 +629,23 @@ public class OrderCenterFragment extends Fragment { public void requestItemFocus() { if (isFirstShowOrderCenter) { + // 首次进入订单中心后不再处理,避免遮挡后续流程主动设置的焦点 + return; + } + if (mVerticalRecyclerViewTv == null + || mVerticalRecyclerViewTv.getAdapter() == null + || mVerticalRecyclerViewTv.getAdapter().getItemCount() <= 0) { return; } isFirstShowOrderCenter = true; - try { - DispatchQueue.main.after(100, () -> { - String policyCode = ((OrderActivity) getActivity()).getPolicyCode(); - int requestFocusIndex = 0; - if (!TextUtils.isEmpty(policyCode)) { - for (int i = 0; i < mOrderCenterAdapter.getPricingBeanList().size(); i++) { - if (mOrderCenterAdapter.getPricingBeanList().get(i).getPolicyCode().equals(policyCode)) { - requestFocusIndex = i; - break; - } - } - LogUtils.d("requestFocusIndex==" + requestFocusIndex+" requestFocusSize="+mOrderCenterAdapter.getPricingBeanList().size()); - } - View focusView = mVerticalRecyclerViewTv.getLayoutManager().findViewByPosition(requestFocusIndex); - if (focusView != null) { - focusView.requestFocus(); - } - if (requestFocusIndex!=0){ - mVerticalRecyclerViewTv.postDelayed(new Runnable() { - @Override - public void run() { - nsv.smoothScrollBy(0,100); - LogUtils.d("requestFocusIndex 滚一段"); - } - },100); - } -// ((OrderActivity)(requireActivity())).requestFirstFocusEnd(); - }); - } catch (Exception e) { - } + // 默认选中列表第一项:post 等 RecyclerView 完成首帧布局后再定位焦点, + // 避免 itemView 未 attach 时 findViewByPosition 返回 null + mVerticalRecyclerViewTv.post(() -> { + View firstView = mVerticalRecyclerViewTv.getLayoutManager().findViewByPosition(0); + if (firstView != null) { + firstView.requestFocus(); + } + }); } private void moneyTipShow(int visible) { @@ -414,76 +709,10 @@ public class OrderCenterFragment extends Fragment { } private void initListener() { - id_cl_xufei_ok.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - ((OrderActivity) getActivity()).requestFocusXufei(); - } - }); - mVerticalRecyclerViewTv.setInFocusGainListener(new VerticalRecyclerViewTv.IonFocusGainListener() { - @Override - public void onGainLost(View child, View focused) { - LogUtils.d("onFocusChange:=="); - ((OrderActivity) requireActivity()).setDuihuanFoucs(); - } - }); - - mVerticalRecyclerViewTv.addOnChildAttachStateChangeListener(new RecyclerView.OnChildAttachStateChangeListener() { - @Override - public void onChildViewAttachedToWindow(View view) { - LogUtils.d("onChildViewAttachedToWindow:==" + view); - if (mVerticalRecyclerViewTv.getChildAdapterPosition(view) == 0) { - - if (requireActivity() instanceof OrderActivity) { - OrderActivity orderActivity = ((OrderActivity) requireActivity()); - boolean firstFocus = orderActivity.isFirstFocus(); - LogUtils.d("onChildViewAttachedToWindow:== firstFocus=" + firstFocus); - if (!firstFocus) { - view.requestFocus(); - orderActivity.setFirstFocus(true); - } - } - } - } - - @Override - public void onChildViewDetachedFromWindow(View view) { - // 不需要实现 - } - }); - mVerticalRecyclerViewTv.setInFocusLostListener(new VerticalRecyclerViewTv.IonFocusLostListener() { @Override public View onFocusLost(View lastFocusView, int direction) { LogUtils.d("Order Center list lost focus lastFocusView=" + lastFocusView + " direction=" + direction); - if (direction == View.FOCUS_UP) { - mOrderViewModel.setDirection(View.FOCUS_UP); - LogUtils.d("go ro dinggouzhongxin"); - if (requireActivity() instanceof OrderActivity) { - OrderActivity orderActivity = ((OrderActivity) requireActivity()); - orderActivity.requestFocusOrderCenter(); - } - } else if (direction == View.FOCUS_RIGHT) { - if (mClTabQrCode.getVisibility() == View.VISIBLE && mClUpdate.getVisibility() == View.VISIBLE) { - LogUtils.d("go to update btn request"); - mOrderViewModel.setDirection(View.FOCUS_RIGHT); - if (requireActivity() instanceof OrderActivity) { - OrderActivity orderActivity = ((OrderActivity) requireActivity()); - orderActivity.setOrderCenterFocusable(false); - } - mClUpdateBtn.requestFocus(); - return mClUpdateBtn; - } else if (clGoxufei.getVisibility() == View.VISIBLE) { - LogUtils.d("go to update btn request"); - mOrderViewModel.setDirection(View.FOCUS_RIGHT); - if (requireActivity() instanceof OrderActivity) { - OrderActivity orderActivity = ((OrderActivity) requireActivity()); - orderActivity.setOrderCenterFocusable(false); - } - id_cl_xufei_ok.requestFocus(); - return id_cl_xufei_ok; - } - } return null; } }); @@ -497,6 +726,120 @@ public class OrderCenterFragment extends Fragment { }); } + // ============== JsapiOrderMonitorCallback 实现 ============== + /** status 回调 msg 去重,避免每 5s 弹一次相同 Toast */ + private String lastStatusMsg = null; -} \ No newline at end of file + /** + * status 状态更新回调(Monitor → Fragment)。 + * + *

阶段 3/4 状态机 UI:根据 status 切换右侧二维码面板持久提示。 + * Toast 用 lastStatusMsg 去重,避免每 5s 弹一次。

+ */ + @Override + public void onStatusUpdate(String status, String msg) { + LogUtils.d(TAG, "jsapi monitor onStatusUpdate status=" + status + " msg=" + msg); + + new Handler(Looper.getMainLooper()).post( + new Runnable() { + @Override + public void run() { + // 状态机 UI(spec §6.1 #14) +// if ("PAID".equals(status)) { +// updateJsapiStatusHint(JSAPI_HINT_PAID); +// } else if ("ACTIVATING".equals(status)) { +// updateJsapiStatusHint(JSAPI_HINT_ACTIVATING); +// } else if ("ACTIVE".equals(status)) { +// updateJsapiStatusHint(JSAPI_HINT_ACTIVE); +// } + // Toast 幂等去重(避免每 5s 弹一次相同 msg) + if (msg != null && !msg.isEmpty() && !msg.equals(lastStatusMsg)) { + ToastUtils.showLongToast(msg); + lastStatusMsg = msg; + } + } + } + ); + } + + /** + * status 命中 CANCELLED / TIMEOUT_CANCELED 回调。 + * + *

重建订单:helper.createOrder(currentCardType) → Monitor.arm(newOrderNo) → startStatusJobBg 续接。 + * 注意:Fragment 不在前台时(用户退出 OrderActivity)此回调不会被触发——Monitor 直接走 recreate() + * 内部 ApiRequesters.app() 自愈(spec §5.3)。

+ */ + @Override + public void onOrderCancelled(String reason, boolean expired) { + LogUtils.d(TAG, "jsapi monitor onOrderCancelled reason=" + reason + " expired=" + expired); + // 重建订单:用当前选中的 cardType,避免空 cardType 触发后端 CARD_TYPE_DISABLED + if (mCurrentCardType == null) { + // 兜底:从 Monitor 取 cachedCardType(armedCardType 为 internal getter) + // 此处简化:用 monitor.armedCardType 但需 Monitor 暴露 getter + LogUtils.w(TAG, "onOrderCancelled: mCurrentCardType is null, cannot recreate"); + return; + } + final String cardType = mCurrentCardType; + mJsapiHelper.createOrder(cardType, (dto, code, msg) -> { + if (dto == null || TextUtils.isEmpty(dto.getOrderNo())) { + LogUtils.e(TAG, "recreate after cancel failed code=" + code + " msg=" + msg); + ToastUtils.showLongToast(TextUtils.isEmpty(msg) ? "订单重建失败,请稍后重试" : msg); + return; + } + LogUtils.d(TAG, "recreate success newOrderNo=" + dto.getOrderNo()); + // 更新 UI QR + 同步 Monitor + String qrUrl = mJsapiHelper.composeQrUrl(dto.getH5Url(), null /*pricingId 由 switchItem 兜底*/); + if (qrUrl != null) { + showJsapiQrCode(qrUrl); + } + JsapiOrderMonitor monitor = JsapiOrderMonitor.Companion.get(ContextUtils.getContext()); + monitor.arm(dto.getOrderNo(), cardType, null); + // startStatusJobBg() 幂等:orderNo 变化时会自动重启 + monitor.startStatusJobBg(); + updateJsapiStatusHint(JSAPI_HINT_SCANNING); + }); + } + + /** + * status 命中 ACTIVE —— 启动 check 卡密轮询。 + * + *

Monitor 仅暴露 startCheckJob,不依赖 Helper。

+ */ + @Override + public void onOrderActive() { + LogUtils.d(TAG, "jsapi monitor onOrderActive, starting checkJob"); + JsapiOrderMonitor.Companion.get(ContextUtils.getContext()).startCheckJob(); + } + + /** check 命中 10200 / thirdCardNo 下发 —— 卡密已下发,权益已生效 */ + @Override + public void onCheckActivated(String cardNo) { + LogUtils.d(TAG, "jsapi monitor onCheckActivated"); + // Monitor 内部已调 JetsenxSdkUtil.cacheReusableUid(cardNo),此处仅 UI + JetsenxSdkUtil.cacheReusableUid(cardNo); // 幂等:二次调用安全 + ToastUtils.showLongToast("会员权益已生效"); + // 退出当前订购页(OrderActivity),回到进入前的页面 + // 注意:必须在 Toast 之后、视图仍附着时调;视图销毁后 getActivity() 可能为 null + Activity activity = getActivity(); + if (activity != null && !activity.isFinishing()) { + activity.finish(); + } + } + + /** check 30s/10 次超时 */ + @Override + public void onCheckTimeout() { + LogUtils.d(TAG, "jsapi monitor onCheckTimeout"); +// updateJsapiStatusHint(JSAPI_HINT_CHECK_FAILED); + ToastUtils.showLongToast(JSAPI_HINT_CHECK_FAILED); + } + + /** status 终态 FAILED */ + @Override + public void onOrderFailed(String reason) { + LogUtils.d(TAG, "jsapi monitor onOrderFailed reason=" + reason); +// updateJsapiStatusHint(TextUtils.isEmpty(reason) ? "订单异常,请联系客服" : reason); + ToastUtils.showLongToast(TextUtils.isEmpty(reason) ? "订单异常,请联系客服" : reason); + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderDuihuanFragment.java b/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderDuihuanFragment.java old mode 100755 new mode 100644 index a8a4248..3ffa144 --- a/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderDuihuanFragment.java +++ b/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderDuihuanFragment.java @@ -238,35 +238,6 @@ public class OrderDuihuanFragment extends Fragment { @Override public View onFocusLost(View lastFocusView, int direction) { LogUtils.d("Order Center list lost focus lastFocusView=" + lastFocusView + " direction=" + direction); - if (direction == View.FOCUS_RIGHT) { - if (mClTabVip.getVisibility() == View.VISIBLE) { - LogUtils.d("go ro vip a card"); - mOrderViewModel.setDirection(View.FOCUS_RIGHT); - View focusView = mRvKeyboard.getLayoutManager().findViewByPosition(0).findViewById(R.id.keyboardTv); - if (focusView != null) { - LogUtils.d("go ro vip a card focusView=" + focusView); - if (requireActivity() instanceof OrderActivity) { - OrderActivity orderActivity = ((OrderActivity) requireActivity()); - orderActivity.setOrderCenterFocusable(false); - } - focusView.requestFocus(); - return focusView; - } - - } else if (mClTabQrCode.getVisibility() == View.VISIBLE && mClUpdate.getVisibility() == View.VISIBLE) { - LogUtils.d("go to update btn request"); - mOrderViewModel.setDirection(View.FOCUS_RIGHT); - if (requireActivity() instanceof OrderActivity) { - OrderActivity orderActivity = ((OrderActivity) requireActivity()); - orderActivity.setOrderCenterFocusable(false); - } - mClUpdateBtn.requestFocus(); - return mClUpdateBtn; - } - } else if (direction == View.FOCUS_DOWN) { - mVerticalRecyclerViewTv.requestFocus(); - return mVerticalRecyclerViewTv; - } return null; } }); diff --git a/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderListFragment.java b/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderListFragment.java old mode 100755 new mode 100644 index 3356e3f..baff84c --- a/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderListFragment.java +++ b/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderListFragment.java @@ -1,75 +1,36 @@ package com.pukka.jiecheng.order.fragment; -import static com.pukka.jiecheng.login.LoginActivity.header; -import static com.pukka.jiecheng.utils.JetsenxSdkUtil.ACCESSER; -import static com.pukka.jiecheng.utils.JetsenxSdkUtil.SECRET; -import static com.pukka.libcore.base.BaseConst.agentVendorCode; -import static com.pukka.libcore.base.BaseConst.serviceComboCode; -import static com.pukka.libcore.base.BaseConst.serviceGroupCode; - -import android.annotation.SuppressLint; -import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.text.TextUtils; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.google.gson.Gson; -import com.jetsenxsdk.api.JetsenxSdk; -import com.jetsenxsdk.callback.JetsenxChangeOrderCallback; -import com.migu.libnetwork.HttpRequestCallback; -import com.migu.libnetwork.HttpUtils; -import com.pukka.jiecheng.JcConfig; import com.pukka.jiecheng.R; -import com.pukka.jiecheng.base.SystemConfigManager; -import com.pukka.jiecheng.order.OrderActivity; -import com.pukka.jiecheng.order.OrderAmberManager; +import com.pukka.jiecheng.network.dto.DeviceBenefitItemDto; +import com.pukka.jiecheng.network.infra.ApiError; import com.pukka.jiecheng.order.adapter.OrderListAdapter; -import com.pukka.jiecheng.order.bean.OrderListBean; -import com.pukka.jiecheng.order.dialog.OrderCancelDialog; -import com.pukka.jiecheng.order.dialog.OrderDialog; -import com.pukka.jiecheng.utils.PaginationUtil; -import com.pukka.jiecheng.utils.ToastUtils; -import com.pukka.jiecheng.widget.MgRecyclerView; -import com.pukka.libcore.base.BaseConst; -import com.pukka.libcore.boxShared.BoxSharedParameters; -import com.pukka.libcore.threading.DispatchQueue; -import com.pukka.libcore.utils.DateUtils; import com.pukka.libcore.utils.LogUtils; -import com.pukka.libcore.utils.SharedPreferenceUtils; -import com.pukka.libcore.utils.StringUtils; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.TimeUnit; public class OrderListFragment extends Fragment { private static final String TAG = "OrderListFragment"; private RecyclerView rvOrder; - private Context context; private OrderListAdapter adapter; private TextView noDataTv; - private List list = new ArrayList<>();// 完整数据源 + private List list = new ArrayList<>();// 当前显示数据源 private boolean isTuiDingBtn = false; - private OrderActivity orderActivity; - //我的订购页面 分页 - private ConstraintLayout mClPage; - private TextView mTvPageUp,mTvPageDown,mTvTotalPages; - // 创建分页对象,每页显示5条 - PaginationUtil pagination; public static OrderListFragment newInstance() { return new OrderListFragment(); @@ -85,7 +46,6 @@ public class OrderListFragment extends Fragment { super.onViewCreated(view, savedInstanceState); } - @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -103,334 +63,76 @@ public class OrderListFragment extends Fragment { } private void initView(View view) { - orderActivity = (OrderActivity) getActivity(); - context = getContext(); - mClPage = view.findViewById(R.id.id_cl_page); - mTvPageUp = view.findViewById(R.id.id_tv_page_up); - mTvPageDown = view.findViewById(R.id.id_tv_page_down); - mTvTotalPages = view.findViewById(R.id.id_tv_sum_page); + Context context = getContext(); noDataTv = view.findViewById(R.id.noDataTv); rvOrder = view.findViewById(R.id.rvOrder); - initListener(); rvOrder.setLayoutManager(new LinearLayoutManager(context)); adapter = new OrderListAdapter(list, context); rvOrder.setAdapter(adapter); rvOrder.setFocusable(false); - //rvOrder.setFocusUpView(false); - adapter.setOnItemClickListener(new OrderListAdapter.OnItemClickListener() { - @Override - public void onItemClick(int pos, boolean isXufei) { - if (isXufei) { - ((OrderActivity) getActivity()).requestFocusXufei(); - return; - } - -// boolean canCancelOrder = getCancelOrder(); -// if (!canCancelOrder) { -// LogUtils.d("====非周期内首次退订===="); -// new OrderCancelDialog((Activity) context).show(); -// return; -// } - - new OrderDialog((Activity) context, new OrderDialog.OnTipsCallBack() { - @Override - public void onLeft() { - LogUtils.d(TAG, "退订 changeOrderDetail:" + list.get(pos).getOrderCode()); - changeOrderDetail(pos, list.get(pos).getOrderCode()); - } - - @Override - public void onRight() { - - } - }).show(); - } - - @Override - public void onTuidingFocused() { - orderActivity.mClXufeiSetFocusableNot(); - } - }); - } - - private void initListener(){ - mTvPageDown.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if(pagination.hasNextPage()) { - adapter.setList(pagination.nextPage()); - PaginationUtil.PageInfo info = pagination.getPageInfo(); - mTvTotalPages.setText(info.getCurrentPage() + "/" + info.getTotalPages()); - }else{ - ToastUtils.showLongToast("已经是最后一页了"); - } - } - }); - mTvPageUp.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if(pagination.hasPreviousPage()) { - adapter.setList(pagination.previousPage()); - PaginationUtil.PageInfo info = pagination.getPageInfo(); - mTvTotalPages.setText(info.getCurrentPage() + "/" + info.getTotalPages()); - }else{ - ToastUtils.showLongToast("已经是第一页了"); - } - } - }); - - mTvPageDown.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean b) { - if(b){ - mTvPageDown.setTextColor(view.getContext().getResources().getColor(R.color.order_list_focus_text_color)); - }else{ - mTvPageDown.setTextColor(view.getContext().getResources().getColor(R.color.white50)); - } - } - }); - mTvPageUp.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View view, boolean b) { - if(b){ - mTvPageUp.setTextColor(view.getContext().getResources().getColor(R.color.order_list_focus_text_color)); - }else{ - mTvPageUp.setTextColor(view.getContext().getResources().getColor(R.color.white50)); - } - } - }); + // 临时关闭退订按钮业务:不再向 Adapter 注册点击监听,行内 tvtuiding 由 Adapter 内部硬隐藏。 + // 如需回滚,把 OrderListAdapter.onBindViewHolder 中 tvtuiding.setVisibility 改回原逻辑, + // 并恢复此处 adapter.setOnItemClickListener(...) + OrderDialog + changeOrderDetail 的旧实现。 } /** * 加载数据 */ public void loadData() { - requestOrderList(); + fetchBenefits(); } /** - * 获取订购内容 - */ - public void requestOrderList() { - String host = JcConfig.QUERAORDERLIST; - - String memberCode = BaseConst.getMemberCode(BoxSharedParameters.getInstance().getToken()); - LogUtils.d("memberCode:" + memberCode); - if (TextUtils.isEmpty(memberCode)) { - noDataTv.setVisibility(View.VISIBLE); - orderActivity.mClMyOrderSetNextFocusDownId(false); - return; - } - String url = host + "agentVendorCode=" + agentVendorCode + "&serviceComboCode=" + serviceComboCode + "&serviceGroupCode=" + serviceGroupCode + "&memberCode=" + memberCode + "&pageLimit=" + 30 + "&pageNum=" + 1; - LogUtils.d("requestOrderList host:" + url); - LogUtils.d("requestOrderList token:" + BoxSharedParameters.getInstance().getToken()); - HttpUtils.getInstance().mHttpPostOrderList(url, BoxSharedParameters.getInstance().getToken(), header, new HttpRequestCallback() { - @Override - public void onRequestNetFail(int type) { - - } - - @Override - public void onRequestSuccess(String result, int type) { - LogUtils.d("requestOrderList onRequestSuccess:" + result); - try { - OrderListBean orderListBean = new Gson().fromJson(result, OrderListBean.class); - if (orderListBean.getRetCode().equals("00000000")) { - list.clear(); - for (OrderListBean.RetInfoBean.RowsBean bean : orderListBean.getRetInfo().getRows() - ) { -// if (bean.getStatus() == 2) { -// //订单状态:1-已订购、2-退订、6-升级年 -// continue; -// } - list.add(bean); - } - - if(null != list && list.size() > 0) { - // 创建分页对象,每页显示5条 - pagination = new PaginationUtil(list, 5); - adapter.setList(pagination.getCurrentPageData()); - boolean isHaveTuidingBtn = isHaveTuiDingBtn(pagination.getCurrentPageData()); - LogUtils.d("第一页面是否有退订或已退订按钮?=" + isHaveTuidingBtn); - PaginationUtil.PageInfo info = pagination.getPageInfo(); - if (info.getTotalPages() > 1) { - mClPage.setVisibility(View.VISIBLE); - mTvTotalPages.setText(info.getCurrentPage() + "/" + info.getTotalPages()); - } - LogUtils.d("list=" + list.size()); - LogUtils.d("我的订购列表:" + list.toString()); - if (isHaveTuidingBtn || info.getTotalPages() > 1) { - //我的订购按钮可以去订购列表中退订按钮或者下一页按钮上 - orderActivity.mClMyOrderSetNextFocusDownId(true); - } else { - //我的订购按钮回到我的订购按钮上 - orderActivity.mClMyOrderSetNextFocusDownId(false); - } - } - - } else { - Toast.makeText(context, orderListBean.getRetMsg(), Toast.LENGTH_SHORT).show(); - } - changeXufei(list, (OrderActivity) getActivity()); - noDataTv.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE); - if (noDataTv.getVisibility() == View.VISIBLE) { - orderActivity.mClMyOrderSetNextFocusDownId(false); - } - } catch (Exception e) { - Toast.makeText(context, e.toString(), Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void onRequestFail(String value, String failCode, int type) { - - } - }); - } - /** - * 是否展示自动续费TAB - * */ - public static void changeXufei(List list,OrderActivity activity) { - if (list.isEmpty()) { - try { - activity.isShowClXufei(0); - } catch (Exception e) { - } - return; - } - int isSubscription = 0; - for (OrderListBean.RetInfoBean.RowsBean bean : list - ) { - //已定连续包年 - if (bean.getStatus() != 2 - && bean.getPolicyType() == 2 - && bean.getChargeType2() == 5) { -// if (bean.isSupportRefund()){ -// -// } - isSubscription = 2; - break; - } else if (bean.getStatus() != 2 - && bean.getPolicyType() == 2 - && bean.getChargeType2() == 2) { -// if (bean.isSupportRefund()){ -// isSubscription = 1; -// } - isSubscription = 1; - break; - } - } - try { - activity.isShowClXufei(isSubscription); - } catch (Exception e) { - } - } - - /** - * 是否有退订按钮 - */ - public boolean isHaveTuiDingBtn(List list) { - boolean isTuiDingBtn = false; - if (list.isEmpty()) { - return false; - } - for (OrderListBean.RetInfoBean.RowsBean bean : list - ) { - if (bean.isSupportRefund() || bean.getStatus() == 2) { - LogUtils.d("我的订购列表中包含退订按钮或已退订按钮"); - isTuiDingBtn = true; - break; - } - } - return isTuiDingBtn; - } - /** - * 退订 - * payAccountType 10-未来电视 11-橘视听未来 - * 目前业务逻辑 - * 1、申请退款仅支持购买会员订单,暂不支持卡密订单申请退款。 - * 2、可申请退款的订单,自订购起7天内可以申请退款,超出7天后不支持申请。 - * 3、申请退款完成后,再次申请退款需要间隔365天。 - * 这里的测试订单 可以单独把订单给小强 进行退订 + * 拉取当前设备 ACTIVE 权益(接口 /api/jst/benefit/device/me/benefits)。 * + *

onSuccess 拿到 List 后直接交给 Adapter 渲染;onError 时 Toast 提示并显示空态。

*/ - public void changeOrderDetail(int pos, String mOrderCode) { - int actionType = 1; - //String payAccountType = "11"; - //9月18日 目前的新的版本都是用的支付到未来电视,应该对应的是 payAccountType=10-未来电视 - String payAccountType = "10"; - String refundReason = "退款"; - JetsenxSdk.changeOrderDetail(ACCESSER, SECRET, mOrderCode, actionType, payAccountType - , refundReason, new JetsenxChangeOrderCallback() { - @SuppressLint("SetTextI18n") - @Override - public void onFail(String code, String message) { - Log.d(TAG, "changeOrderDetail -- code : " + code + " message : " + message); - } - - @SuppressLint("SetTextI18n") - @Override - public void onChangeOrderDetail(String desc, String orderCode, int payFee) { - Log.d(TAG, "changeOrderDetail -- desc : " + desc - + " orderCode : " + orderCode + " payFee : " + payFee); - Toast.makeText(context, desc, Toast.LENGTH_SHORT).show(); - try { - String product_id = list.get(pos).getPolicyCode(); - String product_name = list.get(pos).getPolicyName(); - String product_type = list.get(pos).getChargeType1() == 1 ? "单点" : "包周期"; - String order_price = list.get(pos).getPayFee() / 100.00 + ""; - String order_id_f = list.get(pos).getOrderCode(); - OrderAmberManager.orderFefund(product_id, product_name, product_type, order_price, order_id_f, true); - //保存退订时间 规则:电话号码+@@@+当前时间yyyy-MM-dd HH:mm:ss -// String temp = BoxSharedParameters.getInstance().getUserId() + "@@@" + DateUtils.getStringNowDate(); -// LogUtils.d("cancel_time=" + temp); -// SharedPreferenceUtils.put("cancel_time", temp); -// list.remove(pos); -// adapter.setList(list); - LogUtils.d("list=", "requestOrderList() 00"); - DispatchQueue.main.after(5000, () -> { - LogUtils.d("list=", "requestOrderList() 11"); - requestOrderList(); - }); - ((OrderActivity) getActivity()).requestFocusOrderList(); - } catch (Exception ex) { - ex.printStackTrace(); - } - } + private void fetchBenefits() { + OrderNetworkBridge.INSTANCE.fetchActiveBenefits( + androidx.lifecycle.LifecycleOwnerKt.getLifecycleScope(getViewLifecycleOwner()), + benefits -> { + onBenefitsLoaded(benefits); + return kotlin.Unit.INSTANCE; + }, + err -> { + onBenefitsError(err); + return kotlin.Unit.INSTANCE; }); } + private void onBenefitsLoaded(List benefits) { + list.clear(); + list.addAll(benefits); + noDataTv.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE); + adapter.setList(list); + LogUtils.d(TAG, "fetchActiveBenefits success, rows=" + list.size()); + } + + private void onBenefitsError(ApiError err) { + String msg = err == null || TextUtils.isEmpty(err.getMessage()) + ? "获取订单失败" + : "获取订单失败:" + err.getMessage(); + LogUtils.d(TAG, "fetchActiveBenefits fail: " + msg); + //ToastUtils.showShortToast(msg); + list.clear(); + noDataTv.setVisibility(View.VISIBLE); + adapter.setList(list); + } + /** - * 是否为周期内首次退订 + * 是否包含退订按钮 — 已退订业务,固定返回 false 占位。 + * 保留以备回滚恢复 OrderListBean 旧数据源后启用。 */ -// private boolean getCancelOrder() { -// boolean canCancelOrder = true; -// try { -// //上次取消时间 -// String last_cancel_time = SharedPreferenceUtils.getString("cancel_time", ""); -// LogUtils.d("last_cancel_time=" + last_cancel_time); -// if (!StringUtils.isEmpty(last_cancel_time) && last_cancel_time.contains("@@@") && last_cancel_time.contains(BoxSharedParameters.getInstance().getUserId())) { -// String[] strings = last_cancel_time.split("@@@"); -// if (strings.length == 2) { -// String lastCancelTime2 = last_cancel_time.split("@@@")[1]; -// //用户发生退订时需判断是否为周期内首次退订(当前时间-上次退订时间>退订周期) -// int tempSecond = DateUtils.getSecondCompareStringDate(lastCancelTime2, DateUtils.getStringNowDate()); -// -// int cancelOrderPeriod = SystemConfigManager.getInstance().getCancelOrderPeriod(); -// //一天多少秒 -// long secondsCancelOrderPeriod = TimeUnit.DAYS.toSeconds(cancelOrderPeriod); -// LogUtils.d("tempSecond===" + tempSecond + " secondsCancelOrderPeriod=" + secondsCancelOrderPeriod + " "); -// LogUtils.d("cancelOrderPeriod===" + cancelOrderPeriod); -// if (tempSecond - secondsCancelOrderPeriod > 0) { -// canCancelOrder = true; -// } else { -// canCancelOrder = false; -// } -// } -// } -// } catch (Exception ex) { -// ex.printStackTrace(); -// } -// return canCancelOrder; -// } -} + public boolean isHaveTuiDingBtn(List list) { + return false; + } + + /** + * 退订业务已临时关闭 — 方法体保留为占位,后续回滚时按原 JetsenxSdk.changeOrderDetail 链路恢复。 + */ + public void changeOrderDetail(int pos, String mOrderCode) { + // TODO 恢复退订:调用 JetsenxSdk.changeOrderDetail + OrderAmberManager.orderFefund, + // 退订成功后 DispatchQueue.main.after(5000) 调 fetchBenefits() 刷新列表。 + LogUtils.d(TAG, "changeOrderDetail 已禁用,pos=" + pos + " orderCode=" + mOrderCode); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderNetworkBridge.kt b/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderNetworkBridge.kt new file mode 100644 index 0000000..9753a63 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderNetworkBridge.kt @@ -0,0 +1,65 @@ +package com.pukka.jiecheng.order.fragment + +import android.util.Log +import com.pukka.jiecheng.network.ApiRequesters +import com.pukka.jiecheng.network.api.AsApi +import com.pukka.jiecheng.network.dto.DeviceBenefitItemDto +import com.pukka.jiecheng.network.from +import com.pukka.jiecheng.network.infra.ApiError +import kotlinx.coroutines.CoroutineScope + +/** + * 给 Java Fragment 调用的 Kotlin DSL 薄封装。 + * + *

背景:Project Kotlin DSL (ApiRequesters.ui(...).from(AsApi::class).action{...}) 的几个 + * 关键节点都对 Java 不可达 —— ApiRequesters 是 Kotlin `object`(Java 必须走 INSTANCE), + * `from` 是顶层扩展函数(在 JVM 字节码里落在 ApiRequestersKt),`action` 的 lambda 参数类型是 + * `suspend (API) -> NetResponse`(Java 调用必须显式传 Continuation,无法用 1-arg lambda)。 + * 项目里所有现有 DSL 调用方都是 Kotlin 文件;本桥让 Java Fragment 也能复用同套 DSL。

+ * + *

使用约束:scope 必须传 Fragment.getViewLifecycleOwner() 的 lifecycleScope, + * 与 [ApiRequesters.ui] 的契约一致(界面 destroy 时自动取消在途请求)。

+ */ +object OrderNetworkBridge { + + private const val TAG = "OrderNetworkBridge" + + /** + * 调 /me/benefits 拉当前权益(spec §5.1 ④,端点原 active-benefits 已废弃)。 + * + *

在权益非空时回调 onSuccess 并把权益列表透出(供「我的订购」页渲染); + * 权益空 / 请求失败时回调 onError,不弹任何提示(由调用方决定是否提示)。

+ * + *

注意:`NetResponse.code` 的成功值约定是 200(项目其它接口),但本桥判 `0` — + * 与 /me/benefits 端点的服务端约定保持一致,避免破坏既有调用方语义。

+ * + * @param scope 传入 Fragment.viewLifecycleOwner.lifecycleScope(destroy 时自动取消) + * @param onSuccess 权益 ACTIVE 时回调,参数为已解析的权益列表(可能为空 list) + * @param onError 权益空 / 网络失败时回调(默认 no-op);参数为结构化 ApiError + */ + fun fetchActiveBenefits( + scope: CoroutineScope, + onSuccess: (List) -> Unit, + onError: (ApiError) -> Unit = {} + ) { + ApiRequesters.ui(scope) + .from() + .action { api: AsApi -> api.deviceBenefits() } + .onSuccess { resp -> + val benefits = resp.data?.benefits.orEmpty() + if (resp.code == 0) { + onSuccess(benefits) + } else { + onError( + ApiError( + kind = ApiError.Kind.BUSINESS, + message = "me/benefits code=${resp.code} msg=${resp.msg}", + businessCode = resp.code + ) + ) + } + } + .onFailure { err -> onError(err) } + .launch() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderXufeiFragment.java b/app/src/main/java/com/pukka/jiecheng/order/fragment/OrderXufeiFragment.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/order/viewmodel/OrderViewModel.java b/app/src/main/java/com/pukka/jiecheng/order/viewmodel/OrderViewModel.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/permission/AgreementStoragePermissionGateway.java b/app/src/main/java/com/pukka/jiecheng/permission/AgreementStoragePermissionGateway.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/request/AsApiErrorCode.java b/app/src/main/java/com/pukka/jiecheng/request/AsApiErrorCode.java old mode 100755 new mode 100644 index da3706a..b88d63d --- a/app/src/main/java/com/pukka/jiecheng/request/AsApiErrorCode.java +++ b/app/src/main/java/com/pukka/jiecheng/request/AsApiErrorCode.java @@ -10,10 +10,12 @@ public enum AsApiErrorCode { ERROR_SIGN_MISSING(1001, "token凭证为空"), ERROR_MEMBER_CODE_EMPTY(1002, "会员码为空"), ERROR_REQUEST_PARAM(1003, "请求参数异常"), - ERROR_NETWORK(1004, "网络异常,请稍后重试"), + // 2026-09-21 语义对换:1004 由"网络异常"改为"解析异常",与 1007 对调。 + // 全局约定:1007=网络错误(无网络/无法访问服务器),1004=解析异常。 + ERROR_RESULT_PARSE(1004, "结果解析失败"), ERROR_SERVER(1005, "服务器错误"), ERROR_RESULT_EMPTY(1006, "结果为空"), - ERROR_RESULT_PARSE(1007, "结果解析失败"), + ERROR_NETWORK(1007, "网络异常,请稍后重试"), ERROR_TIMEOUT(1008, "操作超时"), ERROR_ACTIVATE_CODE_EMPTY(1009, "激活码为空"), EEROR_ACTIVE_FAIL(1010, "激活失败"), diff --git a/app/src/main/java/com/pukka/jiecheng/request/AsApiErrorMapper.kt b/app/src/main/java/com/pukka/jiecheng/request/AsApiErrorMapper.kt new file mode 100644 index 0000000..3810e3a --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/request/AsApiErrorMapper.kt @@ -0,0 +1,53 @@ +package com.pukka.jiecheng.request + +import com.pukka.jiecheng.network.infra.ApiError +import com.pukka.jiecheng.network.infra.toApiError + +/** + * AS 接口错误码统一映射单点(2026-09-21 约定): + * - 1007 = 网络错误(IOException / ApiError.Kind.NETWORK),文案按 cause 细分: + * UnknownHostException → 无网络;ConnectException → 无法访问服务器; + * SocketTimeoutException → 连接超时;其余 → 通用网络异常 + * - 1004 = 解析异常(Kind.PARSE) + * - 1005 = 服务器错误(Kind.HTTP) + * - BUSINESS(后端业务码)原样透传 code + msg + * - UNKNOWN → 1099(替代旧代码里各站点混用的 1007/1010/1005 fallback) + * + * 所有 AsApiRequester 的 catch / Failure 分支必须经本 mapper 收敛, + * 禁止再散落裸数字 fallback。 + */ +object AsApiErrorMapper { + + const val MSG_NO_NETWORK = "无网络连接,请检查网络后重试" + const val MSG_SERVER_UNREACHABLE = "无法访问服务器,请稍后重试" + const val MSG_SERVER_TIMEOUT = "连接服务器超时,请稍后重试" + const val MSG_NETWORK_GENERIC = "网络异常,请稍后重试" + const val MSG_PARSE = "数据解析异常,请稍后重试" + const val MSG_HTTP_SERVER = "服务器异常,请稍后重试" + + /** Throwable → (errorCode, 友好文案)。Java 回调层的唯一入口。 */ + fun mapThrowableToAsFail(e: Throwable): Pair = + mapApiErrorToAsFail(e.toApiError()) + + /** ApiError → (errorCode, 友好文案)。按 kind 分派,BUSINESS 透传。 */ + fun mapApiErrorToAsFail(err: ApiError): Pair = when (err.kind) { + ApiError.Kind.NETWORK -> + AsApiErrorCode.ERROR_NETWORK.getCode() to friendlyNetworkMessage(err.cause) + ApiError.Kind.PARSE -> + AsApiErrorCode.ERROR_RESULT_PARSE.getCode() to MSG_PARSE + ApiError.Kind.HTTP -> + AsApiErrorCode.ERROR_SERVER.getCode() to MSG_HTTP_SERVER + ApiError.Kind.BUSINESS -> + (err.businessCode ?: AsApiErrorCode.ERROR_UNKNOWN.getCode()) to err.message + ApiError.Kind.UNKNOWN -> + AsApiErrorCode.ERROR_UNKNOWN.getCode() to err.message + } + + /** 按 IOException 子类给出可区分的网络文案(用户能看懂"无网络"还是"服务器连不上")。 */ + fun friendlyNetworkMessage(cause: Throwable?): String = when (cause) { + is java.net.UnknownHostException -> MSG_NO_NETWORK + is java.net.ConnectException -> MSG_SERVER_UNREACHABLE + is java.net.SocketTimeoutException -> MSG_SERVER_TIMEOUT + else -> MSG_NETWORK_GENERIC + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/request/AsApiRequester.kt b/app/src/main/java/com/pukka/jiecheng/request/AsApiRequester.kt old mode 100755 new mode 100644 index e1cd652..7f364a3 --- a/app/src/main/java/com/pukka/jiecheng/request/AsApiRequester.kt +++ b/app/src/main/java/com/pukka/jiecheng/request/AsApiRequester.kt @@ -14,6 +14,7 @@ import com.pukka.jiecheng.network.dto.CheckBenefitsDto import com.pukka.jiecheng.network.dto.CheckBenefitsReq import com.pukka.jiecheng.network.dto.CheckDto import com.pukka.jiecheng.network.dto.CheckReq +import com.pukka.jiecheng.network.dto.HeartBeatDto import com.pukka.jiecheng.network.dto.HeartBeatReq import com.pukka.jiecheng.network.dto.InitDeviceDto import com.pukka.jiecheng.network.dto.InitDto @@ -57,6 +58,7 @@ object AsApiRequester { private const val TAG = "AsApiRequester" private const val AS_ROOM_NO_KEY = "as_roomNo" private const val MAX_POLLING_COUNT = 10 + private val deviceMacPairCache = SingleValueCache>() // 埋点常量(与原 Java 保持一致,供 VideoDetailActivity/VideoDetailStatusManager 引用) const val TRACKING_TYPE_SDK_INIT = "sdk_init" @@ -64,6 +66,8 @@ object AsApiRequester { // 扫码激活(BenefitOrderController 三阶段命中 10200)成功后重新初始化 SDK 的埋点类型 const val TRACKING_TYPE_SDK_SCAN_INIT = "scan_init" const val TRACKING_TYPE_VIDEO_STREAM = "video_stream" + // 天卡扫码激活:阶段一 create 订单创建失败埋点类型 + const val TRACKING_TYPE_BENEFIT_ORDER_CREATE = "benefit_order_create" const val TRACKING_STAGE_START = "start" const val TRACKING_STAGE_SUCCESS = "success" @@ -105,58 +109,9 @@ object AsApiRequester { private var pollingJob: Job? = null private val pollingLock = Any() - /** - * 外部注入的第三方设备参数(优先于自行采集)。 - * - * 调用方通过 [setThirdPartyDeviceParams] 注入后,[buildInitReq] 会优先使用其中非空字段; - * 未提供的字段(null 或空)回退到 MacUtils / DeviceIdentityInitManager 自行采集。 - */ - @Volatile - private var thirdPartyDeviceParams: ThirdPartyDeviceParams? = null - - /** 第三方变体标记(thirdparty flavor),try-catch 兜底避免 BuildConfig 未初始化场景崩溃 */ - private fun isThirdPartyVariant(): Boolean = try { - AppConfiguration.getInstance().isThirdPartyVariant() - } catch (e: Throwable) { - false - } - - /** - * 第三方后端(58.220.83.58:8084)checkBenefits 响应归一化: - * - code=0 + status=ACTIVE -> 映射为 10200(Activated) - * - code=0 + status=UNBOUND -> 映射为 10400(Activating 中间态,进入轮询) - * - expireAt -> expireTime 字段映射(业务侧统一消费 expireTime) - * 返回 null 表示非第三方响应或无法识别(走原版逻辑)。 - */ - private fun normalizeThirdPartyBenefits(code: Int, dto: CheckBenefitsDto): Pair? { - if (!isThirdPartyVariant() || code != 0) { - return null - } - val mapped = dto.let { - if (it.expireTime.isNullOrEmpty() && !it.expireAt.isNullOrEmpty()) it.copy(expireTime = it.expireAt) else it - } - return when (mapped.status?.trim()?.uppercase()) { - "ACTIVE" -> CHECK_STATUS_ACTIVATED to mapped - "UNBOUND" -> CHECK_STATUS_ACTIVATING to mapped - else -> code to mapped - } - } - /** 重置激活场景标记 */ fun resetActiveSence() { activeSence = false } - /** 注入第三方设备参数,后续 buildInitReq 优先使用 */ - @JvmStatic - fun setThirdPartyDeviceParams(params: ThirdPartyDeviceParams?) { - thirdPartyDeviceParams = params - } - - /** 清除已注入的第三方设备参数,恢复自行采集 */ - @JvmStatic - fun clearThirdPartyDeviceParams() { - thirdPartyDeviceParams = null - } - // ===================================================================================== // 公共业务结果类型 // ===================================================================================== @@ -189,7 +144,7 @@ object AsApiRequester { /** * check 接口统一返回。 * - * 替代原 CheckStatusCallback 9 个回调方法:sealed class 把状态合并 + Failure(code, msg) 表达错误。 + * 替代原 CheckStatusCallback 10 个回调方法:sealed class 把状态合并 + Failure(code, msg) 表达错误。 */ sealed class CheckStatus { /** @@ -197,8 +152,14 @@ object AsApiRequester { * * @param msg 服务端响应原文(如"已经激活")——Finding #1 修复:必须保留业务消息, * MainActivity.onActivated 通过 result.msg 显示给用户,原 Java 保留此字段。 + * @param code 真实业务码(10200=已激活 / 10900=已通过激活码激活), + * 供 Java 兼容层按码分发 onActivated / onCodeActivated */ - data class Activated(val dto: CheckBenefitsDto, val msg: String = "") : CheckStatus() + data class Activated( + val dto: CheckBenefitsDto, + val msg: String = "", + val code: Int = CHECK_STATUS_ACTIVATED + ) : CheckStatus() data class Preset(val dto: CheckBenefitsDto) : CheckStatus() data class Activating(val dto: CheckBenefitsDto) : CheckStatus() data class ActivateFailed(val dto: CheckBenefitsDto) : CheckStatus() @@ -230,18 +191,6 @@ object AsApiRequester { data class Failure(val errorCode: Int, val message: String) : CheckBenefitsStatus() } - /** - * 第三方设备参数,用于外部注入覆盖自行采集的设备标识。 - * - * 所有字段可空:null 或空串表示未提供,[buildInitReq] 回退到 MacUtils / DeviceIdentityInitManager。 - */ - data class ThirdPartyDeviceParams( - @JvmField val mac: String? = null, - @JvmField val wifiMac: String? = null, - @JvmField val deviceSn: String? = null, - @JvmField val deviceUid: String? = null - ) - // ===================================================================================== // 7 个端点 // ===================================================================================== @@ -273,8 +222,7 @@ object AsApiRequester { throw ApiException.WrapperException(e.toApiError()) } - // 兼容两种成功码:原版后端返回 200,第三方后端(58.220.83.58:8084)文档约定返回 0 - if (resp.code != 200 && resp.code != 0) { + if (resp.code != 200) { throw ApiException.BusinessException(code = resp.code ?: -1, msg = resp.msg) } @@ -351,7 +299,9 @@ object AsApiRequester { } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - return CheckStatus.Failure(1004, e.toApiError().message) + // 网络错误统一 1007(区分无网络/无法访问服务器/超时),解析 1004 + val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(e) + return CheckStatus.Failure(code, msg) } if (activateResp.code != 200) { @@ -387,6 +337,7 @@ object AsApiRequester { * * 终态语义(来自原 dispatchCheckStatusCallback): * - 10200 Activated + * - 10900 CodeActivated (终态,Java 回调拆分走 onCodeActivated) * - 10300 Preset (中间态,继续轮询) * - 10400 Activating (中间态,继续轮询) * - 10500 ActivateFailed (终态) @@ -443,18 +394,14 @@ object AsApiRequester { } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - return CheckBenefitsStatus.Failure(1004, e.toApiError().message) + val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(e) + return CheckBenefitsStatus.Failure(code, msg) } // checkBenefits 的 code 是业务状态码,不是 200 成功码,不能按 !=200 判失败 - var code = resp.code ?: -1 + val code = resp.code ?: -1 val msg = resp.msg.orEmpty() - var dto = resp.data ?: CheckBenefitsDto() - // 第三方后端(58.220.83.58:8084):code=0 + data.status=ACTIVE/UNBOUND,归一化到 10200/10400 - normalizeThirdPartyBenefits(code, dto)?.let { (mappedCode, mappedDto) -> - code = mappedCode - dto = mappedDto - } + val dto = resp.data ?: CheckBenefitsDto() return CheckBenefitsStatus.Success(code, msg, dto) } @@ -462,7 +409,8 @@ object AsApiRequester { * 刷新 sign。 * * 由 SignAuthenticator 在 OkHttp Authenticator 链路内同步调用(runBlocking),业务侧通常不直接调用。 - * 路径 jst/refreshSign 已在 SignRequestPolicy 中排除 sign 注入与刷新触发。 + * 路径 jst/refreshSign 已在 SignRequestPolicy 中排除刷新触发(Authenticator/BusinessCodeRefreshInterceptor 不拦截自身), + * 但 HeaderInterceptor 仍会注入旧 sign 头供服务端鉴权(服务端用旧 sign 校验合法性后下发新 sign)。 */ suspend fun requestAsRefreshSignApi(context: Context): String { // 第 4 点:sign 缺失前置校验(对齐 Java:refreshSign 不首签,缺 sign 直接失败) @@ -484,7 +432,8 @@ object AsApiRequester { val newSign = resp.data?.sign.orEmpty() if (newSign.isEmpty()) { - throw ApiException.BusinessException(code = 1007, msg = "解析新 sign 错误") + // 解析异常统一 1004(原 1007 与网络混用) + throw ApiException.BusinessException(code = 1004, msg = "解析新 sign 错误") } // 双写:TokenStore(OkHttp 拦截器读)+ 同步 SharedPreferences(兼容旧 SP 读取路径) @@ -494,8 +443,11 @@ object AsApiRequester { /** * 心跳。 + * + * 返回 [HeartBeatDto](服务端可能通过 `commandType` 字段下发控制命令, + * 调用方需自行按命令值分派处理,例如 "revokeCardSecret")。 */ - suspend fun requestAsHeartBeatApi(context: Context) { + suspend fun requestAsHeartBeatApi(context: Context): HeartBeatDto? { // 第 4 点:sign 缺失前置校验(对齐 Java:心跳不首签,缺 sign 直接失败) if (Network.tokenStore.getSign().isNullOrEmpty()) { throw ApiException.BusinessException(code = 1001, msg = "token凭证为空") @@ -513,6 +465,35 @@ object AsApiRequester { if (resp.code != 200) { throw ApiException.BusinessException(code = resp.code ?: -1, msg = resp.msg) } + return resp.data + } + + /** + * 设备登出(POST /api/jst/benefit/device/logout,家用版微信扫码登录的逆操作)。 + * + * - 无请求体;sign 请求头由 HeaderInterceptor 自动注入(路径不在 SignRequestPolicy 排除名单) + * - 与 init 等老接口不同:本接口(benefit/device 系)成功码是 **0** 而非 200 + * - 成功响应 data 恒为 null,故无返回值;失败抛 [ApiException.WrapperException] / [ApiException.BusinessException] + * - sign 缺失说明设备侧本就无有效会话,直接视为成功(本地清理由调用方执行) + */ + suspend fun requestAsLogoutApi() { + if (Network.tokenStore.getSign().isNullOrEmpty()) { + LogUtils.w(TAG, "requestAsLogoutApi: no sign, treat as already logged out") + return + } + val api: AsApi = RetrofitFactory.createApi(EnvProvider.AS_DOMAIN) + val resp = try { + api.deviceLogout() + } catch (ce: CancellationException) { + throw ce + } catch (e: Throwable) { + throw ApiException.WrapperException(e.toApiError()) + } + // benefit/device 系接口成功码为 0(非 200) + if (resp.code != 0) { + throw ApiException.BusinessException(code = resp.code ?: -1, msg = resp.msg) + } + LogUtils.d(TAG, "requestAsLogoutApi success") } /** @@ -583,10 +564,11 @@ object AsApiRequester { } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - val apiError = e.toApiError() // 第 2 点:轮询中网络错误继续重试(返回 null,外层 pollCheckStatus 进入下一轮); - // 独立调用直接返回 Failure(对齐 Java 轮询 onFail->checkNextPolling,独立 onFail) - return if (isPolling) null else CheckStatus.Failure(apiError.businessCode ?: 1004, apiError.message) + // 独立调用返回 Failure,错误码经 mapper 统一(1007=网络/1004=解析) + if (isPolling) return null + val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(e) + return CheckStatus.Failure(code, msg) } val code = resp.code @@ -607,7 +589,7 @@ object AsApiRequester { // 第 6 点:未知 code(含 200)走 Unknown,对齐 Java dispatchCheckStatusCallback 的 default->onUnknown return when (code) { - CHECK_STATUS_ACTIVATED, CHECK_STATUS_CODE_ACTIVATED -> CheckStatus.Activated(dto, msg) + CHECK_STATUS_ACTIVATED, CHECK_STATUS_CODE_ACTIVATED -> CheckStatus.Activated(dto, msg, code) CHECK_STATUS_PRESET -> if (isPolling) null else CheckStatus.Preset(dto) CHECK_STATUS_ACTIVATING -> if (isPolling) null else CheckStatus.Activating(dto) CHECK_STATUS_ACTIVATE_FAIL -> CheckStatus.ActivateFailed(dto) @@ -652,45 +634,6 @@ object AsApiRequester { return null } - /** - * 第三方后端(58.220.83.58:8084)权益轮询。 - * - * 与 [pollCheckStatus] 的差异(对齐第三方接口文档 §4.4): - * - 轮询 checkBenefits 本身(第三方后端无 /api/jst/check 接口) - * - 固定 2s 间隔(非指数退避),总时长 5 分钟(150 次)封顶 - * - data.status=ACTIVE 停止;UNBOUND 继续;其他状态视为终态失败 - * - 单次网络异常不中断轮询,由 5 分钟总时长兜底 - */ - private suspend fun pollThirdPartyCheckBenefits( - context: Context, - api: AsApi, - maxAttempts: Int = 150, - intervalMs: Long = 2000L - ): CheckStatus? { - var attempt = 0 - while (attempt < maxAttempts) { - kotlinx.coroutines.delay(intervalMs) - attempt++ - try { - val resp = api.checkBenefits(CheckBenefitsReq()) - var dto = resp.data ?: CheckBenefitsDto() - if (dto.expireTime.isNullOrEmpty() && !dto.expireAt.isNullOrEmpty()) { - dto = dto.copy(expireTime = dto.expireAt) - } - when (dto.status?.trim()?.uppercase()) { - "ACTIVE" -> return CheckStatus.Activated(dto) - "UNBOUND" -> Unit // 激活中,继续轮询 - else -> return CheckStatus.Unknown(dto) - } - } catch (ce: CancellationException) { - throw ce - } catch (e: Throwable) { - // 瞬时网络异常吞掉,继续下一轮;attempt 计数保证最终退出 - } - } - return null - } - /** * 组装 init 请求体(提取原 AsApiRequester.java:217-232 的字段映射)。 */ @@ -702,9 +645,8 @@ object AsApiRequester { } catch (e: Throwable) { null } - val params = thirdPartyDeviceParams - val deviceSn = safeText(params?.deviceSn?.takeIf { it.isNotEmpty() } ?: identity?.deviceSn) - val deviceUid = safeText(params?.deviceUid?.takeIf { it.isNotEmpty() } ?: identity?.deviceUid) + val deviceSn = safeText(identity?.deviceSn) + val deviceUid = safeText(identity?.deviceUid) val channelId = safeText(AppConfiguration.getInstance().getChannelId()) val isTouch = try { AppConfiguration.getInstance().isTouchMode() @@ -713,18 +655,13 @@ object AsApiRequester { } val deviceType = if (isTouch) "touch" else "remote" - val wifiMac = safeText(params?.wifiMac?.takeIf { it.isNotEmpty() } ?: MacUtils.getWlan0Mac()) - // 第三方变体:58 后端文档字段名为 camelCase(wifiMac/deviceSn/deviceUid), - // 双写两种命名,服务端各自忽略不认识的字段;原版变体别名为 null,请求体不变。 - val thirdParty = isThirdPartyVariant() + // 拿不到有线 MAC 时以 WifiMAC 为主(两者相同),由 MacUtils.getDeviceMacPair 封装 + val macPair = getCachedDeviceMacPair(context) return InitReq( - mac = safeText(params?.mac?.takeIf { it.isNotEmpty() } ?: MacUtils.getMac(context)), - wifiMac = wifiMac, + mac = safeText(macPair[0]), + wifiMac = safeText(macPair[1]), deviceSn = deviceSn, deviceUid = deviceUid, - wifiMacAlias = if (thirdParty) wifiMac else null, - deviceSnAlias = if (thirdParty) deviceSn else null, - deviceUidAlias = if (thirdParty) deviceUid else null, verCode = verCode, verName = verName, orgCode = channelId, @@ -752,13 +689,15 @@ object AsApiRequester { } catch (e: Throwable) { null } + // 拿不到有线 MAC 时以 WifiMAC 为主(两者相同),由 MacUtils.getDeviceMacPair 封装 + val macPair = getCachedDeviceMacPair(context) return TrackingReq( trackingType = safeText(trackingType), trackingStage = safeText(trackingStage), resultCode = safeText(resultCode), resultMessage = safeText(resultMessage), - mac = safeText(MacUtils.getMac(context)), - wifiMac = safeText(MacUtils.getWlan0Mac()), + mac = safeText(macPair[0]), + wifiMac = safeText(macPair[1]), appVersion = safeText(PackageUtils.getVersionName(context)), deviceUid = safeText(identity?.deviceUid), other = buildTrackingOther(context, identity), @@ -781,6 +720,13 @@ object AsApiRequester { return obj.toString() } + private fun getCachedDeviceMacPair(context: Context): Array { + val appContext = context.applicationContext ?: context + return deviceMacPairCache.getOrLoad { + MacUtils.getDeviceMacPair(appContext) + } + } + private fun safeText(value: String?): String = value?.trim().orEmpty() // ===================================================================================== @@ -823,8 +769,9 @@ object AsApiRequester { } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - val err = e.toApiError() - callback?.onFail(err.businessCode ?: 1007, err.message) + // init 失败统一走 mapper:1007=网络(无网络/无法访问服务器)→ MainActivity 拉全屏错误页 + val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(e) + callback?.onFail(code, msg) } } } @@ -862,8 +809,8 @@ object AsApiRequester { } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - val err = e.toApiError() - callback?.onFail(err.businessCode ?: 1010, err.message) + val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(e) + callback?.onFail(code, msg) } } } @@ -881,8 +828,8 @@ object AsApiRequester { } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - val err = e.toApiError() - callback?.onFail(err.businessCode ?: 1007, err.message) + val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(e) + callback?.onFail(code, msg) } } } @@ -916,37 +863,6 @@ object AsApiRequester { val api: AsApi = RetrofitFactory.createApi(EnvProvider.AS_DOMAIN) val code = result.code - if (isThirdPartyVariant()) { - // 第三方后端(58.220.83.58:8084)无 /api/jst/check 接口: - // - ACTIVE(已归一化为 10200):跳过 check 确认轮询,直接回调已激活 - // - UNBOUND(已归一化为 10400):按文档轮询 checkBenefits 本身(2s 间隔,5min 超时) - when (code) { - CHECK_STATUS_ACTIVATED -> { - val r = toJavaResultWithCode(CHECK_STATUS_ACTIVATED, result.msg, result.dto) - callback?.onActivated(r) - } - CHECK_STATUS_ACTIVATING -> { - dispatchCheckBenefitsByCode(code, result.msg, result.dto, callback) - val polled = pollThirdPartyCheckBenefits(context, api) - when (polled) { - null -> dispatchToJavaCallbacks( - CheckStatus.Failure(1008, POLLING_TIMEOUT_MSG), - null, - callback, - "checkBenefits-tp-poll" - ) - is CheckStatus.Activated -> { - val r = toJavaResultWithCode(CHECK_STATUS_ACTIVATED, "已经激活", polled.dto) - callback?.onActivated(r) - } - else -> dispatchToJavaCallbacks(polled, null, callback, "checkBenefits-tp-poll") - } - } - else -> dispatchCheckBenefitsByCode(code, result.msg, result.dto, callback) - } - return@launch - } - when (code) { // 激活类状态码(10200/10300/10400):10300/10400 先分发中间态,再轮询; // 10200 跳过直接分发,统一经轮询确认后回调 onActivated(与原 Java 一致) @@ -971,9 +887,14 @@ object AsApiRequester { ) } is CheckStatus.Activated -> { - // 保留原 Java:轮询命中激活后 msg 置"已经激活" - val r = toJavaResultWithCode(CHECK_STATUS_ACTIVATED, "已经激活", polled.dto) - callback?.onActivated(r) + // 保留原 Java:轮询命中激活后 msg 置"已经激活"; + // code 保留真实业务码,10900 走 onCodeActivated + val r = toJavaResultWithCode(polled.code, "已经激活", polled.dto) + if (polled.code == CHECK_STATUS_CODE_ACTIVATED) { + callback?.onCodeActivated(r) + } else { + callback?.onActivated(r) + } } else -> { // 轮询命中其他终态(失败/未绑定/过期/占用/未知),统一分发 @@ -982,7 +903,7 @@ object AsApiRequester { } } else -> { - // 终态失败码(10500-10800)/未知码:直接分发,不轮询 + // 10900/终态失败码(10500-10800)/未知码:直接分发,不轮询 dispatchCheckBenefitsByCode(code, result.msg, result.dto, callback) } } @@ -991,8 +912,8 @@ object AsApiRequester { } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - val err = e.toApiError() - callback?.onFail(err.businessCode ?: 1007, err.message) + val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(e) + callback?.onFail(code, msg) } } } @@ -1010,26 +931,29 @@ object AsApiRequester { } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - val err = e.toApiError() - callback?.onFail(err.businessCode ?: 1005, err.message) + val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(e) + callback?.onFail(code, msg) } } } /** * Java 兼容:阻塞调用 heartBeat。 + * + * 回调 [HeartBeatCallback.onSuccess] 会接收完整 [HeartBeatDto], + * 调用方按 dto.action[].commandType 自行分支处理(如 "revokeCardSecret")。 */ @JvmStatic fun requestAsHeartBeatApi(context: Context, callback: HeartBeatCallback?) { AppScope.launch { try { - requestAsHeartBeatApi(context) - callback?.onSuccess() + val dto = requestAsHeartBeatApi(context) + callback?.onSuccess(dto) } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - val err = e.toApiError() - callback?.onFail(err.businessCode ?: 1005, err.message) + val (code, msg) = AsApiErrorMapper.mapThrowableToAsFail(e) + callback?.onFail(code, msg) } } } @@ -1070,12 +994,21 @@ object AsApiRequester { } interface HeartBeatCallback { - fun onSuccess() + /** + * 心跳成功。 + * + * @param dto 完整心跳响应数据;调用方按 dto.action[].commandType 分支处理 + * (如 "revokeCardSecret")。dto 为 null 表示服务端未返回 data。 + */ + fun onSuccess(dto: HeartBeatDto?) fun onFail(errorCode: Int, message: String) } interface CheckStatusCallback { fun onActivated(result: CheckBenefitsResult) + + /** 10900=已通过激活码激活:与 10200 拆分回调,调用方可按业务码区分处理 */ + fun onCodeActivated(result: CheckBenefitsResult) fun onPreset(result: CheckBenefitsResult) fun onActivating(result: CheckBenefitsResult) fun onActivateFailed(result: CheckBenefitsResult) @@ -1116,9 +1049,14 @@ object AsApiRequester { is CheckStatus.Activated -> { // Finding #1 修复:透传业务消息(来自服务端 NetResponse.msg), // MainActivity.onActivated 通过 result.msg 显示弹窗文案(原 Java 保留此字段)。 - val result = toJavaResult(status.dto, status.msg) + val result = toJavaResultWithCode(status.code, status.msg, status.dto) memberCallback?.onSuccess(result) - statusCallback?.onActivated(result) + // 按真实业务码分发:10900 走 onCodeActivated,10200 走 onActivated + if (status.code == CHECK_STATUS_CODE_ACTIVATED) { + statusCallback?.onCodeActivated(result) + } else { + statusCallback?.onActivated(result) + } } is CheckStatus.Preset -> { statusCallback?.onPreset(toJavaResult(status.dto)) @@ -1162,24 +1100,39 @@ object AsApiRequester { dto: CheckBenefitsDto, callback: CheckStatusCallback? ) { - clearCachedCardNosIfInvalidStatus(code) + // 仅 INVALID 业务码分支前清缓存:显式隔离清理触发面,避免后续维护时误伤 Activated 等保留态 val result = toJavaResultWithCode(code, msg, dto) when (code) { - CHECK_STATUS_ACTIVATED, CHECK_STATUS_CODE_ACTIVATED -> callback?.onActivated(result) + CHECK_STATUS_ACTIVATED -> callback?.onActivated(result) + CHECK_STATUS_CODE_ACTIVATED -> callback?.onCodeActivated(result) CHECK_STATUS_PRESET -> callback?.onPreset(result) CHECK_STATUS_ACTIVATING -> callback?.onActivating(result) - CHECK_STATUS_ACTIVATE_FAIL -> callback?.onActivateFailed(result) - CHECK_STATUS_UNBOUND -> callback?.onUnbound(result) - CHECK_STATUS_EXPIRED -> callback?.onExpired(result) - CHECK_STATUS_OCCUPIED -> callback?.onOccupied(result) - else -> callback?.onUnknown(result) + CHECK_STATUS_ACTIVATE_FAIL -> { + clearCachedCardNosIfInvalidStatus(code) + callback?.onActivateFailed(result) + } + CHECK_STATUS_UNBOUND -> { + clearCachedCardNosIfInvalidStatus(code) + callback?.onUnbound(result) + } + CHECK_STATUS_EXPIRED -> { + clearCachedCardNosIfInvalidStatus(code) + callback?.onExpired(result) + } + CHECK_STATUS_OCCUPIED -> { + clearCachedCardNosIfInvalidStatus(code) + callback?.onOccupied(result) + } + else -> { + clearCachedCardNosIfInvalidStatus(code) + callback?.onUnknown(result) + } } } private val INVALID_CARD_STATUS = intArrayOf( CHECK_STATUS_EXPIRED, - CHECK_STATUS_OCCUPIED, - CHECK_STATUS_CODE_ACTIVATED + CHECK_STATUS_OCCUPIED ) private fun clearCachedCardNosIfInvalidStatus(code: Int) { @@ -1224,4 +1177,32 @@ object AsApiRequester { unusedCardCount = dto.unusedCardCount.orEmpty() ) } -} \ No newline at end of file +} + +internal class SingleValueCache { + @Volatile + private var value: Any? = UNINITIALIZED + + fun getOrLoad(loader: () -> T): T { + val cached = value + if (cached !== UNINITIALIZED) { + @Suppress("UNCHECKED_CAST") + return cached as T + } + return synchronized(this) { + val doubleChecked = value + if (doubleChecked !== UNINITIALIZED) { + @Suppress("UNCHECKED_CAST") + doubleChecked as T + } else { + val loaded = loader() + value = loaded + loaded + } + } + } + + private companion object { + private val UNINITIALIZED = Any() + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/request/AsHeartBeatManager.java b/app/src/main/java/com/pukka/jiecheng/request/AsHeartBeatManager.java old mode 100755 new mode 100644 index 867fc67..15c2f3f --- a/app/src/main/java/com/pukka/jiecheng/request/AsHeartBeatManager.java +++ b/app/src/main/java/com/pukka/jiecheng/request/AsHeartBeatManager.java @@ -1,11 +1,19 @@ package com.pukka.jiecheng.request; +import android.app.Activity; import android.content.Context; import com.pukka.jiecheng.config.AppConfiguration; +import com.pukka.jiecheng.login.HomeLogoutHandler; +import com.pukka.jiecheng.network.dto.HeartBeatDto; +import com.pukka.jiecheng.utils.BenefitRevokedDialog; +import com.pukka.jiecheng.utils.JetsenxSdkUtil; import com.pukka.libcore.threading.DispatchQueue; +import com.pukka.libcore.utils.AppManager; import com.pukka.libcore.utils.LogUtils; +import java.util.UUID; + /** * @file AsHeartBeatManager.java * @description 负责管理心跳请求的周期性调度,不依赖 UI,保证鲁棒性 @@ -14,6 +22,13 @@ import com.pukka.libcore.utils.LogUtils; */ public class AsHeartBeatManager { private static final String TAG = "AsHeartBeatManager"; + + /** + * 心跳响应中服务端下发的"撤销权益码"命令值。 + * 收到此命令时,设备需要把本地缓存的 sUid 置为随机数并重新初始化 SDK。 + */ + private static final String COMMAND_REVOKE_CARD_SECRET = "revokeCardSecret"; + private static volatile AsHeartBeatManager instance; private boolean isRunning = false; @@ -46,7 +61,7 @@ public class AsHeartBeatManager { Context appContext = context.getApplicationContext(); isRunning = true; - + long interval = AppConfiguration.getInstance().getHeartBeatInterval(); LogUtils.i(TAG, "Starting HeartBeat with interval: " + interval + " ms"); @@ -58,8 +73,9 @@ public class AsHeartBeatManager { LogUtils.d(TAG, "Executing scheduled heart beat request..."); requester.requestAsHeartBeatApi(appContext, new AsApiRequester.HeartBeatCallback() { @Override - public void onSuccess() { + public void onSuccess(HeartBeatDto dto) { LogUtils.d(TAG, "Heart beat request success."); + dispatchHeartBeatActions(appContext, dto); } @Override @@ -75,6 +91,33 @@ public class AsHeartBeatManager { DispatchQueue.global.timer(interval, interval, heartBeatRunnable); } + /** + * 遍历服务端在心跳响应里下发的动作数组,按 commandType 分派到具体处理函数。 + *

当前已实现命令: + *

    + *
  • {@value #COMMAND_REVOKE_CARD_SECRET}:撤销权益码 → {@link #handleRevokeCardSecret}
  • + *
+ * 未知命令 / 缺失 commandType 的元素忽略,仅打日志便于排查。 + */ + private void dispatchHeartBeatActions(Context context, HeartBeatDto dto) { + java.util.List actions = + dto == null ? null : dto.getAction(); + if (actions == null || actions.isEmpty()) { + return; + } + for (com.pukka.jiecheng.network.dto.HeartBeatActionDto action : actions) { + if (action == null) { + continue; + } + String commandType = action.getCommandType(); + LogUtils.d(TAG, "dispatchHeartBeatActions: commandType=" + commandType); + if (COMMAND_REVOKE_CARD_SECRET.equals(commandType)) { + handleRevokeCardSecret(context); + } + // 其他 commandType 后续按需扩展分支 + } + } + /** * 停止心跳定时器 */ @@ -89,4 +132,63 @@ public class AsHeartBeatManager { } isRunning = false; } + + /** + * 处理服务端下发的 revokeCardSecret 命令:把本地缓存的权益码(sUid)置为随机数, + * 然后调用 JetsenxSdkUtil.globalInit() 重新初始化 SDK,使设备在 SDK 侧回到未激活状态; + * 同时委托 {@link HomeLogoutHandler#performDeviceLogout()} 通知后端登出并清理本地登录缓存 + * (头像、昵称、unionid、expireTime),与 HomeMyFragment 的退出登录走同一条路径; + * 接着在 UI 线程弹出 {@link BenefitRevokedDialog} 提示用户"权益已取消,将退出程序!", + * 用户点击确定后退出整个程序。 + * + *

并发安全由 JetsenxSdkUtil.globalInit() 内部 sReInitializing + sPendingReInitQueue + * 串行机制保证,本方法无需额外加锁。

+ */ + private void handleRevokeCardSecret(final Context context) { + // 生成 32 字符十六进制的随机 uid;UUID 全局去重,与设备原 thirdCardNo 无任何关联。 + final String randomUid = UUID.randomUUID().toString().replace("-", ""); + LogUtils.d(TAG, "handleRevokeCardSecret: writing random uid to sUid and re-init SDK"); + + // 先写入新的随机标识(globalInit 内部会读 sUid 触发 SDK init) + JetsenxSdkUtil.cacheReusableUid(randomUid); + + JetsenxSdkUtil.globalInit(context, new JetsenxSdkUtil.InitMemberCodeCallback() { + @Override + public void onSuccess(String memberCode) { + LogUtils.d(TAG, "handleRevokeCardSecret: SDK re-init success, memberCode=" + memberCode); + } + + @Override + public void onFail(int errorCode, String message) { + // 失败不抛:保持与 JetsenxSdkUtil.globalInit 内部回调容错一致; + // sUid 已替换为随机数,UI 入口短路自然失效,后续 /api/jst/check 会走正常激活判定。 + LogUtils.e(TAG, "handleRevokeCardSecret: SDK re-init failed, code=" + errorCode + ", msg=" + message); + } + }); + + // 通知后端设备登出 + 清理本地登录缓存(头像、昵称、unionid、expireTime、SDK 卡密), + // 复用 HomeLogoutHandler 与 HomeMyFragment.performLogout 共用的清理路径, + // 保证服务端与本地状态一致;该调用异步进行,失败仅打日志,不阻塞后续弹框与退出流程。 + HomeLogoutHandler.INSTANCE.performDeviceLogout(); + + // 切到 UI 线程弹框。心跳回调跑在 DispatchQueue.global(后台线程), + // Dialog 必须 Activity + 主线程。从 AppManager 拿栈顶 Activity; + // 若无 Activity(应用已退出 / 进程被回收),仅打日志跳过,sUid 已重置 + // 下次冷启动会自然走到未激活状态。 + final Activity activity = AppManager.getAppManager().currentActivity(); + if (activity == null || activity.isFinishing() || activity.isDestroyed()) { + LogUtils.w(TAG, "handleRevokeCardSecret: no active Activity, skip showing dialog"); + return; + } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + try { + new BenefitRevokedDialog(activity).show(); + } catch (Exception e) { + LogUtils.e(TAG, "handleRevokeCardSecret: show BenefitRevokedDialog exception: " + e.getMessage()); + } + } + }); + } } diff --git a/app/src/main/java/com/pukka/jiecheng/search/Bean/HistoryBean.java b/app/src/main/java/com/pukka/jiecheng/search/Bean/HistoryBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/SearchActivity.java b/app/src/main/java/com/pukka/jiecheng/search/SearchActivity.java old mode 100755 new mode 100644 index 60b8382..d18a440 --- a/app/src/main/java/com/pukka/jiecheng/search/SearchActivity.java +++ b/app/src/main/java/com/pukka/jiecheng/search/SearchActivity.java @@ -119,6 +119,32 @@ public class SearchActivity extends BaseActivity implements UnFocusListener { if (intent != null) { int type = intent.getIntExtra("byPos", 1); pageEnter(type); + // 语音接入入口(文档 §9.1):search_keyword + auto_search + handleVoiceIntent(intent); + } + } + + /** + * 处理语音协议携带的搜索关键词与自动搜索标记(文档 §9.1)。 + */ + private void handleVoiceIntent(Intent intent) { + if (intent == null) { + return; + } + String keyword = intent.getStringExtra("search_keyword"); + boolean autoSearch = intent.getBooleanExtra("auto_search", false); + if (keyword == null || keyword.isEmpty()) { + return; + } + // 写入 searchValue 与 searchTv + searchValue.setLength(0); + searchValue.append(keyword); + searchTv.setText(searchValue); + if (autoSearch) { + // 直接触发搜索(不依赖键盘焦点) + setHotShow(false); + setNoResultShow(false, false); + getVideoResult(keyword, ""); } } diff --git a/app/src/main/java/com/pukka/jiecheng/search/SearchAmberManger.java b/app/src/main/java/com/pukka/jiecheng/search/SearchAmberManger.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/SearchConstant.java b/app/src/main/java/com/pukka/jiecheng/search/SearchConstant.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/SearchRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/search/SearchRecyclerView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/T9KeyboardPop.java b/app/src/main/java/com/pukka/jiecheng/search/T9KeyboardPop.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/UnFocusListener.java b/app/src/main/java/com/pukka/jiecheng/search/UnFocusListener.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/adapter/ContentInfoAdapter.java b/app/src/main/java/com/pukka/jiecheng/search/adapter/ContentInfoAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/adapter/DisplayTypeAdapter.java b/app/src/main/java/com/pukka/jiecheng/search/adapter/DisplayTypeAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/adapter/HistoryAdapter.java b/app/src/main/java/com/pukka/jiecheng/search/adapter/HistoryAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/adapter/HotContentAdapter.java b/app/src/main/java/com/pukka/jiecheng/search/adapter/HotContentAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/adapter/KeyboardAdapter.java b/app/src/main/java/com/pukka/jiecheng/search/adapter/KeyboardAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/adapter/LiveWidgetInfoAdapter.java b/app/src/main/java/com/pukka/jiecheng/search/adapter/LiveWidgetInfoAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/adapter/SuggestAdapter.java b/app/src/main/java/com/pukka/jiecheng/search/adapter/SuggestAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/adapter/T9KeyboardAdapter.java b/app/src/main/java/com/pukka/jiecheng/search/adapter/T9KeyboardAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/search/widget/SearchResultRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/search/widget/SearchResultRecyclerView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/setting/SettingActivity.kt b/app/src/main/java/com/pukka/jiecheng/setting/SettingActivity.kt new file mode 100644 index 0000000..2a0edf8 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/setting/SettingActivity.kt @@ -0,0 +1,205 @@ +package com.pukka.jiecheng.setting + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.util.Log +import androidx.recyclerview.widget.LinearLayoutManager +import com.pukka.jiecheng.R +import com.pukka.jiecheng.base.BaseActivity +import com.pukka.jiecheng.utils.ToastUtils + +/** + * 设置页面。 + * + * 当前支持的设置项: + * 1. 开启语音唤起 — 开关状态直接绑定系统 [Settings.canDrawOverlays]: + * - 显示态 = 当前悬浮窗权限是否被授予(每次 onResume / 点击都重新从系统读) + * - 切换时跳 [Settings.ACTION_MANAGE_OVERLAY_PERMISSION],由用户在系统里授权 / 撤销 + * - SharedPreferences 仅记录"用户是否在设置页主动开过",供业务模块判定是否启用悬浮窗保活路径 + */ +class SettingActivity : BaseActivity() { + + companion object { + private const val TAG = "SettingActivity" + + /** SharedPreferences 文件名,集中管理所有设置项开关 */ + private const val PREF_NAME = "jiecheng_setting" + + /** 用户在设置页主动开启过的开关 key;用于业务模块判定是否走悬浮窗保活路径 */ + private const val KEY_VOICE_WAKEUP_INTENT = "voice_wakeup_intent" + + /** 申请悬浮窗权限的请求码 */ + private const val REQUEST_CODE_OVERLAY = 1001 + + @JvmStatic + fun start(context: Context) { + val intent = Intent(context, SettingActivity::class.java) + if (context !is Activity) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) + } + + /** + * 业务模块查询"用户是否主动开启过语音唤起"。 + * + * 注意:返回值代表用户意图,**不是**当前系统是否实际授权。 + * 业务侧真正使用悬浮窗前应自行调用 [Settings.canDrawOverlays] 二次校验。 + */ + @JvmStatic + fun isVoiceWakeupEnabled(context: Context): Boolean { + return context.applicationContext + .getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getBoolean(KEY_VOICE_WAKEUP_INTENT, false) + } + } + + private lateinit var adapter: SettingAdapter + private val items: MutableList = mutableListOf() + + /** 进入页面时把焦点交给首个 item 的 SwitchCompat,遥控 OK 键直接切换 */ + private var firstItemSwitch: androidx.appcompat.widget.SwitchCompat? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_setting) + initRecyclerView() + initGamePad() + enableBackButton() + } + + override fun onResume() { + super.onResume() + // 从权限设置页返回时,从系统重新读真实授权状态、刷新 UI + refreshVoiceWakeupState() + // 遥控支持:返回页面后重新把焦点交到 SwitchCompat + firstItemSwitch?.requestFocus() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CODE_OVERLAY) { + // 部分 ROM 在 onResume 之前就回调;保险起见这里再刷新一次 + refreshVoiceWakeupState() + } + } + + private fun initRecyclerView() { + adapter = SettingAdapter( + items, + onSwitchChanged = { item, isChecked -> + when (item.id) { + SettingItem.ID_VOICE_WAKEUP -> onVoiceWakeupSwitchChanged(isChecked) + } + }, + onFirstItemBound = { view -> + // 首次 bind 后让 Activity 拿到内部 SwitchCompat 引用,便于 onResume requestFocus + firstItemSwitch = view.getSwitch() + } + ) + val rv = findViewById(R.id.id_rv_setting) + rv.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) + rv.adapter = adapter + + items.clear() + items.add(buildVoiceWakeupItem()) + adapter.notifyDataSetChanged() + } + + /** + * 构建列表项,开关状态 = 系统 [Settings.canDrawOverlays] 的真实值。 + * 注意:这里**不**与 SharedPreferences 复合,因为业务侧需要的是"系统当前授权与否", + * 而不是"曾经被开启过",否则会出现"系统已撤销但页面仍显示开"的脱节。 + */ + private fun buildVoiceWakeupItem(): SettingItem { + return SettingItem( + id = SettingItem.ID_VOICE_WAKEUP, + title = getString(R.string.setting_voice_wakeup_title), + subtitle = getString(R.string.setting_voice_wakeup_subtitle), + checked = isOverlayGranted(), + enabled = true + ) + } + + /** + * 处理"开启语音唤起"开关点击。 + * + * 因为 SYSTEM_ALERT_WINDOW 没有运行时权限弹窗 API(必须跳系统设置页), + * 这里统一跳 [Settings.ACTION_MANAGE_OVERLAY_PERMISSION],由用户在该页面授权或撤销; + * 返回后 [onResume] / [onActivityResult] 会调用 [refreshVoiceWakeupState] 重新从系统读真实状态。 + * + * @param isChecked 期望切换到的目标状态 + */ + private fun onVoiceWakeupSwitchChanged(isChecked: Boolean) { + val currentGranted = isOverlayGranted() + Log.d(TAG, "voice wakeup switch clicked: target=$isChecked, currentGranted=$currentGranted") + + if (isChecked) { + // 期望开启:跳系统权限页申请授权 + writeVoiceWakeupIntent(true) + if (!currentGranted) { + ToastUtils.showShortToast(getString(R.string.setting_voice_wakeup_requesting)) + requestOverlayPermission() + } else { + ToastUtils.showShortToast(getString(R.string.setting_voice_wakeup_opened)) + } + } else { + // 期望关闭:跳系统权限页让用户撤销(Android 没有应用内撤销自身悬浮窗权限的 API) + writeVoiceWakeupIntent(false) + if (currentGranted) { + ToastUtils.showShortToast(getString(R.string.setting_voice_wakeup_requesting)) + requestOverlayPermission() + } + } + } + + /** + * 从系统重新读权限状态,刷新 UI。 + * 不回写 SharedPreferences:系统真实授权是唯一权威,业务侧使用时再做二次校验。 + */ + private fun refreshVoiceWakeupState() { + if (items.isEmpty()) return + val granted = isOverlayGranted() + val item = items[0] + if (item.checked != granted) { + item.checked = granted + adapter.notifyItemChanged(0) + Log.d(TAG, "voice wakeup state refreshed: granted=$granted") + } + } + + private fun writeVoiceWakeupIntent(value: Boolean) { + getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putBoolean(KEY_VOICE_WAKEUP_INTENT, value) + .apply() + } + + private fun isOverlayGranted(): Boolean { + // 标准权限,Build.VERSION_CODES.M 以下始终视为已授权(无需运行时申请) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Settings.canDrawOverlays(this) + } else { + true + } + } + + private fun requestOverlayPermission() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return + val intent = Intent( + Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:$packageName") + ) + try { + startActivityForResult(intent, REQUEST_CODE_OVERLAY) + } catch (e: Exception) { + Log.e(TAG, "request overlay permission failed: " + e.message) + ToastUtils.showShortToast(getString(R.string.setting_voice_wakeup_permission_denied)) + } + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/setting/SettingAdapter.kt b/app/src/main/java/com/pukka/jiecheng/setting/SettingAdapter.kt new file mode 100644 index 0000000..7af520b --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/setting/SettingAdapter.kt @@ -0,0 +1,49 @@ +package com.pukka.jiecheng.setting + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.pukka.jiecheng.R +import com.pukka.jiecheng.widget.SettingItemView + +/** + * 设置页 RecyclerView 适配器。 + * + * 使用 [SettingItemView] 作为统一 item 视图承载(标题 + 副标题 + SwitchCompat), + * 通过 [onSwitchChanged] 把开关变化抛回 Activity,Activity 自行决定后续逻辑 + * (持久化、权限请求、Toast 等)。 + */ +class SettingAdapter( + private val items: List, + private val onSwitchChanged: (SettingItem, Boolean) -> Unit, + /** RecyclerView 首次 bind 第一个 item 时回调,用于 Activity 主动请求 SwitchCompat 焦点(遥控) */ + private val onFirstItemBound: (SettingItemView) -> Unit = {} +) : RecyclerView.Adapter() { + + private var firstBound = false + + class VH(val view: SettingItemView) : RecyclerView.ViewHolder(view) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + val v = LayoutInflater.from(parent.context) + .inflate(R.layout.item_setting_switch, parent, false) as SettingItemView + return VH(v) + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: VH, position: Int) { + val item = items[position] + holder.view.bind(item) + holder.view.setOnCheckedChangeListener { isChecked -> + if (isChecked == item.checked) return@setOnCheckedChangeListener + // 先回写到 model,再回调外部,避免 item 状态与 view 状态错位 + item.checked = isChecked + onSwitchChanged(item, isChecked) + } + if (!firstBound) { + firstBound = true + onFirstItemBound(holder.view) + } + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/setting/SettingItem.kt b/app/src/main/java/com/pukka/jiecheng/setting/SettingItem.kt new file mode 100644 index 0000000..331ec30 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/setting/SettingItem.kt @@ -0,0 +1,24 @@ +package com.pukka.jiecheng.setting + +/** + * 设置页列表项数据模型。 + * + * 当前仅支持一种 item 形态:开关型(标题 + 副标题 + SwitchCompat)。 + * 后续如需扩展(普通点击、seekbar、二级页入口等),在此添加 type 字段并改造 Adapter 即可。 + */ +data class SettingItem( + /** 项唯一标识,用于回调时识别具体设置项 */ + val id: Int, + /** 主标题 */ + val title: String, + /** 副标题/说明文字,可空 */ + val subtitle: String? = null, + /** 当前开关状态 */ + var checked: Boolean = false, + /** 是否允许点击 */ + var enabled: Boolean = true +) { + companion object { + const val ID_VOICE_WAKEUP = 1 + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/utils/ActivitedDialog.java b/app/src/main/java/com/pukka/jiecheng/utils/ActivitedDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/ActivityMonitor.java b/app/src/main/java/com/pukka/jiecheng/utils/ActivityMonitor.java old mode 100755 new mode 100644 index b026420..3e700ee --- a/app/src/main/java/com/pukka/jiecheng/utils/ActivityMonitor.java +++ b/app/src/main/java/com/pukka/jiecheng/utils/ActivityMonitor.java @@ -91,7 +91,10 @@ public class ActivityMonitor implements Application.ActivityLifecycleCallbacks { public Activity getLastAct(){ - return activityStack.lastElement(); + if (!activityStack.isEmpty()) { + return activityStack.lastElement(); + } + return null; } public String getPreAct(){ diff --git a/app/src/main/java/com/pukka/jiecheng/utils/BenefitRevokedDialog.java b/app/src/main/java/com/pukka/jiecheng/utils/BenefitRevokedDialog.java new file mode 100644 index 0000000..850048b --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/utils/BenefitRevokedDialog.java @@ -0,0 +1,141 @@ +/** + * @file BenefitRevokedDialog.java + * @description 权益被服务端撤销时的提示对话框:唯一交互入口是"确定"按钮, + * 点击后退出程序。对话框不可被返回键/外部点击关闭,确保用户 + * 看到"权益已取消"的提示并主动确认退出。 + * @author ainetv + * @date 2026-08-24 + */ +package com.pukka.jiecheng.utils; + +import android.app.Activity; +import android.app.Dialog; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.view.KeyEvent; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.NonNull; + +import com.pukka.jiecheng.R; +import com.pukka.libcore.utils.AppManager; + +/** + * 权益被服务端撤销(心跳响应 commandType="revokeCardSecret")时弹出。 + * + *

设计约束: + *

    + *
  • 屏蔽所有关闭手势(setCancelable + setCanceledOnTouchOutside + onBackPressed 全堵死)
  • + *
  • 只能点击"确定"按钮退出,按钮同时兼容遥控器 DPAD_CENTER / ENTER
  • + *
  • 点击确定 → {@link AppManager#appExit()},先 setKeep(false) 再 appExit, + * 与 MainActivity.exitApp 一致;1 秒延迟 killProcess 给神策退出埋点上报留窗口
  • + *
+ */ +public class BenefitRevokedDialog extends Dialog { + + private Activity activity; + private View btnConfirm; + private boolean isShowing = false; + + public BenefitRevokedDialog(@NonNull Activity activity) { + super(activity, R.style.FullScreenDialog); + this.activity = activity; + initView(); + } + + private void initView() { + setContentView(R.layout.dialog_benefit_revoked); + // 屏蔽所有关闭路径:返回键 / 外部点击 / Dialog 自带的 cancel 动作 + setCancelable(false); + setCanceledOnTouchOutside(false); + + Window window = getWindow(); + if (window != null) { + window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT); + window.setGravity(android.view.Gravity.CENTER); + window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + } + + btnConfirm = findViewById(R.id.btnConfirm); + btnConfirm.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + exitApp(); + } + }); + + // 遥控器中心键 / 遥控器 OK 键兼容:与 ActivitedDialog 一致 + btnConfirm.setOnKeyListener(new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN && + (keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_ENTER + || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)) { + dismiss(); + exitApp(); + return true; + } + return false; + } + }); + } + + /** + * 退出整个程序:对齐 MainActivity.exitApp,先 setKeep(false) 让 appExit 走 + * killProcess 分支;1 秒延迟给神策埋点上报留窗口,与项目约定一致。 + */ + private void exitApp() { + AppManager.getAppManager().setKeep(false); + AppManager.getAppManager().appExit(); + } + + @Override + public void show() { + if (isShowing) { + return; + } + isShowing = true; + super.show(); + requestConfirmFocus(); + } + + @Override + public void dismiss() { + if (!isShowing) { + return; + } + isShowing = false; + super.dismiss(); + } + + /** + * 屏蔽 Activity 层的返回键关闭 Dialog。 + * 与 setCancelable(false) 一起保证 Dialog 不会被任何方式关掉。 + */ + @Override + public void onBackPressed() { + // 故意 no-op:用户必须点"确定"退出程序 + } + + private void requestConfirmFocus() { + if (btnConfirm == null) { + return; + } + btnConfirm.setFocusable(true); + btnConfirm.setFocusableInTouchMode(true); + btnConfirm.requestFocus(); + btnConfirm.post(new Runnable() { + @Override + public void run() { + if (!btnConfirm.hasFocus()) { + btnConfirm.requestFocus(); + } + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/utils/BuglyInitManager.java b/app/src/main/java/com/pukka/jiecheng/utils/BuglyInitManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/BuildConfigUtils.java b/app/src/main/java/com/pukka/jiecheng/utils/BuildConfigUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/ChannelManger.java b/app/src/main/java/com/pukka/jiecheng/utils/ChannelManger.java old mode 100755 new mode 100644 index 637ba79..2cba9b1 --- a/app/src/main/java/com/pukka/jiecheng/utils/ChannelManger.java +++ b/app/src/main/java/com/pukka/jiecheng/utils/ChannelManger.java @@ -60,14 +60,17 @@ public class ChannelManger { // publicKeyStr_Hotel = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnemyIjwf1S5PsqgPhwiPGcF2Efa4UvFiFpg34+d2ZHQ6mkTTaussXCWQQRZGUfGbvkhIJOspGxmxtaMYgNxMQv9BT5B6xcnMILqYzG40hc2N3EiLm4hDYP12f8pshikh5aNxolbgkdA+Y2vrMf2KNucArMh8gT1mUuQRyUMiefy+N/kk2nK2p6SSX0628/BtczNgj13MaMHMlr8krNTlzB/22M8WMSk10nFS4ULp1KOPia8VxqzMIFKeREPIHIUnarz+llFIj6zKGg0ocqS6KEml7jv1yCgBpmyA2kAGEgnuTreO/5h5DqXByKLBEW8bYDiMUb7H/58IRerrITXfVQIDAQAB"; // // } - JetsenxSdkUtil.ACCESSER = "JSTASJD"; - JetsenxSdkUtil.SECRET = "kMeWsBu0QsTXpnft"; + // 橘视厅5.1环绕声业务渠道参数(agcode/servicegroup 与原渠道一致,业务组合与业务线切换到5.1业务) + JetsenxSdkUtil.ACCESSER = "JSTAS51"; + JetsenxSdkUtil.SECRET = "oam25ZqvGy3gVY0x"; JetsenxSdkUtil.AGCODE = "1100101010200492358120169"; BaseConst.agentVendorCode = JetsenxSdkUtil.AGCODE; - BaseConst.serviceComboCode = "1100121010200494755850172"; + BaseConst.serviceComboCode = "1100121044377892530980001"; BaseConst.serviceGroupCode = "1100122010201752209710182"; - BaseConst.businessLine = "4100450011381755998420012"; - publicKeyStr_Hotel_release = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtlfaNsJwQ8wuVdITMLJPrXMeD9idJ0aTziOgkZZhigwzhyPNDAqw+nCKeJGGoTJyhbjPXKtnyI8eNpj+xhCCMXUD3BxOVMFWBhFxzIl83IQJG43mPRXwsJrI4oCYDaKv5aeZZBqF0OLPglXU+q19/28ltONvxcKp8YxXagmixzz1SDgLITM3s3ExqiaScK1dcpPtXbCpI4GXPP9l0ekkCBAo/d1oQqbxHvdE5ufAhEZ042k5tVHUgX9cUoedA9F2JcPhc+zUNCJUAQLvfVXxqNdhwxA4TY+64OX2iSje/cTqxp+4RcrX4rbUIYZitVQCRexZjKz7Yd/KXp8CKCho6wIDAQAB"; + BaseConst.businessLine = "4100450044057893707260002"; + // 橘视厅5.1业务免登录公钥(官方下发,debug/release 暂用同一把) + publicKeyStr_Hotel = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsEYpWdT+OumCBtx3lncrklXF/4O/oY+FpTNUE6D2/RwBbV6ZsodYZre0ndtLULBh+HgWyn/k+L1HbIhtS/bnt4PZpM2i9zM1LjMNPxCgZWAqJ1vZnfEQxUORcE3Sqj/So5KZC0JoHOsvtT/l9/+BsArKI1QdXplW3XE6M9TwxsMG8hclo+GqVxR4KIleAULliPoF9xYWEBeMxT/FGVgkDA7TJLqNzVDOQpjLc37YS4rGijMSSqBNaFNGdAqNdjFTYkodrjNe9CUzVIojVJezQcfYG92Sal2vECO81cIdUEo12zKm7Vz5wbl6NRR6Wwqj8oBh37E+auo6chAgcHA0jQIDAQAB"; + publicKeyStr_Hotel_release = publicKeyStr_Hotel; BoxSharedParameters.getInstance().setNewTvPass(true); } diff --git a/app/src/main/java/com/pukka/jiecheng/utils/CloudKeyCodeUtils.java b/app/src/main/java/com/pukka/jiecheng/utils/CloudKeyCodeUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/ColorSpanUtils.java b/app/src/main/java/com/pukka/jiecheng/utils/ColorSpanUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/ColorUtil.java b/app/src/main/java/com/pukka/jiecheng/utils/ColorUtil.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/CommonLoadingDialog.java b/app/src/main/java/com/pukka/jiecheng/utils/CommonLoadingDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/CrashHandler.java b/app/src/main/java/com/pukka/jiecheng/utils/CrashHandler.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/DataValueUtils.java b/app/src/main/java/com/pukka/jiecheng/utils/DataValueUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/GridSpacingItemDecoration.java b/app/src/main/java/com/pukka/jiecheng/utils/GridSpacingItemDecoration.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/IsLoginTimerUtil.java b/app/src/main/java/com/pukka/jiecheng/utils/IsLoginTimerUtil.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/ItemOnClickUtils.java b/app/src/main/java/com/pukka/jiecheng/utils/ItemOnClickUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/JetsenxSdkUtil.java b/app/src/main/java/com/pukka/jiecheng/utils/JetsenxSdkUtil.java old mode 100755 new mode 100644 index 56396bb..e9d4e3b --- a/app/src/main/java/com/pukka/jiecheng/utils/JetsenxSdkUtil.java +++ b/app/src/main/java/com/pukka/jiecheng/utils/JetsenxSdkUtil.java @@ -1,7 +1,7 @@ package com.pukka.jiecheng.utils; import android.content.Context; -import android.os.Handler; +import android.os.Build; import android.text.TextUtils; import com.jetsenxsdk.api.JetsenxSdk; @@ -34,49 +34,40 @@ import java.util.Map; * @since V0.1.0.1 */ public class JetsenxSdkUtil { - public static String AGCODE = "1100101010208919998410060"; + public static String AGCODE = "1100101010200492358120169"; public static String TENANT = "huashitv"; - public static String ACCESSER = "JSTPKTEST"; - public static String SECRET = "utudfD9gVU94r52D"; + public static String ACCESSER = "JSTAS51"; + public static String SECRET = "oam25ZqvGy3gVY0x"; private static final String TAG = "JetsenxSdkUtil"; - private static Handler orderTimeOutHandler = new Handler(); + // 显式绑定主线程 Looper:本类可能被后台线程(如 JsapiOrderMonitor.checkJob 的 Dispatchers.IO、 + // AsHeartBeatManager 心跳的 DispatchQueue.global)首次触发类初始化,无参 new Handler() 会取当前线程 + // 的 Looper(后台线程为 null)抛 "Can't create handler inside thread that has not called Looper.prepare()"。 +// private static Handler orderTimeOutHandler = new Handler(Looper.getMainLooper()); private static Thread thread; - private static String productCodeSp; + /** 橘视厅5.1环绕声业务产品包编码(官方渠道参数下发,直接使用,避免 getAllAssetPrices 取包不确定) */ + private static final String PRODUCT_CODE_51 = "2100130010166202685000333"; - private static String sUid = ""; + private static String productCodeSp = PRODUCT_CODE_51; + + // volatile:sUid 由后台线程(checkBenefits 回调 / JsapiOrderMonitor)写入、 + // 主线程读取,保证跨线程可见,避免读到旧空值 + private static volatile String sUid = ""; + + /** 兜底请求最小间隔:3 秒内只触发一次 /api/jst/checkBenefits */ + private static final long FALLBACK_THROTTLE_MS = 3000L; /** - * 第三方变体(thirdparty)EPG 传入的 MAC 覆盖。 + * 上次发起兜底请求的时间戳(System.currentTimeMillis(),0 表示从未触发)。 * - * 背景:EPG 在后端用该 MAC 注册设备并下发卡密(thirdCardNo),SDK init 必须用同一 MAC - * 才能与后端权益绑定匹配;若回退 MacUtils.getMac 本机自采集 MAC,会导致权益不生效。 - * 为空时回退本机自采集(原版行为不变)。 - * 进程级静态:ThirdPartyEntryActivity 解析 Intent extras 时设置。 + * 3 秒节流:VideoDetailActivity onResume、MainActivity 启动检查等高频入口 + * 在窗口期内只放行一次请求,避免 fallback 变成轮询;窗口期外允许重试, + * 网络瞬时失败无需杀进程即可恢复。仅放行方推进时间戳,被挡调用不影响窗口。 */ - private static volatile String sMacOverride = ""; - - /** - * 设置 SDK init 的 MAC 覆盖值(EPG 传入的 mac 优先)。 - * 传空字符串/null 清除覆盖,恢复本机自采集。 - */ - public static void setMacOverride(String mac) { - String normalized = mac == null ? "" : mac.trim(); - if (normalized.equals(sMacOverride)) { - return; - } - sMacOverride = normalized; - LogUtils.d(TAG, "setMacOverride: " + (TextUtils.isEmpty(normalized) ? "" : normalized)); - } - - /** - * 读取 SDK init 的 MAC 覆盖值(空串表示未设置)。 - */ - public static String getMacOverride() { - return sMacOverride; - } + private static final java.util.concurrent.atomic.AtomicLong sLastFallbackAt = + new java.util.concurrent.atomic.AtomicLong(0L); /** * 标记当前是否有 re-init 正在进行,保证 globalInit 串行执行 @@ -92,7 +83,25 @@ public class JetsenxSdkUtil { public interface InitMemberCodeCallback { void onSuccess(String memberCode); - void onFail(String message); + // 状态码在前;0 表示无 SDK 错误码(缺标识 / 同步异常等内部场景),非 0 为 SDK 返回的错误码 + void onFail(int errorCode, String message); + } + + /** + * 将 Jetsenx SDK onFail 回调返回的字符串错误码转换为 int。 + * + *

SDK 内部 JetsenxInitCallback#onFail(String errorCode, String message) 第一参 + * 是错误码字符串,非数字格式或为空时返回 0 表示"未知错误码"。

+ */ + private static int parseErrorCode(String code) { + if (TextUtils.isEmpty(code)) { + return 0; + } + try { + return Integer.parseInt(code); + } catch (NumberFormatException e) { + return 0; + } } /** @@ -129,15 +138,17 @@ public class JetsenxSdkUtil { /** * 全局初始化方法,复用已缓存的 uid * + *

sUid 为空时不直接失败,而是经 {@link #recoverUidAndInit} 做一次进程级兜底恢复 + * (/api/jst/checkBenefits 拉取卡密);恢复成功则继续正常初始化,失败(无权益/网络异常) + * 统一映射为 {@link InitMemberCodeCallback#onFail}。

+ * * @param context 上下文 * @param callback 初始化结果回调 */ public static void globalInit(Context context, InitMemberCodeCallback callback) { if (TextUtils.isEmpty(sUid)) { - LogUtils.d(TAG, "globalInit skipped: no verified uid cached in current process"); - if (callback != null) { - callback.onFail("缺少可复用的SDK初始化标识"); - } + LogUtils.d(TAG, "globalInit: no cached uid, falling back to checkBenefits recovery"); + recoverUidAndInit(context, toFallbackCallback(callback)); return; } synchronized (sPendingReInitQueue) { @@ -166,11 +177,11 @@ public class JetsenxSdkUtil { } @Override - public void onFail(String message) { + public void onFail(int errorCode, String message) { dispatchNextReInit(); if (callback != null) { try { - callback.onFail(message); + callback.onFail(errorCode, message); } catch (Exception e) { LogUtils.e(TAG, "globalInit onFail callback exception: " + e.getMessage()); } @@ -183,7 +194,8 @@ public class JetsenxSdkUtil { dispatchNextReInit(); if (callback != null) { try { - callback.onFail("SDK初始化调用异常: " + e.getMessage()); + // -1: SDK init 同步异常场景,无 SDK 错误码可用 + callback.onFail(-1, "SDK初始化调用异常: " + e.getMessage()); } catch (Exception ce) { LogUtils.e(TAG, "globalInit onFail callback exception: " + ce.getMessage()); } @@ -235,6 +247,204 @@ public class JetsenxSdkUtil { return sUid; } + /** + * 带兜底恢复的 SDK 全局初始化。 + * + * 流程: + * 1. 若 sUid 非空,直接走 [globalInit](已有串行化与重入队列保护),onSuccess → onReady,onFail → onNetworkError。 + * 2. 若 sUid 为空: + * a. 首次调用:通过 [AsApiRequester.requestAsCheckBenefitsApi] 拉一次 /api/jst/checkBenefits + * (含 sign 缺失自动首签流程,沿用 checkBenefits 既有行为)。 + * b. 业务码 10200(Activated) 且 thirdCardNo 非空 → cacheReusableUid + globalInit。 + * c. 业务码 10900(CodeActivated) 且 thirdCardNo 非空 → cacheReusableUid + globalInit(副卡也支持回放)。 + * d. 业务码 10300/10400/10500/10600/10700/10800 → onNoEntitlement。 + * e. 网络异常 / 解析失败 / 签名缺失 → onNetworkError。 + * f. 业务码 Activated 但 thirdCardNo 为空 → onNoEntitlement(不写入空卡密)。 + * 3. 3 秒节流:窗口期内的高频调用直接 onNoEntitlement(0, "权益信息为空"), + * 不发请求也不走"网络异常"分支,避免覆盖真实业务状态; + * 窗口期外允许重试,网络瞬时失败无需杀进程即可恢复。 + * + * 与 [globalInit] 的差异:兜底逻辑(recoverUidAndInit)已由二者共用, + * 差异仅在回调形态——本方法为三态回调,可区分"无权益"与"网络异常"; + * globalInit 为二态回调,兜底失败统一映射 onFail。 + * 回调一定在调用线程(主线程或业务线程)上触发(沿用 AppScope 调度)。 + * + * @param context 上下文(Application 优先,避免 Activity 泄漏) + * @param callback 三态回调;callback.onReady 等价 globalInit.onSuccess, + * callback.onNetworkError 等价 globalInit.onFail, + * callback.onNoEntitlement 是本方法新增的分支 + */ + public static void ensureGlobalInitAsync(Context context, FallbackInitCallback callback) { + if (context == null) { + if (callback != null) { + callback.onNetworkError(1003, "context is null (stale Activity?)"); + } + return; + } + final Context appContext = context.getApplicationContext(); + // 第 1 步:sUid 非空 → 直接复用 globalInit + if (!TextUtils.isEmpty(sUid)) { + globalInit(appContext, new InitMemberCodeCallback() { + @Override + public void onSuccess(String memberCode) { + if (callback != null) callback.onReady(memberCode); + } + @Override + public void onFail(int errorCode, String message) { + // globalInit 自身失败归类为网络/SDK 异常(权益状态未知) + if (callback != null) callback.onNetworkError(errorCode, message); + } + }); + return; + } + recoverUidAndInit(appContext, callback); + } + + /** + * 三态回调到二态回调的适配:globalInit 无 sUid 时走兜底恢复, + * onReady 等价 onSuccess,onNoEntitlement / onNetworkError 统一映射 onFail。 + */ + static FallbackInitCallback toFallbackCallback(InitMemberCodeCallback callback) { + return new FallbackInitCallback() { + @Override + public void onReady(String memberCode) { + if (callback != null) callback.onSuccess(memberCode); + } + + @Override + public void onNoEntitlement(int businessCode, String message) { + if (callback != null) callback.onFail(businessCode, message); + } + + @Override + public void onNetworkError(int businessCode, String message) { + if (callback != null) callback.onFail(businessCode, message); + } + }; + } + + /** + * 节流闸门:距上次放行不足 {@link #FALLBACK_THROTTLE_MS} 时拒绝,且不推进时间戳。 + * CAS 保证并发下只有一个赢家。 + * + * @param nowMillis 当前时间戳(System.currentTimeMillis()),注入参数便于单测 + */ + static boolean tryAcquireFallback(long nowMillis) { + long last = sLastFallbackAt.get(); + if (nowMillis - last < FALLBACK_THROTTLE_MS) { + return false; + } + return sLastFallbackAt.compareAndSet(last, nowMillis); + } + + /** + * sUid 为空时的兜底恢复:/api/jst/checkBenefits 拉取卡密,3 秒节流。 + * + *

恢复成功后 cacheReusableUid 并回到 globalInit 正常串行初始化——此时 sUid 已非空, + * 不会再进入本方法;失败路径回调后即终止,节流窗口内的高频重入也会被闸门挡下, + * 调用图为单向无递归。供 globalInit 与 ensureGlobalInitAsync 共用,避免两套逻辑分叉。

+ */ + private static void recoverUidAndInit(Context context, FallbackInitCallback callback) { + if (context == null) { + if (callback != null) { + callback.onNetworkError(1003, "context is null (stale Activity?)"); + } + return; + } + final Context appContext = context.getApplicationContext(); + // 3 秒节流:窗口期内的高频入口直接挡下,不发请求。 + // 命中时视为"权益信息未知"(无权益)走 onNoEntitlement,而不是 onNetworkError, + // 避免 UI 端把"无可用卡密"误显示为"错误码:0 网络异常,请重试"。 + if (!tryAcquireFallback(System.currentTimeMillis())) { + LogUtils.d(TAG, "recoverUidAndInit skipped: throttled within " + FALLBACK_THROTTLE_MS + "ms"); + // 必须仍 fire callback,否则上层闸门(VideoDetailActivity.mPlayGateInFlight)永远不被清位, + // 后续 fullVideo() 直接被闸门挡下无法播放。 + if (callback != null) { + callback.onNoEntitlement(0, "权益信息为空(请求频繁)"); + } + return; + } + LogUtils.d(TAG, "recoverUidAndInit: sUid empty, fetching via /api/jst/checkBenefits"); + + // 第 3 步:sUid 为空 + 首次 → 异步拉 checkBenefits + // 10200 经 onActivated、10900 经 onCodeActivated 回调,result.code 保留真实业务码, + // result.thirdCardNo 为卡密,二者统一交给 handleCheckBenefitsForFallback 处理。 + com.pukka.jiecheng.request.AsApiRequester.requestAsCheckBenefitsApi(appContext, new com.pukka.jiecheng.request.AsApiRequester.CheckStatusCallback() { + @Override + public void onActivated(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) { + handleCheckBenefitsForFallback(result, appContext, callback); + } + @Override + public void onCodeActivated(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) { + handleCheckBenefitsForFallback(result, appContext, callback); + } + @Override + public void onPreset(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) { + if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_PRESET, "设备未激活"); + } + @Override + public void onActivating(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) { + if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_ACTIVATING, "权益激活中"); + } + @Override + public void onActivateFailed(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) { + if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_ACTIVATE_FAIL, "激活失败"); + } + @Override + public void onUnbound(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) { + if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_UNBOUND, "设备未绑定"); + } + @Override + public void onExpired(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) { + if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_EXPIRED, "权益已过期"); + } + @Override + public void onOccupied(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) { + if (callback != null) callback.onNoEntitlement(com.pukka.jiecheng.request.AsApiRequester.CHECK_STATUS_OCCUPIED, "权益已被占用"); + } + @Override + public void onUnknown(com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result) { + if (callback != null) callback.onNoEntitlement(result != null ? result.code : 0, "权益状态未知"); + } + @Override + public void onFail(int errorCode, String message) { + if (callback != null) callback.onNetworkError(errorCode, message); + } + }); + } + + /** + * 把 Activated/CodeActivated 响应归一化为 fallback 写入分支。 + * + * 关键不变量:仅当 thirdCardNo 非空时才 cacheReusableUid,避免写入空串。 + * Activated 但 thirdCardNo 为空 → 视为 NO_ENTITLEMENT(与 MainActivity.onActivated + * 对 home 版的处理一致:见 MainActivity.java:1047-1055)。 + */ + private static void handleCheckBenefitsForFallback( + com.pukka.jiecheng.request.AsApiRequester.CheckBenefitsResult result, + Context appContext, + FallbackInitCallback callback + ) { + if (result == null || TextUtils.isEmpty(result.thirdCardNo)) { + LogUtils.d(TAG, "ensureGlobalInitAsync: activated but thirdCardNo empty, treat as no entitlement"); + // 透传真实业务码(如 10900),供家用版调用方区分"激活码已激活但无权益码"场景 + if (callback != null) callback.onNoEntitlement(result == null ? 0 : result.code, "权益信息为空"); + return; + } + LogUtils.d(TAG, "ensureGlobalInitAsync: checkBenefits returned activated, caching uid and globalInit"); + cacheReusableUid(result.thirdCardNo); + globalInit(appContext, new InitMemberCodeCallback() { + @Override + public void onSuccess(String memberCode) { + if (callback != null) callback.onReady(memberCode); + } + @Override + public void onFail(int errorCode, String message) { + if (callback != null) callback.onNetworkError(errorCode, message); + } + }); + } + public static void clearReusableUid() { sUid = ""; LogUtils.d(TAG, "clearReusableUid: sUid cleared"); @@ -248,9 +458,7 @@ public class JetsenxSdkUtil { * @param callback 初始化结果回调 */ public static void init(Context context, String uid, InitMemberCodeCallback callback) { - // EPG 传入的 MAC 覆盖优先(thirdparty 变体):后端权益以 EPG 注册的 MAC 绑定, - // 本机自采集 MAC 与之不匹配会导致 SDK 鉴权失败、权益不生效 - String mac = TextUtils.isEmpty(sMacOverride) ? MacUtils.getMac(context) : sMacOverride; + String mac = MacUtils.getMac(context); String utype = "2"; if (!TextUtils.isEmpty(uid) && uid.matches("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")) { mac = uid; @@ -288,7 +496,7 @@ public class JetsenxSdkUtil { LogUtils.d("onFail s=" + s); LogUtils.d("onFail s1=" + s1); if (callback != null) { - callback.onFail(StringUtils.isEmpty(s1) ? "SDK初始化失败" : s1); + callback.onFail(parseErrorCode(s), StringUtils.isEmpty(s1) ? "SDK初始化失败" : s1); } } }); @@ -566,7 +774,8 @@ public class JetsenxSdkUtil { playCodeBean.setSpecCode(info.specCode); PlayRecordManager.playCodeMaps.put(720,playCodeBean); }else if (info.getFormat().contains("1080") - &&TextUtils.isEmpty(videoUrl_1080)){ + &&TextUtils.isEmpty(videoUrl_1080) + &&!Surround51Manager.isSurroundFormat(info.getFormat())){ videoUrl_1080 = info.getVideoUrl(); PlayCodeBean playCodeBean = new PlayCodeBean(); playCodeBean.setMovieCode(info.movieCode); @@ -575,6 +784,23 @@ public class JetsenxSdkUtil { PlayRecordManager.playCodeMaps.put(1080,playCodeBean); } } + // 5.1 环绕声独立档(format='1080P 5.1 环绕声',H265+AC3_51,单AC3音轨无2.0兜底): + // 开关开启时作为 1080 档播放地址,输出路径由播放器按设备能力自动决策 + // (AC3直通 > 平台硬解 > FFmpeg软解自动下混);开关关闭时主循环已排除 5.1 档,走普通 2.0 档。 + if (Surround51Manager.is51Preferred()) { + for (VideoInfo info : videoInfos) { + if (Surround51Manager.isSurroundFormat(info.getFormat())) { + videoUrl_1080 = info.getVideoUrl(); + PlayCodeBean playCodeBean = new PlayCodeBean(); + playCodeBean.setMovieCode(info.movieCode); + playCodeBean.setProgramCode(info.progCode); + playCodeBean.setSpecCode(info.specCode); + PlayRecordManager.playCodeMaps.put(1080, playCodeBean); + LogUtils.d(TAG, "videoUrl_51 applied:" + videoUrl_1080); + break; + } + } + } LogUtils.d(TAG, "videoUrl_1080:" + videoUrl_1080); LogUtils.d(TAG, "videoUrl_720:" + videoUrl_720); if (null != onGetAllVideoUrlCallback) { @@ -614,7 +840,8 @@ public class JetsenxSdkUtil { playCodeBean.setSpecCode(info.specCode); PlayRecordManager.playCodeMaps.put(720,playCodeBean); }else if (info.getFormat().contains("1080") - &&TextUtils.isEmpty(videoUrl_1080)){ + &&TextUtils.isEmpty(videoUrl_1080) + &&!Surround51Manager.isSurroundFormat(info.getFormat())){ videoUrl_1080 = info.getVideoUrl(); PlayCodeBean playCodeBean = new PlayCodeBean(); playCodeBean.setMovieCode(info.movieCode); @@ -623,6 +850,23 @@ public class JetsenxSdkUtil { PlayRecordManager.playCodeMaps.put(1080,playCodeBean); } } + // 5.1 环绕声独立档(format='1080P 5.1 环绕声',H265+AC3_51,单AC3音轨无2.0兜底): + // 开关开启时作为 1080 档播放地址,输出路径由播放器按设备能力自动决策 + // (AC3直通 > 平台硬解 > FFmpeg软解自动下混);开关关闭时主循环已排除 5.1 档,走普通 2.0 档。 + if (Surround51Manager.is51Preferred()) { + for (VideoInfo info : videoInfos) { + if (Surround51Manager.isSurroundFormat(info.getFormat())) { + videoUrl_1080 = info.getVideoUrl(); + PlayCodeBean playCodeBean = new PlayCodeBean(); + playCodeBean.setMovieCode(info.movieCode); + playCodeBean.setProgramCode(info.progCode); + playCodeBean.setSpecCode(info.specCode); + PlayRecordManager.playCodeMaps.put(1080, playCodeBean); + LogUtils.d(TAG, "videoUrl_51 applied:" + videoUrl_1080); + break; + } + } + } LogUtils.d(TAG, "videoUrl_1080:" + videoUrl_1080); LogUtils.d(TAG, "videoUrl_720:" + videoUrl_720); if (null != onGetAllVideoUrlCallback) { @@ -653,4 +897,38 @@ public class JetsenxSdkUtil { void onErr(String code, String msg); } + /** + * sUid 兜底恢复的回调接口。 + * + * 三个分支语义明确分离: + * - onReady: sUid 已恢复,SDK 初始化成功,调用方可以继续走原有业务流(如 getAllVideoUrl)。 + * - onNoEntitlement: 服务端明确说无权益(Preset/Activating/ActivateFailed/Unbound/Expired/Occupied + * 等业务码,或 Activated 但 thirdCardNo 为空),调用方应走"请购买/请激活"分支。 + * - onNetworkError: 网络/解析异常或签名失败,调用方应展示"网络异常请重试",**不能**误判为未激活。 + * + * 设计依据:见 docs/superpowers/plans/2026-09-14-suid-fallback-recovery.md 第 3 节。 + */ + public interface FallbackInitCallback { + /** sUid 已恢复,SDK 初始化成功(memberCode 与 globalInit 一致) */ + void onReady(String memberCode); + + /** + * 服务端确认无权益或当前卡密不可用。 + * @param businessCode 服务端业务码(10700/10800/10600/10500/10400/10300 等), + * 或 Activated/CodeActivated 响应的真实业务码(如 10900)—— + * 当 Activated 但 {@code thirdCardNo} 为空时, + * {@code handleCheckBenefitsForFallback} 早退并提示"权益信息为空"。 + * {@code 0} 为合成哨兵码,仅在 result 为 null 时使用(非真实业务码)。 + * @param message 给调用方的提示文案,可直接展示给用户 + */ + void onNoEntitlement(int businessCode, String message); + + /** + * 网络异常、解析失败或签名失败——权益状态未知。 + * 业务码约定:1004 = 网络/解析异常;1001 = 签名缺失;1003 = context null; + * 0 = 本地合成码(3 秒节流窗口内被挡下,提示"请求过于频繁,请稍后重试") + */ + void onNetworkError(int businessCode, String message); + } + } diff --git a/app/src/main/java/com/pukka/jiecheng/utils/MgRoute.java b/app/src/main/java/com/pukka/jiecheng/utils/MgRoute.java old mode 100755 new mode 100644 index bb263b6..34e36ca --- a/app/src/main/java/com/pukka/jiecheng/utils/MgRoute.java +++ b/app/src/main/java/com/pukka/jiecheng/utils/MgRoute.java @@ -16,6 +16,7 @@ import com.pukka.jiecheng.index.IndexAmberManager; import com.pukka.jiecheng.list.ListActivity; import com.pukka.jiecheng.login.LoginActivity; import com.pukka.jiecheng.order.OrderActivity; +import com.pukka.jiecheng.setting.SettingActivity; import com.pukka.jiecheng.search.SearchActivity; import com.pukka.jiecheng.video.VideoDetailActivity; import com.pukka.jiecheng.web.SimpleWebActivity; @@ -82,7 +83,6 @@ public class MgRoute { // return; // } Intent intent = new Intent(context, OrderActivity.class); - intent.putExtra(OrderActivity.go_type_param,go_type); context.startActivity(intent); } @@ -104,14 +104,8 @@ public class MgRoute { * 跳转到设置面 */ public static void go2Setting(Context context) { -// Intent intent = new Intent(context, MainWebActivity.class); -// Bundle bundle = new Bundle(); -// StringBuilder sbUrl = new StringBuilder(); -// sbUrl.append(BaseUrl.getSingleton().getBaseEpgHttpUrl()); -// sbUrl.append(MgRoute.urlSetting); -// bundle.putString("url",sbUrl.toString()); -// intent.putExtras(bundle); -// context.startActivity(intent); + Intent intent = new Intent(context, SettingActivity.class); + context.startActivity(intent); } /** diff --git a/app/src/main/java/com/pukka/jiecheng/utils/NewRecordCollManager.java b/app/src/main/java/com/pukka/jiecheng/utils/NewRecordCollManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/NextPageManger.java b/app/src/main/java/com/pukka/jiecheng/utils/NextPageManger.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/PageNumberUtil.java b/app/src/main/java/com/pukka/jiecheng/utils/PageNumberUtil.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/PaginationUtil.java b/app/src/main/java/com/pukka/jiecheng/utils/PaginationUtil.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/PayResultTimerUtil.java b/app/src/main/java/com/pukka/jiecheng/utils/PayResultTimerUtil.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/QRCodeUtil.java b/app/src/main/java/com/pukka/jiecheng/utils/QRCodeUtil.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/QRCodeWriter.java b/app/src/main/java/com/pukka/jiecheng/utils/QRCodeWriter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/ResUtil.java b/app/src/main/java/com/pukka/jiecheng/utils/ResUtil.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/RsaSignUtil.java b/app/src/main/java/com/pukka/jiecheng/utils/RsaSignUtil.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/Surround51Manager.java b/app/src/main/java/com/pukka/jiecheng/utils/Surround51Manager.java new file mode 100644 index 0000000..75beb8c --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/utils/Surround51Manager.java @@ -0,0 +1,121 @@ +package com.pukka.jiecheng.utils; + +import android.content.Context; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.os.Build; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.pukka.libcore.utils.LogUtils; +import com.pukka.libcore.utils.SharedPreferenceUtils; + +/** + * 5.1 环绕声能力检测与播放开关。 + *

开关(默认开启)关闭时不选 5.1 独立档(H265+AC3_51),播放普通 2.0 档; + * 开启时按设备能力自动选择输出路径:AC3直通(HDMI bitstream) > 平台硬解(6ch LPCM) > FFmpeg软解(下混 2.0)。

+ *

CDN 侧 5.1 档为单 AC3 音轨流(无 2.0 兜底音轨),故能力不足或开关关闭时必须回退普通档 URL。

+ */ +public final class Surround51Manager { + private static final String TAG = "Surround51Manager"; + /** SP 键:5.1 环绕声播放开关(设备信息页可切换) */ + private static final String SP_KEY_51_ENABLED = "Surround51Enabled"; + + private static volatile Boolean sAc3Passthrough; + private static volatile Boolean sPlatformAc3Decoder; + + private Surround51Manager() { + } + + /** 5.1 环绕声播放开关,默认开启 */ + public static boolean is51Enabled() { + return SharedPreferenceUtils.getBoolean(SP_KEY_51_ENABLED, true); + } + + public static void set51Enabled(boolean enabled) { + SharedPreferenceUtils.put(SP_KEY_51_ENABLED, enabled); + clearCache(); + } + + /** 清除静态缓存(开关切换后需重新检测) */ + public static void clearCache() { + sAc3Passthrough = null; + sPlatformAc3Decoder = null; + } + + /** + * 是否优先选用 5.1 独立档:开关开启 且 Android 5.1(API 22) 以上。 + * API 22 及以下设备解码能力有限,固定走普通 2.0 档。 + */ + public static boolean is51Preferred() { + return is51Enabled() && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1; + } + + /** 判断 SDK 返回档位是否 5.1 环绕声档(format='1080P 5.1 环绕声') */ + public static boolean isSurroundFormat(String format) { + String fmt = format == null ? "" : format; + return fmt.contains("5.1") || fmt.contains("环绕声"); + } + + /** + * 设备是否支持 AC3 源码直通(HDMI/SPDIF bitstream)。 + * 结果受系统编码环绕声设置(encoded_surround_output)与当前接入的音频设备能力影响。 + */ + public static boolean supportsAc3Passthrough(Context context) { + if (sAc3Passthrough == null) { + try { + sAc3Passthrough = AudioCapabilities.getCapabilities(context) + .supportsEncoding(C.ENCODING_AC3); + } catch (Throwable e) { + LogUtils.d(TAG, "supportsAc3Passthrough err:" + e); + sAc3Passthrough = false; + } + } + return sAc3Passthrough; + } + + /** 平台是否自带 AC3 硬解码器(如带 Dolby/MS12 授权的芯片),硬解输出 6ch LPCM 由系统按输出能力下混 */ + public static boolean hasPlatformAc3Decoder() { + if (sPlatformAc3Decoder == null) { + boolean found = false; + try { + for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) { + // v1.3.36: isAlias() 是 API 29+ 方法,API 28 及以下调用抛 NoSuchMethodError + //(腾讯T5/Android 9 实测),导致整个循环被 catch 中断、检测永远为"无解码器"。 + if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && info.isAlias()) + || info.isEncoder()) { + continue; + } + for (String type : info.getSupportedTypes()) { + if ("audio/ac3".equalsIgnoreCase(type)) { + found = true; + break; + } + } + if (found) { + break; + } + } + } catch (Throwable e) { + LogUtils.d(TAG, "hasPlatformAc3Decoder err:" + e); + found = false; + } + sPlatformAc3Decoder = found; + } + return sPlatformAc3Decoder; + } + + /** 当前设备 5.1 音频输出路径描述(设备信息页展示用) */ + public static String getAudioPathDesc(Context context) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { + return "不支持(系统版本过低)"; + } + if (supportsAc3Passthrough(context)) { + return "AC3源码直通"; + } + if (hasPlatformAc3Decoder()) { + return "硬解码"; + } + return "软解"; + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/utils/ToastUtils.java b/app/src/main/java/com/pukka/jiecheng/utils/ToastUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/TopStyleUtils.java b/app/src/main/java/com/pukka/jiecheng/utils/TopStyleUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/UiUtils.java b/app/src/main/java/com/pukka/jiecheng/utils/UiUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/UpdateDialog.java b/app/src/main/java/com/pukka/jiecheng/utils/UpdateDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/UpdateOtherDialog.java b/app/src/main/java/com/pukka/jiecheng/utils/UpdateOtherDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/UrlParamBuilder.java b/app/src/main/java/com/pukka/jiecheng/utils/UrlParamBuilder.java old mode 100755 new mode 100644 index a967af1..107a7b4 --- a/app/src/main/java/com/pukka/jiecheng/utils/UrlParamBuilder.java +++ b/app/src/main/java/com/pukka/jiecheng/utils/UrlParamBuilder.java @@ -1,8 +1,8 @@ package com.pukka.jiecheng.utils; -import java.io.UnsupportedEncodingException; import java.util.Map; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; public class UrlParamBuilder { @@ -60,15 +60,9 @@ public class UrlParamBuilder { urlBuilder.append("&"); } - try { - urlBuilder.append(URLEncoder.encode(entry.getKey(), "UTF-8")) + urlBuilder.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)) .append("=") - .append(URLEncoder.encode(entry.getValue(), "UTF-8")); - } catch (UnsupportedEncodingException e) { - urlBuilder.append(entry.getKey()) - .append("=") - .append(entry.getValue()); - } + .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); } } diff --git a/app/src/main/java/com/pukka/jiecheng/utils/VideoContainerHelper.java b/app/src/main/java/com/pukka/jiecheng/utils/VideoContainerHelper.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/utils/XCrashInitManager.java b/app/src/main/java/com/pukka/jiecheng/utils/XCrashInitManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/VideoDetailActivity.java b/app/src/main/java/com/pukka/jiecheng/video/VideoDetailActivity.java old mode 100755 new mode 100644 index 41c9c22..5a207c4 --- a/app/src/main/java/com/pukka/jiecheng/video/VideoDetailActivity.java +++ b/app/src/main/java/com/pukka/jiecheng/video/VideoDetailActivity.java @@ -12,6 +12,7 @@ import static com.pukka.jiecheng.video.utils.SelectedProgramsManager.PROGRAM_TYP import static com.pukka.jiecheng.video.utils.VideoDetailStatusManager.getDefinitionUrl; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Build; @@ -56,11 +57,11 @@ import com.pukka.jiecheng.utils.DataValueUtils; import com.pukka.jiecheng.utils.JetsenxSdkUtil; import com.pukka.jiecheng.utils.NewRecordCollManager; import com.pukka.jiecheng.utils.ToastUtils; -import com.pukka.jiecheng.video.benefit.BenefitOrderCallback; -import com.pukka.jiecheng.video.benefit.BenefitOrderController; import com.pukka.jiecheng.video.amber.DetailAmberManager; import com.pukka.jiecheng.video.bean.ErrInfoBean; import com.pukka.jiecheng.video.bean.FindPlayrecordBean; +import com.pukka.jiecheng.video.benefit.BenefitOrderCallback; +import com.pukka.jiecheng.video.benefit.BenefitOrderController; import com.pukka.jiecheng.video.face.DetailInterface; import com.pukka.jiecheng.video.face.NavigationInterface; import com.pukka.jiecheng.video.face.PlayTheNextVideoInterface; @@ -87,6 +88,8 @@ import com.pukka.jiecheng.widget.QrCodeView; import com.pukka.libcore.boxShared.BoxSharedParamsUtils; import com.pukka.libcore.boxShared.MacUtils; import com.pukka.libcore.fresco.PkSimpleDraweeView; +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.SharedPreferenceUtils; @@ -97,6 +100,10 @@ import com.tencent.bugly.crashreport.BuglyLog; import org.json.JSONObject; import java.lang.ref.WeakReference; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; /** * 视频详情页面 @@ -205,6 +212,38 @@ public class VideoDetailActivity extends BaseActivity */ private boolean isOrder = false; private boolean isStarFullVideo = false; + /** + * 语音自动全屏标记:正常进入详情页(isFull=false,不走全屏启动链路), + * 详情数据加载完成后模拟点击全屏按钮进入全屏 + */ + private boolean isAutoFullScreen = false; + /** 自动全屏只触发一次,防止后续再次 onPrepared(换清晰度/异常重启)重复点击 */ + private boolean hasAutoFullScreenTriggered = false; + /** + * 【用户主动触发路径专用】家用版 10900 跳转 OrderActivity 的会话内一次性标志。 + */ + private boolean mUserClickRoutedToOrder10900 = false; + /** 播放闸门 500ms 防抖时间戳:单位 ms,0 表示尚未触发过 */ + private long mLastPlayGatePassTime = 0L; + /** 播放闸门防抖阈值:500ms 内只允许一次通过 */ + private static final long PLAY_GATE_DEBOUNCE_MS = 500L; + /** + * 服务端时间漂移阈值:服务端 nowtime 与系统时间差超过 24 小时视为异常, + * 在 fullVideo() 入口阻断并提示 showSdkTimeError。 + */ + private static final long TIME_DRIFT_THRESHOLD_MS = 24L * 60L * 60L * 1000L; + /** + * 详情接口响应中的服务端时间戳(getDetailData onResponse 写入)。 + * 用于 fullVideo() 闸门的时间漂移检查,避免重复解析或额外的网络请求。 + * 可能为空(接口失败 / 旧版本接口无 nowtime 字段),闸门按"未知"处理不阻断。 + */ + @Nullable + private String serverNowtime; + /** 自动全屏等待小窗起播的重试计数(起播初期播放进度为 0,会被 isCanGotoFullPlay 拦截) */ + private int autoFullScreenRetryTimes = 0; + /** 自动全屏等待起播的最大重试次数与间隔:20 次 × 500ms = 10s */ + private static final int AUTO_FULL_SCREEN_MAX_RETRY = 20; + private static final long AUTO_FULL_SCREEN_RETRY_INTERVAL = 500L; private View vLoadBg; /** @@ -333,17 +372,6 @@ public class VideoDetailActivity extends BaseActivity } } - private void registerGlobalFocusListener() { - View decorView = getWindow().getDecorView(); - if (decorView == null) { - return; - } - ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver(); - if (viewTreeObserver.isAlive()) { - viewTreeObserver.addOnGlobalFocusChangeListener(globalFocusChangeListener); - } - } - /** * 初始化 Handler 生命周期管理器 * 注册所有需要管理的 Handler @@ -378,6 +406,24 @@ public class VideoDetailActivity extends BaseActivity } public static void start(Context context, String contentId, String contentType, ModuleSourceType moduleSourceType, boolean isFull) { + start(context, contentId, contentType, moduleSourceType, isFull, false); + } + + /** + * 语音自动全屏播放入口(与 {@link #start(Context, String, String)} 同型): + * 正常进入详情页(不走 isStarFullVideo 全屏启动链路,无加载弹窗与黑屏遮罩), + * 详情数据加载完成后模拟点击全屏按钮({@code llFullScreen.performClick()}), + * 与真实点击走同一 {@link #fullVideo()} 链路(订购校验/试看/扫码拦截全部复用)。 + */ + public static void startAutoFullScreen(Context context, String contentId, String contentType) { + startAutoFullScreen(context, contentId, contentType, ModuleSourceType.HOME_PAGE); + } + + public static void startAutoFullScreen(Context context, String contentId, String contentType, ModuleSourceType moduleSourceType) { + start(context, contentId, contentType, moduleSourceType, false, true); + } + + private static void start(Context context, String contentId, String contentType, ModuleSourceType moduleSourceType, boolean isFull, boolean autoFullScreen) { HomeHttpUtils.getcontentlistDetail(contentId, new AijiaCallback.ContentListCallback() { @Override public void onResponse(ContentListBean response) { @@ -389,6 +435,20 @@ public class VideoDetailActivity extends BaseActivity starter.putExtra("contentType", contentType); starter.putExtra("moduleSourceType", moduleSourceType.getMessage()); starter.putExtra("isFull", isFull); + starter.putExtra("autoFullScreen", autoFullScreen); + // 调用方可能传入 Application Context(如语音指令路由 DefaultPageNavigator、 + // 静态广播接收器等),非 Activity Context 启动 Activity 必须加 NEW_TASK, + // 否则 Android 7.0+ 会抛 AndroidRuntimeException 或被静默忽略,导致无法跳转。 + // 必须叠加 CLEAR_TOP:语音自动播片把本页启动成了独立 task 的根, + // 若已存在以本页为根的 task 且正在前台,仅 NEW_TASK 会命中系统 + // "复用已有 task 拉回前台"语义——task 已在前台时表现为完全无反应 + // (新实例不创建、旧播放不退出)。加 CLEAR_TOP 后旧实例会被销毁 + // (正常走 onPause/onStop 释放播放器并保存播放记录)、以新 contentId + // 重建新实例,实现"退出旧播放 -> 刷新新详情并自动播放"。 + if (!(context instanceof Activity)) { + starter.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + } context.startActivity(starter); } } @@ -549,6 +609,7 @@ public class VideoDetailActivity extends BaseActivity contentType = getIntent().getStringExtra("contentType"); sourceType = getIntent().getStringExtra("moduleSourceType"); isStarFullVideo = getIntent().getBooleanExtra("isFull", false); + isAutoFullScreen = getIntent().getBooleanExtra("autoFullScreen", false); LogUtils.d("getcontentlistDetail contentId:" + contentId + ",contentType:" + contentType); LogUtils.d("getcontentlistDetail sourceType:" + sourceType); } @@ -598,9 +659,6 @@ public class VideoDetailActivity extends BaseActivity } ivOrder = findViewById(R.id.id_iv_order); llOrder = findViewById(R.id.id_ll_order); - if (AppConfiguration.getInstance().isThirdPartyVariant()) { - llOrder.setVisibility(View.GONE); - } llBack = findViewById(R.id.id_ll_back); ivBack = findViewById(R.id.id_iv_back); llFullScreen = findViewById(R.id.id_ll_fullScreen); @@ -710,9 +768,7 @@ public class VideoDetailActivity extends BaseActivity // 三阶段流程(create → status 轮询 → check 轮询);二维码渲染与 Toast 由回调驱动, // 不引入新的 Activity/Dialog,保持详情页 0 侵入。详见 docs/天卡-扫码激活业务流程.md §3。 if (qrCodeView != null) { - if (AppConfiguration.getInstance().isThirdPartyVariant()) { - qrCodeView.setVisibility(View.GONE); - } else if (isScanChannel()) { + if (isScanChannel()) { qrCodeView.setClickable(false); qrCodeView.setFocusable(false); qrCodeView.setOnFocusChangeListener(new View.OnFocusChangeListener() { @@ -851,7 +907,7 @@ public class VideoDetailActivity extends BaseActivity String selectedVideoUrl = getDefinitionUrl(videoUrl_1080, videoUrl_720); reportVideoStreamTracking(true, "0", "videoUrl_1080 : " + videoUrl_1080 + " videoUrl_720 : " + videoUrl_720+ " isFree : " + isFree); onGetVideUrlCallBack.onSuccessVideoUrl(selectedVideoUrl); - if (!BoxSharedParamsUtils.isJcHotel() && !AppConfiguration.getInstance().isThirdPartyVariant()) { + if (!BoxSharedParamsUtils.isJcHotel()) { llOrder.setVisibility(isFree ? View.GONE : View.VISIBLE); } if (!isFree) { @@ -891,16 +947,18 @@ public class VideoDetailActivity extends BaseActivity changeOrderBtn(false); videoDetailStatusManager.err(code, msg); } + } else if( "-100".equals(code.trim())){ + showSdkTimeError(-100); } else { - playRecordManager.extReport("error",code+"|"+msg); - if ("605".equals(code.trim())) { - String mac = MacUtils.getMac(ContextUtils.getApplicationContext()); - BuglyLog.e("VideoDetailActivity playVideo", mac + " onErr and Retry :" + msg); - reInitJetsenxSdk("play_video_error_605", code, msg); - } +// playRecordManager.extReport("error",code+"|"+msg); if (isStarFullVideo) { loadFullDialog.show(); } else { + if ("605".equals(code.trim())) { + String mac = MacUtils.getMac(ContextUtils.getApplicationContext()); + BuglyLog.e("VideoDetailActivity playVideo", mac + " onErr and Retry :" + msg); + reInitJetsenxSdk("play_video_error_605", code, msg); + } videoDetailStatusManager.err(code, msg); } } @@ -956,45 +1014,110 @@ public class VideoDetailActivity extends BaseActivity ); return; } - try { - JetsenxSdkUtil.globalInit(appContext, new JetsenxSdkUtil.InitMemberCodeCallback() { - @Override - public void onSuccess(String memberCode) { - reportSdkReInitTracking( - AsApiRequester.TRACKING_STAGE_SUCCESS, - "0", - "SDK重新初始化成功", - triggerReason, - sourceCode, - sourceMessage, - memberCode - ); - } + // 兜底链路:sUid 已缓存直接 globalInit;为空(进程被回收后冷启动)则经 + // /api/jst/checkBenefits 从服务端重拉一次卡密。三态分类见 FallbackInitCallback。 + JetsenxSdkUtil.ensureGlobalInitAsync(appContext, new JetsenxSdkUtil.FallbackInitCallback() { + @Override + public void onReady(String memberCode) { + reportSdkReInitTracking( + AsApiRequester.TRACKING_STAGE_SUCCESS, + "0", + "SDK重新初始化成功", + triggerReason, + sourceCode, + sourceMessage, + memberCode + ); + } - @Override - public void onFail(String message) { - reportSdkReInitTracking( - AsApiRequester.TRACKING_STAGE_FAIL, - "global_init_fail", - TextUtils.isEmpty(message) ? "SDK重新初始化失败" : message, - triggerReason, - sourceCode, - sourceMessage, - "" - ); + @Override + public void onNoEntitlement(int businessCode, String message) { + reportSdkReInitTracking( + AsApiRequester.TRACKING_STAGE_FAIL, + "no_entitlement_" + businessCode, + "SDK重新初始化无权益: " + message, + triggerReason, + sourceCode, + sourceMessage, + "" + ); + // ============================================================ + // 【SDK 触发路径】reInitJetsenxSdk —— 一律不跳 OrderActivity + // ============================================================ + // 触发源:playVideo.onErr(605) / judgeOrder.onErr(605),均为 SDK / 生命周期自动触发。 + // 业务约定:跳转 OrderActivity 必须是用户主动触发(fullVideo 用户点击 / 语音自动全屏), + // SDK / 生命周期路径禁止调用 tryUserClickHomeOrderJump,仅走试看 UI 或异常视图。 + // 与用户路径的"分流"对照: + // - 用户路径:handlePlayGateNoEntitlement → tryUserClickHomeOrderJump() → startActivity + // - SDK 路径:reInitJetsenxSdk(本方法) → judgeOrderNotPurchased / showEntitlementError + // ============================================================ + // 10200 激活但卡密为空:后端强要求该状态必配卡资源,属异常态, + // 直接展示异常,不引导订购也不误报未购买 + if (businessCode == AsApiRequester.CHECK_STATUS_ACTIVATED) { + runOnUiIfAlive(() -> showEntitlementError(businessCode, message)); + return; } - }); - } catch (Exception e) { - reportSdkReInitTracking( - AsApiRequester.TRACKING_STAGE_FAIL, - "invoke_exception", - e.getMessage(), - triggerReason, - sourceCode, - sourceMessage, - "" - ); - } + // 其他无权益码(home+10900 / scan+10900 / 10300-10800 等):走试看 UI + runOnUiIfAlive(() -> judgeOrderNotPurchased()); + } + + @Override + public void onNetworkError(int businessCode, String message) { + reportSdkReInitTracking( + AsApiRequester.TRACKING_STAGE_FAIL, + "network_error_" + businessCode, + TextUtils.isEmpty(message) ? "SDK重新初始化网络异常" : message, + triggerReason, + sourceCode, + sourceMessage, + "" + ); + // -100 为 SDK 时间校验错误,走专门的时间异常提示 + if (businessCode == -100) { + runOnUiIfAlive(() -> showSdkTimeError(-100)); + return; + } + // 网络异常不清空已有缓存(权益状态未知),走"网络异常请重试"提示 + runOnUiIfAlive(() -> showNetworkErrorRetry(businessCode)); + } + }); + } + + /** 静态工具回调可能晚于 Activity 销毁到达,统一做生命周期守卫后再切主线程执行 */ + private void runOnUiIfAlive(Runnable action) { + runOnUiThread(() -> { + if (!isFinishing() && !isDestroyed()) { + action.run(); + } + }); + } + + /** + * 兜底初始化判为无权益:走与 onResume judgeOrder 未购分支一致的 UI + * (置为未订购 + 停止播放 + 试看订购提示)。 + */ + private void judgeOrderNotPurchased() { + LogUtils.d(TAG, "judgeOrderNotPurchased: fallback classified as no entitlement"); + isOrder = false; + videoDetailStatusManager.stopVideo(); + videoDetailStatusManager.trySeeOrderMin(); + changeOrderBtn(false); + } + + /** 兜底初始化网络异常:展示"网络异常请重试"错误视图(不清空 sUid,权益状态未知) */ + private void showNetworkErrorRetry(int businessCode) { + LogUtils.d(TAG, "showNetworkErrorRetry: fallback network error, retry available"); + videoDetailStatusManager.err(String.valueOf(businessCode), "网络异常,请重试"); + } + + /** + * 10200 已激活但卡密为空:后端强要求该状态必配卡资源,属异常态, + * 直接展示异常错误视图,不引导订购也不误报未购买。 + */ + private void showEntitlementError(int businessCode, String message) { + LogUtils.d(TAG, "showEntitlementError: activated but thirdCardNo missing"); + videoDetailStatusManager.err(String.valueOf(businessCode), + TextUtils.isEmpty(message) ? "权益信息为空" : message); } /** @@ -1009,25 +1132,27 @@ public class VideoDetailActivity extends BaseActivity * @param thirdCardNo 新下发卡密,仅用于埋点上报(SDK 自身从 sUid 取) */ private void reInitJetsenxSdkForScan(String thirdCardNo) { - Context appContext = ContextUtils.getApplicationContext(); + final Context appContext = ContextUtils.getApplicationContext(); if (appContext == null) { reportScanInitTracking( AsApiRequester.TRACKING_STAGE_FAIL, "context_null", - "扫码初始化失败: application context is null", + "sdk初始化失败: application context is null", thirdCardNo, "" ); return; } + // globalInit 内部同步调用第三方 JetsenxSdk.init,主线程裸调存在 ANR 风险; + // 挪到全局线程池执行,回调本身已做主线程切换(triggerJudgeOrderAfterScanActivation 内部 runOnUiIfAlive)。 try { - JetsenxSdkUtil.globalInit(appContext, new JetsenxSdkUtil.InitMemberCodeCallback() { + DispatchQueue.global.async(() -> JetsenxSdkUtil.globalInit(appContext, new JetsenxSdkUtil.InitMemberCodeCallback() { @Override public void onSuccess(String memberCode) { reportScanInitTracking( AsApiRequester.TRACKING_STAGE_SUCCESS, "0", - "扫码初始化成功", + "sdk初始化成功", thirdCardNo, memberCode ); @@ -1039,11 +1164,11 @@ public class VideoDetailActivity extends BaseActivity } @Override - public void onFail(String message) { + public void onFail(int errorCode, String message) { reportScanInitTracking( AsApiRequester.TRACKING_STAGE_FAIL, "global_init_fail", - TextUtils.isEmpty(message) ? "扫码初始化失败" : message, + TextUtils.isEmpty(message) ? "sdk初始化失败" : message, thirdCardNo, "" ); @@ -1054,7 +1179,7 @@ public class VideoDetailActivity extends BaseActivity triggerJudgeOrderAfterScanActivation(); } } - }); + })); } catch (Exception e) { reportScanInitTracking( AsApiRequester.TRACKING_STAGE_FAIL, @@ -1186,6 +1311,8 @@ public class VideoDetailActivity extends BaseActivity String mac = MacUtils.getMac(ContextUtils.getApplicationContext()); BuglyLog.e("VideoDetailActivity judgeOrder", mac + " onErr and Retry :" + msg); reInitJetsenxSdk("judge_order_error_605", code, msg); + } else if( "-100".equals(code.trim())){ + showSdkTimeError(-100); } } }); @@ -1304,6 +1431,14 @@ public class VideoDetailActivity extends BaseActivity videoDetailStatusManager.setSpeed(onDemandEpisodesView.getCurSpeed()); } loadFullDialog.dismiss(); + // 播放画面已渲染,全屏启动模式下的黑色遮罩同步收起, + // 不再依赖 loadFullDialog.close()(用户主动关闭)路径 + if (isStarFullVideo) { + vLoadBg.setVisibility(View.GONE); + } + // 语音自动全屏:小窗起播渲染完成,模拟点击全屏按钮 + // (酒店版已在 updateView 触发过,此处因一次性标记自动跳过) + tryAutoFullScreen(); } @Override @@ -1312,6 +1447,11 @@ public class VideoDetailActivity extends BaseActivity if (handlerLifecycleManager != null) { handlerLifecycleManager.pauseAllHandlers(); } + // 播放失败兜底:关掉"正在加载中..."弹窗和遮罩,避免用户被永久卡死在加载态 + if (isStarFullVideo) { + vLoadBg.setVisibility(View.GONE); + } + loadFullDialog.dismiss(); playRecordManager.extReport("error",err); } @@ -1477,6 +1617,24 @@ public class VideoDetailActivity extends BaseActivity flStatus.requestFocus(); } + /** + * SDK 重新初始化系统时间异常的错误 + * + * 处理逻辑: + * 1. 隐藏封面图 flImg,避免错误提示被海报遮挡 + * 2. 在错误信息前拼上当前系统时间,便于现场定位问题发生时刻 + * 3. 委托给 videoDetailStatusManager.err 走标准错误展示流程 + * + * @param errorCode 错误码 + */ + private void showSdkTimeError(int errorCode) { + if (flImg != null) { + flImg.setVisibility(View.GONE); + } + String currentTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()); + videoDetailStatusManager.err(String.valueOf(errorCode), "时间异常,系统当前时间:" + currentTime); + } + private void hideErrView() { flStatus.removeAllViews(); flStatus.setVisibility(View.GONE); @@ -1585,7 +1743,7 @@ public class VideoDetailActivity extends BaseActivity if (!TextUtils.isEmpty(JetsenxSdkUtil.getSuid())) { LogUtils.d(TAG, "skip createOrder: sUid already cached, device already activated"); showAlreadyPurchasedUi(); - } else if (!AppConfiguration.getInstance().isThirdPartyVariant()) { + } else { initBenefitOrderController(resultEntity.getCode()); } } @@ -1737,6 +1895,9 @@ public class VideoDetailActivity extends BaseActivity }); addRecommendFragment(getRandomCatalogId(catalogIds), mContentData.getCode()); retentionDialog.initData(videoDetailStatusManager, selectedProgramsManager, getRandomCatalogId(catalogIds), mContentData); + // 语音自动全屏:详情数据加载完成,触发模拟点击全屏 + // (酒店版数据就绪即点击;普通版等小窗起播后再点击,见 tryAutoFullScreen) + tryAutoFullScreen(); } private String catalogIds = ""; @@ -1781,9 +1942,15 @@ public class VideoDetailActivity extends BaseActivity @Override protected void onResume() { super.onResume(); + if(AppConfiguration.getInstance().isHomeEdition()) { + // 每次恢复,都置false状态 + mUserClickRoutedToOrder10900 = false; + } if (mContentData != null) { DetailAmberManager.videoPageEnter(isStarFullVideo, "其他", getControllerTitle(), selectedProgramsManager.getSelectVideoCode(), mContentData.getCategoryname()); - if (llOrder.getVisibility() == View.VISIBLE) { + // 家用版从订购页返回时 llOrder 可能不可见(如全屏场景),仍需重判权益,接回购买成功后的状态恢复 + if (llOrder.getVisibility() == View.VISIBLE + || AppConfiguration.getInstance().isHomeEdition()) { judgeOrder(new DetailInterface.OnJudgeIsOrderCallBack() { @Override public void onJudgeIsOrder(boolean isOrderB, String url) { @@ -1866,6 +2033,8 @@ public class VideoDetailActivity extends BaseActivity return; } LogUtils.d("getcontentlistDetail onResponse:" + response.getNowtime()); + // 缓存服务端时间戳供 fullVideo 闸门做时间漂移检查(可能为空,闸门按未知处理) + serverNowtime = response.getNowtime(); if (response.getResult() == null || response.getResult().isEmpty()) { videoDetailStatusManager.err("0x0000", "数据错误"); showErrView("0x0000", "数据错误"); @@ -1909,19 +2078,52 @@ public class VideoDetailActivity extends BaseActivity } /** - * 视频全屏播放 + * 语音自动全屏:满足起播条件后模拟点击全屏按钮 {@code llFullScreen}, + * 与真实点击走完全相同的 {@link #fullVideo()} 链路(含订购校验/试看/扫码拦截及埋点)。 + * + *

触发时机差异: + *

    + *
  • 酒店版(isJcHotel):进入详情不自动起播(海报态),数据加载完成即可点击, + * fullVideo() 内部会重新拉流并直接 fullWindowPlay,无起播进度门槛;
  • + *
  • 普通版:小窗自动起播,需播放进度 > 0 才能通过 fullVideo() 内 + * isCanGotoFullPlay 的全屏门槛;起播初期进度可能仍为 0,故按 500ms 轮询重试, + * 超过最大次数放弃(等同用户未点击,不强制弹窗)。
  • + *
+ * 通过 {@link #hasAutoFullScreenTriggered} 保证全程只触发一次, + * 避免换清晰度、异常重启等再次 onPrepared 时重复点击。

+ */ + private void tryAutoFullScreen() { + if (!isAutoFullScreen || hasAutoFullScreenTriggered) { + return; + } + if (isFinishing() || isDestroyed()) { + return; + } + if (BoxSharedParamsUtils.isJcHotel() || videoDetailStatusManager.isCanGotoFullPlay()) { + hasAutoFullScreenTriggered = true; + LogUtils.d(TAG, "tryAutoFullScreen: simulate click llFullScreen"); + llFullScreen.performClick(); + return; + } + if (autoFullScreenRetryTimes < AUTO_FULL_SCREEN_MAX_RETRY) { + autoFullScreenRetryTimes++; + llFullScreen.postDelayed(this::tryAutoFullScreen, AUTO_FULL_SCREEN_RETRY_INTERVAL); + } + } + + /** + * 播放入口:先做 typeStatus / 扫码 QR 短路,再走业务闸门; + * 闸门通过后才进入真正的播放链路。 */ public void fullVideo() { LogUtils.d("fullVideo"); if (mContentData == null) { return; } - LogUtils.d("fullVideo1"); - if (videoDetailStatusManager.getTypeStatus() == 3) { - clickOrder(); - return; - } - LogUtils.d("fullVideo2"); +// if (videoDetailStatusManager.getTypeStatus() == 3) { +// clickOrder(); +// return; +// } // 扫码版变体:未购买权益时(小窗 / 全屏 / 海报封面的所有播放入口), // 点击播放统一弹居中二维码弹框,让用户先扫码购买再观看; // 弹框内的二维码复用底部 QrCodeView 已生成的位图,不重复请求。 @@ -1932,19 +2134,197 @@ public class VideoDetailActivity extends BaseActivity // 弹框无可用 codeUrl(create 还在进行中 / 网络失败)→ 落到下方原播放链路, // 由 VideoDetailStatusManager 的 typeStatus 控制真实播放行为 } - if (BoxSharedParamsUtils.isJcHotel()) { - LogUtils.d("fullVideo3"); - playVideo(new DetailInterface.OnGetVideUrlCallBack() { - @Override - public void onSuccessVideoUrl(String videoUrl) { - videoDetailStatusManager.setData(videoUrl, 0, true); - fullWindowPlay(); - } - }); - } else { - if (videoDetailStatusManager.isCanGotoFullPlay()) { - videoDetailStatusManager.fullWindowPlay(getControllerTitle(), selectedProgramsManager.getVideoDetailType() != PROGRAM_TYPE_FILM); + // 业务前置闸门:时间漂移 → sUid → checkBenefits → 业务码分支 + ensureEntitlementBeforePlay(); + } + + /** + * 播放前置闸门:服务端时间漂移 → sUid 命中直接走 SDK init → sUid 为空走 checkBenefits 三态。 + * 收敛原来分散在 reInitJetsenxSdk / judgeOrder / reInitJetsenxSdkForScan 的权益校验入口, + * 让 fullVideo 在不确定 sUid 状态时不再盲调 SDK。 + */ + private void ensureEntitlementBeforePlay() { + final long now = System.currentTimeMillis(); + if (now - mLastPlayGatePassTime < PLAY_GATE_DEBOUNCE_MS) { + return; + } + if (isFinishing() || isDestroyed()) { + return; + } + LogUtils.i("ensureEntitlementBeforePlay >>>>>>>>>>>"); + mLastPlayGatePassTime = now; + + if (isServerTimeDrifted(serverNowtime)) { + LogUtils.w(TAG, "play gate: server time drifted > 24h, block play"); + showSdkTimeError(-100); + return; + } + + // 第 2 步:sUid 已缓存 → 直接走 SDK init + 后续播放 + // globalInit 内部同步调用第三方 JetsenxSdk.init(鉴权签名/状态写入等),主线程裸调 + // 存在 ANR 风险;挪到全局线程池执行,回调本身已通过 runOnUiIfAlive 切回主线程。 + if (!TextUtils.isEmpty(JetsenxSdkUtil.getSuid())) { + final Context appContext = ContextUtils.getApplicationContext(); + DispatchQueue.global.async(() -> JetsenxSdkUtil.globalInit(appContext, + new JetsenxSdkUtil.InitMemberCodeCallback() { + @Override + public void onSuccess(String memberCode) { + runOnUiIfAlive(() -> continueFullVideoAfterGateReady()); + } + + @Override + public void onFail(int code, String message) { + runOnUiIfAlive(() -> { + if (code == -100) { + showSdkTimeError(-100); + return; + } + showNetworkErrorRetry(code); + }); + } + })); + return; + } + + // 第 3 步:sUid 为空 → 经 checkBenefits 走三态分支 + JetsenxSdkUtil.ensureGlobalInitAsync(ContextUtils.getApplicationContext(), + new JetsenxSdkUtil.FallbackInitCallback() { + @Override + public void onReady(String memberCode) { + runOnUiIfAlive(() -> continueFullVideoAfterGateReady()); + } + + @Override + public void onNoEntitlement(int businessCode, String message) { + runOnUiIfAlive(() -> handlePlayGateNoEntitlement(businessCode, message)); + } + + @Override + public void onNetworkError(int businessCode, String message) { + runOnUiIfAlive(() -> { + if (businessCode == -100) { + showSdkTimeError(-100); + return; + } + showNetworkErrorRetry(businessCode); + }); + } + }); + } + + /** + * 闸门通过后:走真正的播放链路。isOrder 标记不强行翻转, + * 仍交给后续 SDK init / onResume judgeOrder 处理。 + */ + private void continueFullVideoAfterGateReady() { + if (isFinishing() || isDestroyed()) { + return; + } + playVideo(new DetailInterface.OnGetVideUrlCallBack() { + @Override + public void onSuccessVideoUrl(String videoUrl) { + videoDetailStatusManager.setData(videoUrl, 0, true); + fullWindowPlay(); } + }); + } + + /** + * 【用户主动触发路径专用】尝试跳转到 OrderActivity。 + *

严禁 SDK / 生命周期路径调用本方法——业务约定 SDK 触发一律不跳 OrderActivity。 + * SDK 路径({@link #reInitJetsenxSdk})只应调用 {@link #judgeOrderNotPurchased} 或 + * {@link #showEntitlementError}。

+ * + *

命中条件(全部满足):

    + *
  • 当前变体为家用版({@link AppConfiguration#isHomeEdition()})
  • + *
  • 业务码为 10900({@link AsApiRequester#CHECK_STATUS_CODE_ACTIVATED})
  • + *
  • 本会话内尚未跳过一次({@link #mUserClickRoutedToOrder10900} == false)
  • + *
命中则置位 + 启动 OrderActivity,调用方应 {@code return} 终止后续分支。

+ * + * @param businessCode checkBenefits 返回的业务码(10200 / 10900 / 10300-10800) + * @return true=已跳转(或已跳过一次不需要再跳),调用方应停止后续分发; + * false=不适用(变体非家用 / 业务码非 10900),调用方继续后续分支 + */ + private boolean tryUserClickHomeOrderJump(int businessCode) { + //内部触发限流的时候也要走权益页 即businessCode 为0的情况 + if (businessCode != AsApiRequester.CHECK_STATUS_CODE_ACTIVATED && businessCode != 0) { + return false; + } + if (!AppConfiguration.getInstance().isHomeEdition()) { + return false; + } + if (mUserClickRoutedToOrder10900) { + LogUtils.d(TAG, "tryUserClickHomeOrderJump USER CLICK 10900 already routed once in this Activity, skip re-jump"); + return true; + } + mUserClickRoutedToOrder10900 = true; + LogUtils.d(TAG, "tryUserClickHomeOrderJump USER CLICK triggered home edition 10900 → OrderActivity"); + startActivity(new Intent(VideoDetailActivity.this, OrderActivity.class)); + return true; + } + + /** + * 闸门判定为无权益后的业务码分发(用户主动触发路径): + * - 10200 激活但卡密为空:后端异常(与 reInitJetsenxSdk 10200 分支一致),错误视图 + * - 家用版 + 10900 首次/二次 → {@link #tryUserClickHomeOrderJump} 统一跳转收敛 + * - 扫码版 + 10900(thirdCardNo 为空)→ 试看 UI 兜底 + * - 行业版任何非 10200 → 后端异常错误视图,不引导付费 + * - 其他 → 试看 UI + */ + private void handlePlayGateNoEntitlement(int businessCode, String message) { + AppConfiguration cfg = AppConfiguration.getInstance(); + // 10200 激活但卡密为空:后端强要求该状态必配卡资源,属异常态, + // 直接展示异常,不引导订购也不误报未购买(与 reInitJetsenxSdk 同业务码分支一致)。 + // 提前置位:行业版 + 10200+empty 也会走这里,不会被后面的 industry 分支误判。 + if (businessCode == AsApiRequester.CHECK_STATUS_ACTIVATED) { + LogUtils.w(TAG, "play gate: 10200 activated but thirdCardNo missing"); + showEntitlementError(businessCode, message); + return; + } + // ====== 分流闸口 ====== + // 用户主动触发路径:家用版 10900 → 跳 OrderActivity(一次性,由 tryUserClickHomeOrderJump 收敛)。 + // SDK / 生命周期触发路径({@link #reInitJetsenxSdk})一律不调用本 helper,按业务约定不跳。 + if (tryUserClickHomeOrderJump(businessCode)) { + return; + } + // ====== /分流闸口 ====== + // 扫码版 + 10900(thirdCardNo 为空):保持现有降级,由 BenefitOrderController 处理; + // 闸门只做兜底,不再引导跳转(asAScan 原有 finish() 仅在 onCheckActivated 内触发)。 + // 上面的 home+10900 helper 已 return,到这里 cfg.isHomeEdition() 必为 false。 + if (BuildConfig.SCAN_VARIANT + && businessCode == AsApiRequester.CHECK_STATUS_CODE_ACTIVATED) { +// judgeOrderNotPurchased(); + return; + } + // 行业版(商用非扫码非家用):checkBenefits 业务码只能是 10200; + // 落到 10900 或其他状态视为后端异常,展示异常错误视图,不引导付费也不误报未购买 + if (cfg.isIndustryEdition()) { + LogUtils.w(TAG, "play gate: industry edition got non-10200 code " + businessCode); + showEntitlementError(businessCode, message); + return; + } + } + + /** + * 服务端时间漂移检查:nowtime 与系统时间差绝对值超过 24 小时视为异常。 + * nowtime 为空(详情接口异常)按"未知"处理,不阻断播放。 + * 使用 {@link Locale#ROOT} 避免 Thai/Arabic-Indic 等区域设置下数字字符被本地化导致解析失败。 + */ + private boolean isServerTimeDrifted(@Nullable String nowtime) { + if (TextUtils.isEmpty(nowtime)) { + return false; + } + try { + SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT); + Date server = fmt.parse(nowtime); + if (server == null) { + return false; + } + long drift = Math.abs(System.currentTimeMillis() - server.getTime()); + return drift > TIME_DRIFT_THRESHOLD_MS; + } catch (ParseException e) { + LogUtils.w(TAG, "isServerTimeDrifted: parse failed for nowtime=" + nowtime); + return false; } } @@ -1979,16 +2359,21 @@ public class VideoDetailActivity extends BaseActivity } public String getControllerTitle() { - if(mContentData != null) { - if (selectedProgramsManager.getVideoDetailType() != PROGRAM_TYPE_FILM) { - return mContentData.getName().trim() + " 第 " + mContentData.getSubcontents().get(selectedProgramsManager.getSelectIndex()).getEpisodeindex() + (mContentData.getCategoryname().equals("综艺") ? " 期" : " 集"); - } else { - return mContentData.getName().trim(); - } - }else{ + if (mContentData == null) { LogUtils.e("getControllerTitle : a null object reference "); return ""; } + String name = mContentData.getName() == null ? "" : mContentData.getName().trim(); + if (selectedProgramsManager == null || selectedProgramsManager.getVideoDetailType() == PROGRAM_TYPE_FILM) { + return name; + } + ContentListBean.ResultEntity.SubcontentsEntity episode = obtainEpisodeContent(); + if (episode == null) { + return name; + } + String category = mContentData.getCategoryname(); + String suffix = "综艺".equals(category) ? " 期" : " 集"; + return name + " 第 " + episode.getEpisodeindex() + suffix; } /** @@ -1998,20 +2383,7 @@ public class VideoDetailActivity extends BaseActivity if (isOrder) { return; } - if (BoxSharedParamsUtils.isJcHotel()) { - return; - } - DetailAmberManager.videoPageClick(isStarFullVideo, llOrder.getVisibility() == View.GONE, getControllerTitle() - , mContentData.getContentcode(), mContentData.getCategoryname() - , "订购"); - String contentName = mContentData != null ? mContentData.getName() : ""; - String media_classify = mContentData != null ? mContentData.getCategoryname() : ""; - Intent starter = new Intent(mContext, OrderActivity.class); - starter.putExtra(OrderActivity.media_classify_param, media_classify); - starter.putExtra(OrderActivity.content_id_param, contentId); - starter.putExtra(OrderActivity.content_name_param, contentName); - starter.putExtra(OrderActivity.refer_page_path_param, isStarFullVideo ? "播放页" : "详情页"); - startActivity(starter); + } /** @@ -2034,16 +2406,13 @@ public class VideoDetailActivity extends BaseActivity /** * 判断当前变体是否属于扫码版(as8Scan / asAScan)。 * - *

判断依据:AGP 自动注入的 {@code BuildConfig.FLAVOR} 等于 "as8Scan" 或 "asAScan"。 - * 仅在这两个扫码 flavor 下,详情页底部二维码入口启用为权益页跳转;其余 flavor + *

判断依据:通过 {@code BuildConfig.SCAN_VARIANT} 布尔常量直接判断, + * 该常量由 build.gradle 根据 flavor 维度注入,扫码版变体自动置为 true。 + * 仅在扫码版 flavor 下,详情页底部二维码入口启用为权益页跳转;其余 flavor * (aishi / ruigeer / ceshi)保留原占位二维码链接,避免破坏既有布局。

*/ private boolean isScanChannel() { - String flavor = BuildConfig.FLAVOR; - if (flavor == null) return false; - // BuildConfig.FLAVOR 是 channel + control 维度拼接,例如 as8ScanRemote / as8ScanRemoteDebug; - // 用 contains 匹配扫码版渠道标识,兼容所有 build type / control 组合 - return flavor.contains("as8Scan") || flavor.contains("asAScan"); + return BuildConfig.SCAN_VARIANT; } /** @@ -2271,7 +2640,7 @@ public class VideoDetailActivity extends BaseActivity // 该场景下没有可写入 sUid 的卡密,跳过 globalInit 重置链路;直接 finish 当前 // 详情页回到首页 IndexActivity,让用户重新从首页触发正常播放流程(as8Scan 走的是 // 带卡密的 10200 路径,不会进入此分支)。 - if (isAsAScanChannel()) { + if (isScanChannel()) { LogUtils.d(TAG, "benefit order onCheckActivated: asAScan variant 10900 (no cardNo), " + "finish and back to IndexActivity"); finish(); @@ -2281,19 +2650,6 @@ public class VideoDetailActivity extends BaseActivity ToastUtils.showLongToast(R.string.benefit_order_toast_activated); } - /** - * 判断当前变体是否为「激活扫码版」asAScan(有激活码)。 - * - *

仅在该变体下,{@code /api/jst/check} 会返回业务码 10900(已通过激活码激活、 - * 无权益码 / 无 thirdCardNo)。该场景没有可写入 sUid 的卡密,跳过 SDK 重置链路; - * 视频详情页流程与 as8Scan 保持一致:直接 finish 当前详情页回到首页 - * IndexActivity,由用户在首页重新触发正常播放链路。

- */ - private boolean isAsAScanChannel() { - String flavor = BuildConfig.FLAVOR; - return flavor != null && flavor.contains("asAScan"); - } - @Override public void onCheckFailed(String orderNo, String message) { LogUtils.w(TAG, "benefit order onCheckFailed orderNo=" + orderNo + " msg=" + message); @@ -2303,18 +2659,63 @@ public class VideoDetailActivity extends BaseActivity } @Override - public void onError(String stage, String message) { - LogUtils.e(TAG, "benefit order onError stage=" + stage + " msg=" + message); + public void onError(String stage, int code, String message) { + LogUtils.e(TAG, "benefit order onError stage=" + stage + " code=" + code + " msg=" + message); if (BenefitOrderController.STAGE_CREATE.equals(stage)) { - ToastUtils.showLongToast(R.string.benefit_order_toast_create_fail); + String msg = getString(R.string.benefit_order_toast_create_fail)+"<"+code+">"; + ToastUtils.showLongToast(msg); } else if (BenefitOrderController.STAGE_STATUS.equals(stage)) { - ToastUtils.showLongToast(R.string.benefit_order_toast_status_fail); + String msg = getString(R.string.benefit_order_toast_status_fail)+"<"+code+">"; + ToastUtils.showLongToast(msg); } else { - // 兜底:直接展示 message(已用户友好化) - ToastUtils.showLongToast(message); + ToastUtils.showLongToast(message+"<"+code+">"); } } + /** + * 会员权益到期回调(由 [BenefitOrderController] 在 check 命中激活后启动的监控触发)。 + * + *

处理逻辑:

+ *
    + *
  1. 恢复 UI:把"已购买VIP"容器隐藏,重新展示二维码区(清空过期态)
  2. + *
  3. 清空 AppConfiguration.expireTime + 取消 benefitOrderController,避免下次进入仍走"已激活"短路
  4. + *
  5. Toast 提示「会员权益已到期」
  6. + *
  7. 退出整个程序([AppManager.appExit] + killProcess),让用户重启后走激活流程
  8. + *
+ * + *

仅在扫码版变体(as8Scan / asAScan)下有意义;非扫码版变体不应调用。

+ */ + @Override + public void onBenefitExpired() { + LogUtils.w(TAG, "benefit expired, restore QR + toast + exit app"); + runOnUiThread(() -> { + // 1. 恢复为二维码状态:隐藏"已购买VIP"容器、显示二维码区(与初始未购买态一致) + if (llAlreadyPurchasedContainer != null) { + llAlreadyPurchasedContainer.setVisibility(View.GONE); + } + if (qrCodeView != null) { + qrCodeView.setVisibility(View.VISIBLE); + // 不主动发起 createOrder:用户重新进入程序后会走正常 create 流程; + // 此处仅恢复 UI 占位,避免一帧空白 + } + // 清空当前扫码 codeUrl,让下次 createOrder 不再被旧值覆盖 + currentCodeUrl = null; + // 2. 清空进程级到期时间缓存,避免进入首页又走"已激活"短路 + AppConfiguration.getInstance().setExpireTime(""); + // 3. 取消业务控制器,停止 status / check 轮询 + if (benefitOrderController != null) { + benefitOrderController.cancel(); + benefitOrderController = null; + } + // 4. Toast 提示会员到期 + ToastUtils.showLongToast(R.string.benefit_order_toast_expired); + // 5. 退出整个程序:与 MainActivity.exitApp 一致(finishAllActivity + 延迟 killProcess) + // 用户重新开机后走正常启动流程,重新扫码购买 VIP + AppManager.getAppManager().setKeep(false); + AppManager.getAppManager().appExit(); + }); + } + // @SuppressLint("NonConstantResourceId") @Override public void onClick(View v) { @@ -2610,14 +3011,6 @@ public class VideoDetailActivity extends BaseActivity handlerLifecycleManager = null; } - // 保存清晰度设置:触摸模式或 Android 5.1(API 22)及以下系统默认 720,其余默认 1080 - if(AppConfiguration.getInstance().isTouchMode() - || Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1){ - SharedPreferenceUtils.put("Definition", 720); - }else { - SharedPreferenceUtils.put("Definition", 1080); - } - // 从 Activity 栈中移除 VideoActivityStackManager.getInstance().removeActivity(this); diff --git a/app/src/main/java/com/pukka/jiecheng/video/adapter/HistoryAdapter.java b/app/src/main/java/com/pukka/jiecheng/video/adapter/HistoryAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/adapter/ProgramTableAdapter.java b/app/src/main/java/com/pukka/jiecheng/video/adapter/ProgramTableAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/adapter/ProgramTablePageAdapter.java b/app/src/main/java/com/pukka/jiecheng/video/adapter/ProgramTablePageAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/adapter/RecommendAdapter.java b/app/src/main/java/com/pukka/jiecheng/video/adapter/RecommendAdapter.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/HistoryViewHolder.java b/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/HistoryViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableEmptyViewHolder.java b/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableEmptyViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableVarietyEmptyViewHolder.java b/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableVarietyEmptyViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableVarietyViewHolder.java b/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableVarietyViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableViewHolder.java b/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/ProgramTableViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/RecommendViewHolder.java b/app/src/main/java/com/pukka/jiecheng/video/adapter/vh/RecommendViewHolder.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/amber/DetailAmberManager.java b/app/src/main/java/com/pukka/jiecheng/video/amber/DetailAmberManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/bean/ErrInfoBean.java b/app/src/main/java/com/pukka/jiecheng/video/bean/ErrInfoBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/bean/FindPlayrecordBean.java b/app/src/main/java/com/pukka/jiecheng/video/bean/FindPlayrecordBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/bean/JetsenxEventReportBean.java b/app/src/main/java/com/pukka/jiecheng/video/bean/JetsenxEventReportBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/bean/PlayCodeBean.java b/app/src/main/java/com/pukka/jiecheng/video/bean/PlayCodeBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/bean/SendAddPlayRecordBean.java b/app/src/main/java/com/pukka/jiecheng/video/bean/SendAddPlayRecordBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/bean/TablePageBean.java b/app/src/main/java/com/pukka/jiecheng/video/bean/TablePageBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/bean/TestBean.java b/app/src/main/java/com/pukka/jiecheng/video/bean/TestBean.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/benefit/BenefitOrderCallback.java b/app/src/main/java/com/pukka/jiecheng/video/benefit/BenefitOrderCallback.java old mode 100755 new mode 100644 index be11a54..905980e --- a/app/src/main/java/com/pukka/jiecheng/video/benefit/BenefitOrderCallback.java +++ b/app/src/main/java/com/pukka/jiecheng/video/benefit/BenefitOrderCallback.java @@ -83,7 +83,19 @@ public interface BenefitOrderCallback { * {@link BenefitOrderController#STAGE_STATUS} / {@link BenefitOrderController#STAGE_CHECK}。

* * @param stage 阶段标识 + * @param code 业务/HTTP 状态码;无明确业务码时为 -1(网络异常/解析异常等场景) * @param message 错误描述(已面向用户友好化) */ - void onError(String stage, String message); + void onError(String stage, int code, String message); + + /** + * 会员权益到期回调。 + * + *

由 {@link BenefitOrderController} 在扫描激活成功后,基于 [com.pukka.jiecheng.config.AppConfiguration] + * 中缓存的会员到期时间启动单次延时触发;命中时回调本方法,由宿主恢复二维码区、Toast 提示「会员权益已到期」 + * 并退出整个程序。

+ * + *

仅在扫码版变体(as8Scan / asAScan)下有意义;非扫码版变体不应实现本回调。

+ */ + void onBenefitExpired(); } \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/video/benefit/BenefitOrderController.kt b/app/src/main/java/com/pukka/jiecheng/video/benefit/BenefitOrderController.kt old mode 100755 new mode 100644 index 666ba32..42b40e4 --- a/app/src/main/java/com/pukka/jiecheng/video/benefit/BenefitOrderController.kt +++ b/app/src/main/java/com/pukka/jiecheng/video/benefit/BenefitOrderController.kt @@ -11,6 +11,8 @@ import com.pukka.jiecheng.network.infra.EnvProvider import com.pukka.jiecheng.network.infra.NetResponse import com.pukka.jiecheng.network.infra.RetrofitFactory import com.pukka.jiecheng.network.infra.toApiError +import com.pukka.jiecheng.request.AsApiRequester +import com.pukka.libcore.utils.ContextUtils.getApplicationContext import com.pukka.libcore.utils.LogUtils import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope @@ -18,6 +20,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import org.json.JSONObject /** * 天卡-扫码激活业务控制类(Kotlin)。 @@ -109,6 +112,15 @@ class BenefitOrderController( /** 二维码过期监控 Job;按 expireTime 触发重新 create */ private var expireMonitorJob: Job? = null + /** + * 会员权益过期监控 Job;按 [expireTimeMs] 触发 [BenefitOrderCallback.onBenefitExpired] 回调。 + * + *

仅在阶段三 check 命中激活(handleCheckActivated)后启动;与二维码过期监控不同: + * 二维码过期监控仅用于「重新生成二维码」,到期后会 cancel status 轮询并重新 create; + * 本 Job 用于「VIP 真正过期 → 恢复 UI + Toast + 退出程序」,不会被重新 create 触发。

+ */ + private var vipExpireMonitorJob: Job? = null + /** 当前有效 orderNo;用于回调中携带 */ private var currentOrderNo: String? = null @@ -149,24 +161,48 @@ class BenefitOrderController( throw ce } catch (e: Throwable) { LogUtils.e(TAG, "create exception: ${e.message}") - callback.onError(STAGE_CREATE, e.toApiError().message) - return@launch - } - - if (resp.code != 0 || resp.data == null) { - LogUtils.e(TAG, "create business error code=${resp.code} msg=${resp.msg}") - callback.onError(STAGE_CREATE, resp.msg ?: "订单创建失败,请稍后重试") + val apiError = e.toApiError() + val code = apiError.businessCode ?: apiError.httpCode ?: -1 + callback.onError(STAGE_CREATE, code, apiError.message) return@launch } + val respCode = resp.code + val respMsg = resp.msg val dto = resp.data + + if (respCode != 0 || dto == null) { + LogUtils.e(TAG, "create business error code=$respCode msg=$respMsg") + val extraData = JSONObject().apply { + put("film_name", filmName) + put("content_code", contentCode) + put("card_type", CARD_TYPE_DAY) + put("resp_code", respCode) + put("resp_msg", respMsg) + } + lifecycleScope.launch { + try { + AsApiRequester.requestAsTrackingApiSuspend( + getApplicationContext(), + AsApiRequester.TRACKING_TYPE_BENEFIT_ORDER_CREATE, + AsApiRequester.TRACKING_STAGE_FAIL, + (respCode ?: -1).toString(), + respMsg ?: "订单创建失败", + extraData + ) + } catch (e: Throwable) { + LogUtils.e(TAG, "tracking report error: ${e.message}") + } + } + callback.onError(STAGE_CREATE, respCode ?: -1, respMsg ?: "订单创建失败,请稍后重试 ") + return@launch + } + val orderNo = dto.orderNo.orEmpty() val codeUrl = dto.codeUrl.orEmpty() val amountFen = dto.amount ?: 0 val cycleCheckTime = dto.cycleCheckTime ?: DEFAULT_CYCLE_CHECK_TIME_SEC val expireTime = dto.expireTime ?: 0L - // create 响应的初态:PENDING=待扫码 / PAID=已付款待激活 / ACTIVATING=卡密下发中 / ACTIVE=已激活有权益。 - // 透传给宿主用于:① 初态即已付款/已激活时不展示二维码;② 初态已 ACTIVE 时直接进入阶段三,无需轮询。 val initialStatus = dto.status.orEmpty().uppercase() LogUtils.d(TAG, "create success orderNo=$orderNo amount=${amountFen} " + @@ -174,7 +210,7 @@ class BenefitOrderController( "initialStatus=$initialStatus") if (orderNo.isEmpty() || codeUrl.isEmpty()) { - callback.onError(STAGE_CREATE, "订单数据异常,请稍后重试") + callback.onError(STAGE_CREATE, respCode ?: -1, "订单数据异常,请稍后重试") return@launch } @@ -369,6 +405,9 @@ class BenefitOrderController( lastCheckExpireTime = expireTime statusJob?.cancel() expireMonitorJob?.cancel() + // 阶段三命中激活后启动「会员权益到期」监控: + // 若 expireTime 为空/无法解析,跳过监控(视为无到期时间约定,不主动退出程序) + startVipExpireMonitor(expireTime) callback.onCheckActivated(cardNo) } @@ -409,7 +448,7 @@ class BenefitOrderController( // ===================================================================================== /** - * 取消所有轮询协程(status / check / expireMonitor)。 + * 取消所有轮询协程(status / check / expireMonitor / vipExpireMonitor)。 * *

通常在宿主 onDestroy / 用户主动关闭时调用;lifecycleScope 已绑生命周期, * destroy 时框架会自动取消,本方法是保险机制。

@@ -421,6 +460,59 @@ class BenefitOrderController( checkJob = null expireMonitorJob?.cancel() expireMonitorJob = null + vipExpireMonitorJob?.cancel() + vipExpireMonitorJob = null + } + + // ===================================================================================== + // 会员权益到期监控(check 命中激活后启动 → 到期时回调宿主恢复二维码 + 退出程序) + // ===================================================================================== + + /** + * 启动「会员权益到期」单次延时监控。 + * + *

仅在阶段三 check 命中激活([handleCheckActivated])时调用。计算当前时间到 [expireTimeStr] + * 的剩余毫秒数,delay 至到期时间触发 [BenefitOrderCallback.onBenefitExpired],由宿主恢复 + * 二维码 UI + Toast 提示「会员权益已到期」 + 退出整个程序。

+ * + *

注意:

+ *
    + *
  • 本 Job 是「一次性」触发,与 [startExpireMonitor](二维码过期)独立
  • + *
  • 二维码过期监控到期后会 cancel status + 重新 create,本监控不会受其影响
  • + *
  • [expireTimeStr] 为空 / 解析失败 → 直接返回,不启动监控(视为无明确到期约定)
  • + *
  • 宿主 destroy / 调 [cancel] / 重新走 [createOrder] → 协程由 lifecycleScope 自动取消, + * 本字段用于 [cancel] 显式兜底
  • + *
+ * + * @param expireTimeStr 到期时间字符串(后端约定格式 `yyyy-MM-dd HH:mm:ss`);为空跳过 + */ + fun startVipExpireMonitor(expireTimeStr: String?) { + vipExpireMonitorJob?.cancel() + if (expireTimeStr.isNullOrBlank()) { + LogUtils.d(TAG, "vip expire monitor skipped: expireTime is empty") + return + } + val expireTimeMs = try { + val sdf = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault()) + sdf.isLenient = false + sdf.parse(expireTimeStr)?.time ?: 0L + } catch (e: Throwable) { + LogUtils.w(TAG, "vip expire monitor parse fail: $expireTimeStr err=${e.message}") + 0L + } + if (expireTimeMs <= 0L) return + + vipExpireMonitorJob = lifecycleScope.launch { + val delayMs = (expireTimeMs - System.currentTimeMillis()).coerceAtLeast(0L) + LogUtils.d(TAG, "vip expire monitor delayMs=$delayMs (expireTime=$expireTimeStr)") + delay(delayMs) + if (!isActive) return@launch + + LogUtils.w(TAG, "vip benefit expired, notify host to restore QR + exit app") + // 命中到期:先 cancel status 轮询,避免 onBenefitExpired → 宿主退出程序 后 status 仍在跑 + statusJob?.cancel() + callback.onBenefitExpired() + } } /** 当前有效 orderNo;供宿主上报埋点使用 */ diff --git a/app/src/main/java/com/pukka/jiecheng/video/face/DetailInterface.java b/app/src/main/java/com/pukka/jiecheng/video/face/DetailInterface.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/face/NavigationInterface.java b/app/src/main/java/com/pukka/jiecheng/video/face/NavigationInterface.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/face/PlayTheNextVideoInterface.java b/app/src/main/java/com/pukka/jiecheng/video/face/PlayTheNextVideoInterface.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/face/SelectionsInterface.java b/app/src/main/java/com/pukka/jiecheng/video/face/SelectionsInterface.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/fragment/RecommendFragment.java b/app/src/main/java/com/pukka/jiecheng/video/fragment/RecommendFragment.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/manger/ResolutionManger.java b/app/src/main/java/com/pukka/jiecheng/video/manger/ResolutionManger.java old mode 100755 new mode 100644 index dec50cb..0b63dee --- a/app/src/main/java/com/pukka/jiecheng/video/manger/ResolutionManger.java +++ b/app/src/main/java/com/pukka/jiecheng/video/manger/ResolutionManger.java @@ -84,7 +84,8 @@ public class ResolutionManger { } public void initResolution() { - tv720.setVisibility(videoDetailStatusManager.isHave720()?View.VISIBLE:View.GONE); + // 720P 档下线:清晰度菜单恒只展示 1080P(720 仅老片库无 1080 档时内部兜底播放) + tv720.setVisibility(View.GONE); tv1080.setVisibility(videoDetailStatusManager.isHave1080()?View.VISIBLE:View.GONE); if (videoDetailStatusManager.getCurResolutionUrl() == 0) { return; diff --git a/app/src/main/java/com/pukka/jiecheng/video/manger/SpeedManger.java b/app/src/main/java/com/pukka/jiecheng/video/manger/SpeedManger.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/other/MgDividerItemDecoration.java b/app/src/main/java/com/pukka/jiecheng/video/other/MgDividerItemDecoration.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/other/SpacesItemDecoration.java b/app/src/main/java/com/pukka/jiecheng/video/other/SpacesItemDecoration.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/popup/BlackFullDialog.java b/app/src/main/java/com/pukka/jiecheng/video/popup/BlackFullDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/popup/InfoPopupWindow.java b/app/src/main/java/com/pukka/jiecheng/video/popup/InfoPopupWindow.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/popup/LoadFullDialog.java b/app/src/main/java/com/pukka/jiecheng/video/popup/LoadFullDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/popup/OnDemandEpisodesView.java b/app/src/main/java/com/pukka/jiecheng/video/popup/OnDemandEpisodesView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/popup/QrCodeDialog.java b/app/src/main/java/com/pukka/jiecheng/video/popup/QrCodeDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/popup/RetentionDialog.java b/app/src/main/java/com/pukka/jiecheng/video/popup/RetentionDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/popup/TipsDialog.java b/app/src/main/java/com/pukka/jiecheng/video/popup/TipsDialog.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/ContentDetailUtils.java b/app/src/main/java/com/pukka/jiecheng/video/utils/ContentDetailUtils.java old mode 100755 new mode 100644 index 3d9aaff..dfb0a5a --- a/app/src/main/java/com/pukka/jiecheng/video/utils/ContentDetailUtils.java +++ b/app/src/main/java/com/pukka/jiecheng/video/utils/ContentDetailUtils.java @@ -20,6 +20,7 @@ import com.pukka.jiecheng.R; import com.pukka.libcore.fresco.PkSimpleDraweeView; import com.pukka.libcore.utils.LogUtils; +import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/HandlerLifecycleManager.java b/app/src/main/java/com/pukka/jiecheng/video/utils/HandlerLifecycleManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/KeyThrottleVerifier.java b/app/src/main/java/com/pukka/jiecheng/video/utils/KeyThrottleVerifier.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordContentManager.java b/app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordContentManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordManager.java b/app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordManager.java old mode 100755 new mode 100644 index 213ba7f..c69befa --- a/app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordManager.java +++ b/app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordManager.java @@ -180,7 +180,10 @@ public class PlayRecordManager { } String code = ""; try { - code = playCodeMaps.get(SharedPreferenceUtils.getInt("Definition", 1080)).getProgramCode(); + PlayCodeBean bean = getCurPlayCode(); + if (bean != null) { + code = bean.getProgramCode(); + } } catch (Exception e) { } @@ -242,12 +245,35 @@ public class PlayRecordManager { private void upDataVideoCode() { try { - programCode = playCodeMaps.get(SharedPreferenceUtils.getInt("Definition", 1080)).getProgramCode(); - movieCode = playCodeMaps.get(SharedPreferenceUtils.getInt("Definition", 1080)).getMovieCode(); - specCode = playCodeMaps.get(SharedPreferenceUtils.getInt("Definition", 1080)).getSpecCode(); + PlayCodeBean bean = getCurPlayCode(); + if (bean != null) { + programCode = bean.getProgramCode(); + movieCode = bean.getMovieCode(); + specCode = bean.getSpecCode(); + } } catch (Exception e) { } } + /** + * 取当前播放档位对应的码信息。 + * 5.1 改版后清晰度恒 1080(5.1/普通 1080P 档由 SDK 侧决策),720 仅老片库兜底播放; + * 优先按当前实际播放 URL 匹配档位,匹配失败时回落 1080 再回落 720。 + */ + private PlayCodeBean getCurPlayCode() { + int def = videoDetailStatusManager != null ? videoDetailStatusManager.getCurResolutionUrl() : 0; + if (def != 720 && def != 1080) { + def = 1080; + } + PlayCodeBean bean = playCodeMaps.get(def); + if (bean == null) { + bean = playCodeMaps.get(1080); + } + if (bean == null) { + bean = playCodeMaps.get(720); + } + return bean; + } + } diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordShortManager.java b/app/src/main/java/com/pukka/jiecheng/video/utils/PlayRecordShortManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/SelectedProgramsManager.java b/app/src/main/java/com/pukka/jiecheng/video/utils/SelectedProgramsManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/TimeVideoUtils.java b/app/src/main/java/com/pukka/jiecheng/video/utils/TimeVideoUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/ToastUtils.java b/app/src/main/java/com/pukka/jiecheng/video/utils/ToastUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/VideoActivityStackManager.java b/app/src/main/java/com/pukka/jiecheng/video/utils/VideoActivityStackManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/VideoDetailKeyHandler.java b/app/src/main/java/com/pukka/jiecheng/video/utils/VideoDetailKeyHandler.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/VideoDetailStatusManager.java b/app/src/main/java/com/pukka/jiecheng/video/utils/VideoDetailStatusManager.java old mode 100755 new mode 100644 index cd67647..5778772 --- a/app/src/main/java/com/pukka/jiecheng/video/utils/VideoDetailStatusManager.java +++ b/app/src/main/java/com/pukka/jiecheng/video/utils/VideoDetailStatusManager.java @@ -384,6 +384,11 @@ public class VideoDetailStatusManager implements LifecycleEventObserver { LogUtils.d("VDSM", "setData videoUrl:" + videoUrl + " seekTo" + seekTo); lastPosition = 0; if (mVideoView != null) { + // 与 changeData 保持一致:先释放旧播放器再设置新 URL。 + // 否则 openVideo() 里 mMediaPlayer != null 会直接 return,新 URL 不生效, + // 播放器仍持有旧源(或已失效的实例),表现为一直卡在"正在加载中" + videoViewRelease(); + mVideoView.releaseError(true); mVideoView.setVideoPath(videoUrl); if (isAutoPlay) { mVideoView.start(); @@ -439,23 +444,12 @@ public class VideoDetailStatusManager implements LifecycleEventObserver { public static String getDefinitionUrl(String videoUrl_1080, String videoUrl_720) { - // 默认清晰度:触摸模式 720,遥控模式 1080 - boolean definition_720 = AppConfiguration.getInstance().isTouchMode(); - // Android 5.1(API 22)及以下系统解码能力有限,默认降级为 720P - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) { - definition_720 = true; - } - if (SharedPreferenceUtils.getInt("Definition", definition_720? 720 :1080) == 1080) { - if (TextUtils.isEmpty(videoUrl_1080)) { - return videoUrl_720; - } + // 5.1 改版:清晰度恒选 1080 档(5.1 环绕声档/普通 1080P 档由 SDK 侧决策), + // 720 档仅作老片库无 1080 档时的兜底,不再作为可选清晰度。 + if (!TextUtils.isEmpty(videoUrl_1080)) { return videoUrl_1080; - } else { - if (TextUtils.isEmpty(videoUrl_720)) { - return videoUrl_1080; - } - return videoUrl_720; } + return videoUrl_720; } /** @@ -1024,8 +1018,8 @@ public class VideoDetailStatusManager implements LifecycleEventObserver { } @Override - public void onFail(String message) { - LogUtils.w("VDSM", "trySeeOrderLayoutInit globalInit failed: " + message); + public void onFail(int errorCode, String message) { + LogUtils.w("VDSM", "trySeeOrderLayoutInit globalInit failed: code=" + errorCode + ", msg=" + message); reportReInitTracking(AsApiRequester.TRACKING_STAGE_FAIL, "global_init_fail", TextUtils.isEmpty(message) ? "SDK重新初始化失败" : message, "try_see_order_layout_init", ""); diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/VideoFocusStateMachine.java b/app/src/main/java/com/pukka/jiecheng/video/utils/VideoFocusStateMachine.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/utils/ViewUtils.java b/app/src/main/java/com/pukka/jiecheng/video/utils/ViewUtils.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/video/widget/VideoDetailRecommendRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/video/widget/VideoDetailRecommendRecyclerView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/voice/adapter/SkyworthVoiceAdapter.java b/app/src/main/java/com/pukka/jiecheng/voice/adapter/SkyworthVoiceAdapter.java new file mode 100644 index 0000000..1fa12bb --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/adapter/SkyworthVoiceAdapter.java @@ -0,0 +1,306 @@ +package com.pukka.jiecheng.voice.adapter; + +import android.app.ActivityManager; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +import androidx.core.app.NotificationCompat; + +import com.pukka.jiecheng.voice.log.VoiceCommandLogger; +import com.pukka.jiecheng.voice.protocol.VoiceExtras; +import com.pukka.jiecheng.voice.receiver.VoiceInternalReceiver; +import com.pukka.jiecheng.voice.window.VoiceOverlayHelper; + +import org.json.JSONArray; +import org.json.JSONException; + +/** + * 创维(skyworth)语音识别广播 Adapter。 + * + *

第三方在识别到用户语音后投递广播: + *

+ *   action : com.skyworth.smarthotel.action.voice.RecognizeResult.vod
+ *   extras : videoList : String    JSON 数组字符串(如 ["哪吒之魔童降世","我是哪吒"]),按需取 [0]
+ *           autoPlay   : String    "true" / "false",缺省按 true 处理
+ * 
+ * + *

本 Adapter 当前由 {@link com.pukka.jiecheng.voice.service.VoiceKeepAliveService} + * 在 :voice 进程动态注册。Adapter 只做"翻译 + 转发",不调业务执行: + *

    + *
  1. 校验 action,过滤伪造广播
  2. + *
  3. 取 {@code videoList} 第一个非空作为关键字
  4. + *
  5. 探测主进程是否存活: + *
      + *
    • alive=true → 显式广播转发到主进程 {@link VoiceInternalReceiver}(hot path)
    • + *
    • alive=false → cold launch {@code MainActivity} 带 {@code __voice_pending} extras(cold path)
    • + *
  6. + *
+ * + *

本 Adapter 不做业务执行;命令的 throttle / router / navigator 全部跑在主进程, + * 保证 {@link com.pukka.jiecheng.utils.ActivityMonitor} 状态语义与原版一致。 + * + *

Sender 白名单当前按设计 §7.7 暂以"信任"形态接入(OEM 提供稳定 sender 包名后再做真校验)。 + * + *

详见 {@code docs/2026-08-28-语音冷启动链路设计方案.md}。 + */ +public class SkyworthVoiceAdapter extends BroadcastReceiver { + + /** 第三方广播 action(按创维 smart-hotel SDK 文档)。 */ + public static final String ACTION_VOD = + "com.skyworth.smarthotel.action.voice.RecognizeResult.vod"; + + /** extras key:候选影片名列表。 */ + public static final String EXTRA_VIDEO_LIST = "videoList"; + + /** 跨进程 forward extras 键,与 {@link VoiceInternalReceiver} / {@link VoiceExtras} 共享。 */ + private static final String MAIN_ACTIVITY_CLASS = "com.pukka.jiecheng.MainActivity"; + private static final String DEFAULT_SENDER = "com.skyworth.smarthotel"; + private static final String LOG_TAG = "VoiceCommand"; + + @Override + public void onReceive(Context context, Intent intent) { + if (context == null || intent == null) return; + if (!ACTION_VOD.equals(intent.getAction())) return; + + String[] videoList = resolveVideoList(intent); + String filmName = firstNonEmpty(videoList); + if (TextUtils.isEmpty(filmName)) { + Log.w(LOG_TAG, "skyworth videoList empty :"+filmName); + VoiceCommandLogger.dropped("skyworth videoList empty", ""); + return; + } + + String autoPlay = intent.getStringExtra("autoPlay"); + if (TextUtils.isEmpty(autoPlay)) autoPlay = "true"; + + String requestId = "skyworth-" + + System.currentTimeMillis() + "-" + + (int) (Math.random() * 1000); + + String sender = intent.getPackage(); + if (TextUtils.isEmpty(sender)) sender = DEFAULT_SENDER; + + VoiceCommandLogger.received(requestId, sender, "skyworth", "JUMP", filmName); + + boolean ready = isMainProcessReady(context); + Log.i(LOG_TAG, "skyworth.mainProcessCheck: ready=" + ready + + " requestId=" + requestId); + if (ready) { + forwardToMainProcess(context, filmName, autoPlay, requestId, sender); + } else { + coldLaunchMainActivity(context, filmName, autoPlay, requestId, sender); + } + } + + /** + * 判定主进程是否"够接 forward 广播":只看主进程是否在 AMS 进程列表中。 + * + *

历史版本曾有「两道校验」:除 {@code getRunningAppProcesses} 外,还补一道 + * {@code PackageManager.queryBroadcastReceivers} 反查 {@link VoiceInternalReceiver} + * 是否在 PMS 路由表里。后者实现是错的——{@code VoiceInternalReceiver} 是 + * {@code MyApplication.onCreate} 里动态注册的 receiver, + * {@link android.content.pm.PackageManager#queryBroadcastReceivers} 默认只返回 + * manifest 静态声明的 receiver,动态注册的永远查不到。所以在主进程已经起来 + * UI 已展示的场景(此时第一道一定 true、第二道永远 false),{@code isMainProcessReady} + * 会被误判为 false,强制走 cold path 启动 MainActivity,清掉栈里所有 Activity + * 引发「播放中被清栈 → 重走 MainActivity → IndexActivity → VideoDetailActivity + * 完整链路」的体验。 + * + *

新策略下只要主进程在 AMS 中就视为 ready,走 forward 广播——主进程侧的 + * {@link VoiceInternalReceiver} 由 {@code MyApplication.onCreate} 早于用户 UI + * 渲染前完成注册,正常场景下一定能接住;异常场景(receiver 未注册就被 forward) + * 由 {@link com.pukka.jiecheng.voice.service.VoiceKeepAliveService} 持有的 + * forwardOrPersistFallback 兜底持久化 pending extras,不会丢命令。 + * + *

主进程不在 AMS 中 → 返回 false,走 {@link #coldLaunchMainActivity} 拉起。 + */ + private static boolean isMainProcessReady(Context ctx) { + ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE); + if (am == null) return false; + String mainProcessName = ctx.getPackageName(); + try { + for (ActivityManager.RunningAppProcessInfo p : am.getRunningAppProcesses()) { + if (mainProcessName.equals(p.processName)) { + return true; + } + } + } catch (Throwable ignored) { + // getRunningAppProcesses 在 Android 8+ 文档标记 best-effort,吞掉异常降级到 cold path + } + return false; + } + + private static void forwardToMainProcess(Context ctx, String filmName, String autoPlay, + String requestId, String sender) { + Intent fwd = new Intent(VoiceInternalReceiver.ACTION_VOICE_FORWARD); + fwd.setPackage(ctx.getPackageName()); + fwd.putExtra(VoiceExtras.EXTRA_OEM_FILM_NAME, filmName); + fwd.putExtra(VoiceExtras.EXTRA_OEM_AUTOPLAY, autoPlay); + fwd.putExtra(VoiceExtras.EXTRA_OEM_REQUEST_ID, requestId); + fwd.putExtra(VoiceExtras.EXTRA_OEM_SOURCE_APP, sender); + Log.i(LOG_TAG, "voice forward to main: requestId=" + requestId); + try { + ctx.sendBroadcast(fwd); + } catch (Throwable t) { + VoiceCommandLogger.dropped("voice forward to main failed", t.getMessage()); + } + } + + private static void coldLaunchMainActivity(Context ctx, String filmName, String autoPlay, + String requestId, String sender) { + Intent cold = new Intent(); + cold.setClassName(ctx, MAIN_ACTIVITY_CLASS); + cold.setPackage(ctx.getPackageName()); + cold.putExtra(VoiceExtras.EXTRA_VOICE_PENDING, true); + cold.putExtra(VoiceExtras.EXTRA_OEM_FILM_NAME, filmName); + cold.putExtra(VoiceExtras.EXTRA_OEM_AUTOPLAY, autoPlay); + cold.putExtra(VoiceExtras.EXTRA_OEM_REQUEST_ID, requestId); + cold.putExtra(VoiceExtras.EXTRA_OEM_SOURCE_APP, sender); + cold.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + + Log.i(LOG_TAG, "voice cold launch: requestId=" + requestId); + + // 三级降级路径: + // ① 优先:1px TYPE_APPLICATION_OVERLAY 悬浮窗 + ctx.startActivity(cold)。 + // Android 12+ Background Activity Start 限制对 "calling app has a visible + // window" 显式豁免;OEM ROM 上 Notification.setFullScreenIntent 也被拦时 + // 此路径仍能成功。Activity 起来后延迟 500ms 隐藏悬浮窗。 + // ② 次选:PendingIntent.getActivity + Notification.setFullScreenIntent + // (Android 12+ 官方推荐的合规后台启动 Activity 路径,部分 OEM ROM 拦截 + // 悬浮窗但放行 FullScreenIntent)。 + // ③ 兜底:主进程已激活时 forward 广播;否则仅持久化 pending extras, + // 等待 BootCompletedReceiver / 用户开 App 触发的自然路径消费。 + if (VoiceOverlayHelper.showOverlay(ctx)) { + try { + ctx.startActivity(cold); + Log.i(LOG_TAG, "voice cold launch via overlay: requestId=" + requestId); + // 延迟隐藏:等 AMS 完成 Activity 启动判定再回收悬浮窗 + final android.os.Handler handler = new android.os.Handler(ctx.getMainLooper()); + handler.postDelayed(new Runnable() { + @Override public void run() { + VoiceOverlayHelper.hideOverlay(); + } + }, 500); + return; + } catch (Throwable t) { + // startActivity 自己失败时退化到次选路径 + VoiceCommandLogger.dropped("voice cold launch startActivity failed", + t.getMessage()); + VoiceOverlayHelper.hideOverlay(); + // fall through + } + } + + // ①/① 失败 → 走 FullScreenIntent 通知 + PendingIntent pi = PendingIntent.getActivity(ctx, requestId.hashCode(), cold, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationManager nm = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); + if (nm == null) { + VoiceCommandLogger.dropped("voice cold launch no NotificationManager", ""); + forwardOrPersistFallback(ctx, filmName, autoPlay, requestId, sender); + return; + } + String channelId = "voice_cold_start"; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel ch = new NotificationChannel(channelId, "语音唤醒", + NotificationManager.IMPORTANCE_HIGH); + ch.setShowBadge(false); + ch.setSound(null, null); + nm.createNotificationChannel(ch); + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx, channelId) + .setSmallIcon(android.R.drawable.stat_notify_chat) + .setContentTitle("语音唤醒") + .setContentText("正在打开 " + filmName) + .setContentIntent(pi) + .setFullScreenIntent(pi, true) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setAutoCancel(true); + + try { + nm.notify(0x1002, builder.build()); + } catch (Throwable t) { + // 兜底:setFullScreenIntent 在某些 OEM ROM 上仍被拦。 + // 退化为 forwardToMainProcess(仅在主进程已活时有效,cold 场景仍会丢), + // 把 pending extras 通过 forward extras 透传,主进程若在自然启动路径 + // (BootCompletedReceiver / 用户开 App)激活后由 capturePendingVoiceExtras 恢复。 + VoiceCommandLogger.dropped("voice cold launch notify failed", t.getMessage()); + forwardOrPersistFallback(ctx, filmName, autoPlay, requestId, sender); + } + } + + /** + * FullScreenIntent 路径也失败时的最末兜底:尝试 forward 到主进程,主进程已激活可消费; + * 否则把 extras 写入 SharedPreferences,等下一次自然启动路径消费。 + * 持久化策略由 {@code MainActivity.capturePendingVoiceExtras} 配套消费。 + */ + private static void forwardOrPersistFallback(Context ctx, String filmName, String autoPlay, + String requestId, String sender) { + try { + Intent fwd = new Intent(VoiceInternalReceiver.ACTION_VOICE_FORWARD); + fwd.setPackage(ctx.getPackageName()); + fwd.putExtra(VoiceExtras.EXTRA_OEM_FILM_NAME, filmName); + fwd.putExtra(VoiceExtras.EXTRA_OEM_AUTOPLAY, autoPlay); + fwd.putExtra(VoiceExtras.EXTRA_OEM_REQUEST_ID, requestId); + fwd.putExtra(VoiceExtras.EXTRA_OEM_SOURCE_APP, sender); + ctx.sendBroadcast(fwd); + } catch (Throwable t2) { + VoiceCommandLogger.dropped("voice cold launch fallback failed", t2.getMessage()); + } + // 持久化 pending extras,下次自然启动路径消费 + try { + android.content.SharedPreferences sp = ctx.getSharedPreferences( + "voice_pending", Context.MODE_PRIVATE); + sp.edit() + .putString(VoiceExtras.SP_KEY_PENDING_FILM_NAME, filmName) + .putString(VoiceExtras.SP_KEY_PENDING_AUTOPLAY, autoPlay) + .putString(VoiceExtras.SP_KEY_PENDING_REQUEST_ID, requestId) + .putString(VoiceExtras.SP_KEY_PENDING_SENDER, sender) + .putBoolean(VoiceExtras.SP_PENDING_VOICE_FLAG, true) + .apply(); + } catch (Throwable ignored) { + // 持久化失败也不致命,forward 广播已尝试 + } + } + + /** + * 读取候选影片名列表。 + *

创维 SDK 投递的 {@code videoList} 是 String extra(JSON 数组字符串,对应 adb 的 {@code --es}); + * 兼容 String[] extra({@code --esa})便于旧联调脚本。 + */ + private static String[] resolveVideoList(Intent intent) { + String[] asArray = intent.getStringArrayExtra(EXTRA_VIDEO_LIST); + if (asArray != null && asArray.length > 0) return asArray; + + String asJson = intent.getStringExtra(EXTRA_VIDEO_LIST); + if (TextUtils.isEmpty(asJson)) return null; + try { + JSONArray array = new JSONArray(asJson); + String[] result = new String[array.length()]; + for (int i = 0; i < array.length(); i++) { + result[i] = array.optString(i, ""); + } + return result; + } catch (JSONException ex) { + // 非 JSON 数组格式时按单个裸片名处理 + return new String[]{asJson}; + } + } + + private static String firstNonEmpty(String[] values) { + if (values == null) return null; + for (String v : values) { + if (!TextUtils.isEmpty(v)) return v; + } + return null; + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/voice/guard/AppStateGuard.java b/app/src/main/java/com/pukka/jiecheng/voice/guard/AppStateGuard.java new file mode 100644 index 0000000..59e5b16 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/guard/AppStateGuard.java @@ -0,0 +1,8 @@ +package com.pukka.jiecheng.voice.guard; + +public interface AppStateGuard { + + boolean isActivated(); + + boolean hasOpenDialog(); +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/guard/DefaultAppStateGuard.java b/app/src/main/java/com/pukka/jiecheng/voice/guard/DefaultAppStateGuard.java new file mode 100644 index 0000000..e3e6ee0 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/guard/DefaultAppStateGuard.java @@ -0,0 +1,46 @@ +package com.pukka.jiecheng.voice.guard; + +import android.content.Context; + +import com.pukka.jiecheng.utils.ActivityMonitor; + +/** + * 默认应用激活态判定:只看主进程是否已启动({@link ActivityMonitor} 单例是否初始化), + * 不再依赖栈顶 Activity 类型。 + * + *

历史版本曾通过栈顶 Activity 类型判定"激活"——栈顶是 MainActivity / endsWith + * "MainActivity" 即认为未激活、其他即认为已激活。这套判定在 {@code VideoDetailActivity} + * 全屏播放时被 cold path 误判触发:用户在 VideoDetailActivity 收到语音广播时, + * {@code guard.isActivated()} 因栈管理竞态返回 false,receiver 走 cold path 启动 + * MainActivity,被 FLAG_ACTIVITY_CLEAR_TOP 清掉栈里所有 Activity(包含 + * VideoDetailActivity),再由 MainActivity → IndexActivity → replayColdStartVoiceCommands + * 走一遍完整链路才能消费命令,体验上是「播放页被销毁重走 MainActivity → IndexActivity + * → VideoDetailActivity」。 + * + *

新策略下,只要主进程在({@link ActivityMonitor#mAppStatusManager} 已初始化)就视为 + * 激活,所有页面(IndexActivity / ListActivity / VideoDetailActivity / SearchActivity 等) + * 都已具备消费语音命令的 ActivityMonitor 上下文与 navigator 入口,由 navigator 自行决定 + * 导航到具体页面,无需 receiver 层 cold launch 来"重建栈"。 + * + *

唯一会走到 cold path 的场景是主进程未启动({@link ActivityMonitor} 单例为 null), + * 由 {@link com.pukka.jiecheng.voice.receiver.VoiceInternalReceiver} / + * {@link com.pukka.jiecheng.voice.receiver.VoiceCommandReceiver} 走 cold launch。 + */ +public class DefaultAppStateGuard implements AppStateGuard { + + private final Context appContext; + + public DefaultAppStateGuard(Context appContext) { + this.appContext = appContext; + } + + @Override + public boolean isActivated() { + return ActivityMonitor.mAppStatusManager != null; + } + + @Override + public boolean hasOpenDialog() { + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/log/VoiceCommandLogger.java b/app/src/main/java/com/pukka/jiecheng/voice/log/VoiceCommandLogger.java new file mode 100644 index 0000000..97fa8fd --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/log/VoiceCommandLogger.java @@ -0,0 +1,55 @@ +package com.pukka.jiecheng.voice.log; + +import android.util.Log; + +import com.pukka.jiecheng.voice.protocol.VoiceCommand; +import com.pukka.jiecheng.voice.protocol.VoiceCallbackResult; +import com.pukka.jiecheng.voice.protocol.VoiceProtocolConstants; + +public class VoiceCommandLogger { + + private static final String TAG = VoiceProtocolConstants.LOG_TAG; + + public static void received(String requestId, String sourceApp, String sourcePlatform, + String commandType, String payload) { + Log.i(TAG, "[received] requestId=" + safe(requestId) + + " sourceApp=" + safe(sourceApp) + + " sourcePlatform=" + safe(sourcePlatform) + + " commandType=" + safe(commandType) + + " payload=" + safe(payload)); + } + + public static void parsed(VoiceCommand cmd) { + Log.i(TAG, "[parsed] requestId=" + safe(cmd.getRequestId()) + + " commandType=" + (cmd.getCommandType() == null ? "null" : cmd.getCommandType().name())); + } + + public static void executing(VoiceCommand cmd) { + Log.i(TAG, "[executing] requestId=" + safe(cmd.getRequestId()) + + " commandType=" + (cmd.getCommandType() == null ? "null" : cmd.getCommandType().name())); + } + + public static void finished(VoiceCommand cmd, VoiceCallbackResult result, + long durationMs, String currentPage) { + Log.i(TAG, "[finished] requestId=" + safe(cmd == null ? null : cmd.getRequestId()) + + " success=" + (result != null && result.isSuccess()) + + " resultCode=" + safe(result == null ? null : result.getResultCode()) + + " resultMessage=" + safe(result == null ? null : result.getResultMessage()) + + " durationMs=" + durationMs + + " currentPage=" + safe(currentPage)); + } + + public static void dropped(String reason, String detail) { + Log.w(TAG, "[dropped] reason=" + safe(reason) + " detail=" + safe(detail)); + } + + public static void enqueued(String requestId) { + Log.i(TAG, "[enqueued] requestId=" + safe(requestId) + " (cold start queue)"); + } + + public static void replaying(String requestId) { + Log.i(TAG, "[replaying] requestId=" + safe(requestId)); + } + + private static String safe(String s) { return s == null ? "" : s; } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/parser/SkyworthVodParser.java b/app/src/main/java/com/pukka/jiecheng/voice/parser/SkyworthVodParser.java new file mode 100644 index 0000000..0e25661 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/parser/SkyworthVodParser.java @@ -0,0 +1,169 @@ +package com.pukka.jiecheng.voice.parser; + +import android.content.Intent; +import android.text.TextUtils; + +import com.pukka.jiecheng.voice.protocol.VoiceCommand; +import com.pukka.jiecheng.voice.protocol.VoiceCommandType; +import com.pukka.jiecheng.voice.protocol.VoiceExtras; +import com.pukka.jiecheng.voice.service.VoiceCommandService; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * 创维语音识别平台 Parser(仅点播 / VOD 方向)。 + * + *

本类提供两种入口: + *

    + *
  • 实例方法 {@link #parse(String, String)}:创维广播的归一化由 + * {@link com.pukka.jiecheng.voice.adapter.SkyworthVoiceAdapter} 完成, + * Adapter 把创维 extras 收拢成统一协议 JSON + * ({@code {"jumpType":"FILM","filmName":"..."}})后交给本 Parser, + * 产物是带 {@code filmName} / {@code autoPlay} payload 的 {@code OPEN_FILM_DETAIL} 命令。
  • + *
  • 静态工具 {@link #parseJump} / {@link #parseFromForward}:跨进程共享入口, + * :voice 进程 {@code SkyworthVoiceAdapter} 与主进程 {@code VoiceInternalReceiver} + * 都通过同一份静态工具从 OEM 原始字段构造 {@link VoiceCommand}, + * 保证解析逻辑单点。
  • + *
+ */ +public class SkyworthVodParser implements VoicePlatformParser { + + /** 内部统一协议 commandType,Adapter 把创维广播归一化成 JUMP。 */ + public static final String COMMAND_JUMP = "JUMP"; + + /** 创维 VOD 场景下唯一的 jumpType。 */ + public static final String JUMP_FILM = "FILM"; + + /** :voice 与主进程跨进程共享的 sourcePlatform 标识。 */ + private static final String SOURCE_PLATFORM_SKYWORTH = "skyworth"; + + @Override + public VoiceCommand parse(String commandType, String payloadJson) { + if (commandType == null) { + throw new IllegalArgumentException("commandType is null"); + } + if (!COMMAND_JUMP.equals(commandType)) { + throw new IllegalArgumentException( + "unsupported commandType for skyworth: " + commandType); + } + + Map payload = parsePayload(payloadJson); + String jumpType = payload.get("jumpType"); + if (jumpType == null) jumpType = ""; + if (!JUMP_FILM.equals(jumpType)) { + throw new IllegalArgumentException( + "unsupported jumpType for skyworth vod: " + jumpType); + } + + String filmName = payload.get("filmName"); + if (filmName == null || filmName.isEmpty()) { + throw new IllegalArgumentException("filmName is empty"); + } + + // 创维只给候选影片名 → 映射成 OPEN_FILM_DETAIL, + // 由 DefaultPageNavigator.searchAndOpenFilmDetail() 走 + // /search/getsearchlist.do 命中第一条 contentcode 开详情; + // 搜索无果时兜底开 SearchActivity,无需再写新逻辑。 + // autoPlay 字段由 Adapter 归一化时默认注入(true),Intent extra 可覆盖; + // 这里只做透传,不改变语义,其他上游私有字段一律丢弃(防御性)。 + Map cmdPayload = new HashMap<>(); + cmdPayload.put("filmName", filmName); + String autoPlay = payload.get("autoPlay"); + if (autoPlay != null) cmdPayload.put("autoPlay", autoPlay); + return VoiceCommand.builder() + .commandType(VoiceCommandType.OPEN_FILM_DETAIL) + .payload(cmdPayload) + .build(); + } + + private static Map parsePayload(String payloadJson) { + Map result = new HashMap<>(); + if (payloadJson == null) return result; + try { + JSONObject obj = new JSONObject(payloadJson); + Iterator it = obj.keys(); + while (it.hasNext()) { + String k = it.next(); + Object v = obj.opt(k); + result.put(k, v == null ? "" : v.toString()); + } + } catch (JSONException e) { + throw new IllegalArgumentException( + "skyworth payload is not a JSON object: " + payloadJson, e); + } + return result; + } + + /** + * 从 OEM 原始字段构造 {@link VoiceCommand}。 + * :voice 与主进程共用,构造逻辑单点。 + * + * @param service 主进程 {@link VoiceCommandService} 实例(已持有 Coordinator 引用) + * @param filmName 候选影片名(必填) + * @param autoPlay 是否自动播放;空时归一为 "true" + * @param requestId 请求 ID(由调用方生成) + * @param sourceApp 投递方包名 + * @return 构造的 {@code OPEN_FILM_DETAIL} 命令;filmName 为空时返回 null + */ + public static VoiceCommand parseJump(VoiceCommandService service, + String filmName, String autoPlay, + String requestId, String sourceApp) { + if (service == null || TextUtils.isEmpty(filmName)) return null; + String normalizedPayload = "{\"jumpType\":\"FILM\",\"filmName\":\"" + + jsonEscape(filmName) + + "\",\"autoPlay\":\"" + + jsonEscape(TextUtils.isEmpty(autoPlay) ? "true" : autoPlay) + + "\"}"; + + VoiceCommand parsed; + try { + parsed = service.getCoordinator() + .parse(COMMAND_JUMP, SOURCE_PLATFORM_SKYWORTH, normalizedPayload); + } catch (IllegalArgumentException ex) { + return null; + } + + return VoiceCommand.builder() + .commandType(parsed.getCommandType()) + .requestId(requestId) + .sourceApp(sourceApp) + .sourcePlatform(SOURCE_PLATFORM_SKYWORTH) + .timestamp(System.currentTimeMillis()) + .payload(parsed.getPayload()) + .build(); + } + + /** + * 从跨进程 forward Intent 还原 OEM 原始字段并构造 {@link VoiceCommand}。 + */ + public static VoiceCommand parseFromForward(VoiceCommandService service, Intent intent) { + if (service == null || intent == null) return null; + return parseJump(service, + intent.getStringExtra(VoiceExtras.EXTRA_OEM_FILM_NAME), + intent.getStringExtra(VoiceExtras.EXTRA_OEM_AUTOPLAY), + intent.getStringExtra(VoiceExtras.EXTRA_OEM_REQUEST_ID), + intent.getStringExtra(VoiceExtras.EXTRA_OEM_SOURCE_APP)); + } + + private static String jsonEscape(String v) { + if (v == null) return ""; + StringBuilder out = new StringBuilder(v.length() + 8); + for (int i = 0; i < v.length(); i++) { + char c = v.charAt(i); + switch (c) { + case '"': out.append("\\\""); break; + case '\\': out.append("\\\\"); break; + case '\n': out.append("\\n"); break; + case '\r': out.append("\\r"); break; + case '\t': out.append("\\t"); break; + default: out.append(c); + } + } + return out.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/parser/VendorAParser.java b/app/src/main/java/com/pukka/jiecheng/voice/parser/VendorAParser.java new file mode 100644 index 0000000..6b73d0e --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/parser/VendorAParser.java @@ -0,0 +1,189 @@ +package com.pukka.jiecheng.voice.parser; + +import com.pukka.jiecheng.voice.protocol.VoiceCommand; +import com.pukka.jiecheng.voice.protocol.VoiceCommandType; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class VendorAParser implements VoicePlatformParser { + + public static final String JUMP_FILM = "FILM"; + public static final String JUMP_CATEGORY = "CATEGORY"; + public static final String JUMP_ACTOR = "ACTOR"; + public static final String JUMP_DIRECTOR = "DIRECTOR"; + + public static final String ACTION = "action"; + + @Override + public VoiceCommand parse(String commandType, String payloadJson) { + Map payload = SimpleJsonParser.parseObject(payloadJson); + return parse(commandType, payload); + } + + public VoiceCommand parse(String commandType, Map payload) { + if (commandType == null) throw new IllegalArgumentException("commandType is null"); + switch (commandType) { + case "JUMP": + return parseJumpPayload(payload == null ? Collections.emptyMap() : payload); + case "SEARCH": + return parseSearchPayload(payload == null ? Collections.emptyMap() : payload); + case "CONTROL": + return parseControlPayload(payload == null ? Collections.emptyMap() : payload); + default: + throw new IllegalArgumentException("unsupported commandType: " + commandType); + } + } + + public VoiceCommand parseJumpPayload(Map payload) { + String jumpType = payload.get("jumpType"); + if (jumpType == null) jumpType = ""; + switch (jumpType) { + case JUMP_FILM: return build(VoiceCommandType.OPEN_FILM_DETAIL, payload); + case JUMP_CATEGORY: return build(VoiceCommandType.OPEN_CATEGORY, payload); + case JUMP_ACTOR: return build(VoiceCommandType.OPEN_ACTOR, payload); + case JUMP_DIRECTOR: return build(VoiceCommandType.OPEN_DIRECTOR, payload); + default: + throw new IllegalArgumentException("unsupported jumpType: " + jumpType); + } + } + + public VoiceCommand parseSearchPayload(Map payload) { + return build(VoiceCommandType.SEARCH_FILM, payload); + } + + public VoiceCommand parseControlPayload(Map payload) { + String action = payload.get(ACTION); + if (action == null) action = ""; + VoiceCommandType type = mapControlAction(action); + if (type == null) { + throw new IllegalArgumentException("unsupported control action: " + action); + } + return build(type, payload); + } + + private static VoiceCommandType mapControlAction(String action) { + switch (action) { + case "PLAY": return VoiceCommandType.PLAYBACK_RESUME; + case "PAUSE": return VoiceCommandType.PLAYBACK_PAUSE; + case "FULLSCREEN_ENTER": return VoiceCommandType.PLAYBACK_ENTER_FULLSCREEN; + case "FULLSCREEN_EXIT": return VoiceCommandType.PLAYBACK_EXIT_FULLSCREEN; + case "SEEK_FORWARD": return VoiceCommandType.PLAYBACK_SEEK_FORWARD; + case "SEEK_BACKWARD": return VoiceCommandType.PLAYBACK_SEEK_BACKWARD; + case "SEEK_TO": return VoiceCommandType.PLAYBACK_SEEK_TO; + case "PREV": return VoiceCommandType.PLAYBACK_PREVIOUS; + case "NEXT": return VoiceCommandType.PLAYBACK_NEXT; + case "SELECT_INDEX": return VoiceCommandType.PLAYBACK_SELECT_INDEX; + case "PAGE_NEXT": return VoiceCommandType.PLAYBACK_PAGE_NEXT; + case "PAGE_PREV": return VoiceCommandType.PLAYBACK_PAGE_PREV; + case "GOTO_PAGE": return VoiceCommandType.PLAYBACK_GOTO_PAGE; + default: return null; + } + } + + private static VoiceCommand build(VoiceCommandType type, Map payload) { + VoiceCommand cmd = VoiceCommand.builder().commandType(type).build(); + if (payload != null) { + for (Map.Entry e : payload.entrySet()) { + cmd.putPayload(e.getKey(), e.getValue() == null ? "" : e.getValue()); + } + } + return cmd; + } + + public static final class SimpleJsonParser { + + private SimpleJsonParser() {} + + public static Map parseObject(String json) { + Map result = new HashMap<>(); + if (json == null) return result; + String trimmed = json.trim(); + if (trimmed.isEmpty() || "{}".equals(trimmed)) return result; + if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) { + throw new IllegalArgumentException("payload is not a JSON object: " + json); + } + String body = trimmed.substring(1, trimmed.length() - 1).trim(); + if (body.isEmpty()) return result; + + int i = 0; + int len = body.length(); + while (i < len) { + while (i < len && Character.isWhitespace(body.charAt(i))) i++; + if (i >= len) break; + if (body.charAt(i) != '"') { + throw new IllegalArgumentException("expected '\"' at pos " + i + " in " + json); + } + StringBuilder keyBuf = new StringBuilder(); + i++; + while (i < len && body.charAt(i) != '"') { + char c = body.charAt(i); + if (c == '\\' && i + 1 < len) { + keyBuf.append(unescape(body.charAt(i + 1))); + i += 2; + } else { + keyBuf.append(c); + i++; + } + } + if (i >= len) throw new IllegalArgumentException("unterminated key in " + json); + i++; + String key = keyBuf.toString(); + + while (i < len && Character.isWhitespace(body.charAt(i))) i++; + if (i >= len || body.charAt(i) != ':') { + throw new IllegalArgumentException("expected ':' after key " + key + " in " + json); + } + i++; + while (i < len && Character.isWhitespace(body.charAt(i))) i++; + if (i >= len) { + throw new IllegalArgumentException("missing value for key " + key + " in " + json); + } + + String value; + if (body.charAt(i) == '"') { + StringBuilder valBuf = new StringBuilder(); + i++; + while (i < len && body.charAt(i) != '"') { + char c = body.charAt(i); + if (c == '\\' && i + 1 < len) { + valBuf.append(unescape(body.charAt(i + 1))); + i += 2; + } else { + valBuf.append(c); + i++; + } + } + if (i >= len) { + throw new IllegalArgumentException("unterminated value for key " + key + " in " + json); + } + i++; + value = valBuf.toString(); + } else { + int start = i; + while (i < len && body.charAt(i) != ',' && !Character.isWhitespace(body.charAt(i))) i++; + value = body.substring(start, i).trim(); + } + + result.put(key, value); + + while (i < len && Character.isWhitespace(body.charAt(i))) i++; + if (i < len && body.charAt(i) == ',') i++; + } + return result; + } + + private static char unescape(char c) { + switch (c) { + case 'n': return '\n'; + case 'r': return '\r'; + case 't': return '\t'; + case '"': return '"'; + case '\\': return '\\'; + case '/': return '/'; + default: return c; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/parser/VendorBParser.java b/app/src/main/java/com/pukka/jiecheng/voice/parser/VendorBParser.java new file mode 100644 index 0000000..8ff5fac --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/parser/VendorBParser.java @@ -0,0 +1,12 @@ +package com.pukka.jiecheng.voice.parser; + +import com.pukka.jiecheng.voice.protocol.VoiceCommand; + +public class VendorBParser implements VoicePlatformParser { + + @Override + public VoiceCommand parse(String commandType, String payloadJson) { + throw new UnsupportedOperationException( + "vendorB parser is not yet implemented in MVP, commandType=" + commandType); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/parser/VoicePlatformParser.java b/app/src/main/java/com/pukka/jiecheng/voice/parser/VoicePlatformParser.java new file mode 100644 index 0000000..ed7a8be --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/parser/VoicePlatformParser.java @@ -0,0 +1,8 @@ +package com.pukka.jiecheng.voice.parser; + +import com.pukka.jiecheng.voice.protocol.VoiceCommand; + +public interface VoicePlatformParser { + + VoiceCommand parse(String commandType, String payloadJson); +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCallbackResult.java b/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCallbackResult.java new file mode 100644 index 0000000..290db8f --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCallbackResult.java @@ -0,0 +1,70 @@ +package com.pukka.jiecheng.voice.protocol; + +public final class VoiceCallbackResult { + + public static final String CODE_OK = "OK"; + public static final String CODE_DEVICE_NOT_ACTIVATED = "DEVICE_NOT_ACTIVATED"; + public static final String CODE_INVALID_PAYLOAD = "INVALID_PAYLOAD"; + public static final String CODE_THROTTLED = "THROTTLED"; + public static final String CODE_NOT_IN_PLAYER = "NOT_IN_PLAYER"; + public static final String CODE_DIALOG_BUSY = "DIALOG_BUSY"; + public static final String CODE_FORBIDDEN = "FORBIDDEN"; + + private final String requestId; + private final boolean success; + private final String resultCode; + private final String resultMessage; + private final String data; + + private VoiceCallbackResult(String requestId, boolean success, + String resultCode, String resultMessage, String data) { + this.requestId = requestId; + this.success = success; + this.resultCode = resultCode; + this.resultMessage = resultMessage; + this.data = data; + } + + public static VoiceCallbackResult success(String requestId, String resultCode, String data) { + return new VoiceCallbackResult(requestId, true, resultCode, data, data); + } + + public static VoiceCallbackResult failure(String requestId, String resultCode, String message) { + return new VoiceCallbackResult(requestId, false, resultCode, message, null); + } + + public String getRequestId() { return requestId; } + public boolean isSuccess() { return success; } + public String getResultCode() { return resultCode; } + public String getResultMessage() { return resultMessage; } + public String getData() { return data; } + + public String toJsonString() { + StringBuilder sb = new StringBuilder(128); + sb.append('{'); + sb.append("\"requestId\":\"").append(escape(requestId)).append("\","); + sb.append("\"success\":").append(success).append(","); + sb.append("\"resultCode\":\"").append(escape(resultCode)).append("\","); + sb.append("\"resultMessage\":\"").append(escape(resultMessage)).append("\","); + sb.append("\"data\":\"").append(escape(data)).append("\""); + sb.append('}'); + return sb.toString(); + } + + private static String escape(String v) { + if (v == null) return ""; + StringBuilder sb = new StringBuilder(v.length() + 8); + for (int i = 0; i < v.length(); i++) { + char c = v.charAt(i); + switch (c) { + case '\\': sb.append("\\\\"); break; + case '"': sb.append("\\\""); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: sb.append(c); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCommand.java b/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCommand.java new file mode 100644 index 0000000..2eb5442 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCommand.java @@ -0,0 +1,73 @@ +package com.pukka.jiecheng.voice.protocol; + +import java.util.HashMap; +import java.util.Map; + +public final class VoiceCommand { + + private final VoiceCommandType commandType; + private final String requestId; + private final String sourceApp; + private final String sourcePlatform; + private final String callbackAction; + private final long timestamp; + private final Map payload; + + private VoiceCommand(Builder b) { + this.commandType = b.commandType; + this.requestId = b.requestId; + this.sourceApp = b.sourceApp; + this.sourcePlatform = b.sourcePlatform; + this.callbackAction = b.callbackAction; + this.timestamp = b.timestamp; + this.payload = b.payload != null ? b.payload : new HashMap<>(); + } + + public VoiceCommandType getCommandType() { return commandType; } + public String getRequestId() { return requestId; } + public String getSourceApp() { return sourceApp; } + public String getSourcePlatform() { return sourcePlatform; } + public String getCallbackAction() { return callbackAction; } + public long getTimestamp() { return timestamp; } + public Map getPayload() { return payload; } + + public String getPayloadValue(String key) { + if (key == null || payload == null) return null; + return payload.get(key); + } + + public void putPayload(String key, String value) { + if (key == null) return; + payload.put(key, value); + } + + public boolean isSameThrottleKey(VoiceCommand other) { + if (other == null) return false; + return this.commandType == other.commandType; + } + + public static Builder builder() { return new Builder(); } + + public static final class Builder { + private VoiceCommandType commandType; + private String requestId; + private String sourceApp; + private String sourcePlatform; + private String callbackAction; + private long timestamp; + private Map payload; + + public Builder commandType(VoiceCommandType v) { this.commandType = v; return this; } + public Builder requestId(String v) { this.requestId = v; return this; } + public Builder sourceApp(String v) { this.sourceApp = v; return this; } + public Builder sourcePlatform(String v) { this.sourcePlatform = v; return this; } + public Builder callbackAction(String v) { this.callbackAction = v; return this; } + public Builder timestamp(long v) { this.timestamp = v; return this; } + public Builder payload(Map v) { this.payload = v; return this; } + + public VoiceCommand build() { + if (commandType == null) throw new IllegalStateException("commandType is required"); + return new VoiceCommand(this); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCommandType.java b/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCommandType.java new file mode 100644 index 0000000..18c1398 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceCommandType.java @@ -0,0 +1,24 @@ +package com.pukka.jiecheng.voice.protocol; + +public enum VoiceCommandType { + OPEN_FILM_DETAIL, + OPEN_CATEGORY, + OPEN_ACTOR, + OPEN_DIRECTOR, + OPEN_SEARCH, + GO_HOME, + SEARCH_FILM, + PLAYBACK_RESUME, + PLAYBACK_PAUSE, + PLAYBACK_ENTER_FULLSCREEN, + PLAYBACK_EXIT_FULLSCREEN, + PLAYBACK_SEEK_FORWARD, + PLAYBACK_SEEK_BACKWARD, + PLAYBACK_SEEK_TO, + PLAYBACK_PREVIOUS, + PLAYBACK_NEXT, + PLAYBACK_SELECT_INDEX, + PLAYBACK_PAGE_NEXT, + PLAYBACK_PAGE_PREV, + PLAYBACK_GOTO_PAGE +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceExtras.java b/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceExtras.java new file mode 100644 index 0000000..ff0a66f --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceExtras.java @@ -0,0 +1,45 @@ +package com.pukka.jiecheng.voice.protocol; + +/** + * 语音冷启动跨进程共享 extras / SP 键常量。 + * + *

OEM 创维广播原始字段(filmName / autoPlay / requestId / sourceApp)跨 + * :voice 进程 Adapter → 主进程 VoiceInternalReceiver → MainActivity 传递, + * 为避免 4 个文件各自重复声明导致漂移,统一在此维护。 + * + *

详见 {@code docs/2026-08-28-语音冷启动链路设计方案.md} §4。 + */ +public final class VoiceExtras { + + /** Intent extra:标记本次启动携带语音冷启动待入队命令(boolean)。 */ + public static final String EXTRA_VOICE_PENDING = "__voice_pending"; + + /** Intent extra:候选影片名(OEM 归一化后第一个非空)。 */ + public static final String EXTRA_OEM_FILM_NAME = "__voice_oem_film_name"; + + /** Intent extra:是否自动播放("true" / "false";空时归一为 "true")。 */ + public static final String EXTRA_OEM_AUTOPLAY = "__voice_oem_autoplay"; + + /** Intent extra:请求 ID(Adapter 在 :voice 进程生成,"skyworth-{ts}-{rand}")。 */ + public static final String EXTRA_OEM_REQUEST_ID = "__voice_request_id"; + + /** Intent extra:投递方包名(intent.getPackage();空时默认 "com.skyworth.smarthotel")。 */ + public static final String EXTRA_OEM_SOURCE_APP = "__voice_source_app"; + + /** SharedPreferences 键:是否有待入队命令(boolean)。 */ + public static final String SP_PENDING_VOICE_FLAG = "__voice_pending_flag"; + + /** SharedPreferences 键:持久化的 filmName(用于升级重启 / 进程被杀恢复)。 */ + public static final String SP_KEY_PENDING_FILM_NAME = "__voice_pending_film_name"; + + /** SharedPreferences 键:持久化的 autoPlay。 */ + public static final String SP_KEY_PENDING_AUTOPLAY = "__voice_pending_autoplay"; + + /** SharedPreferences 键:持久化的 requestId。 */ + public static final String SP_KEY_PENDING_REQUEST_ID = "__voice_pending_request_id"; + + /** SharedPreferences 键:持久化的 sourceApp。 */ + public static final String SP_KEY_PENDING_SENDER = "__voice_pending_source_app"; + + private VoiceExtras() {} +} diff --git a/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceProtocolConstants.java b/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceProtocolConstants.java new file mode 100644 index 0000000..9dbec45 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/protocol/VoiceProtocolConstants.java @@ -0,0 +1,23 @@ +package com.pukka.jiecheng.voice.protocol; + +/** + * 语音协议常量。 + */ +public final class VoiceProtocolConstants { + + public static final String ACTION_VOICE_COMMAND = "com.pukka.jiecheng.action.VOICE_COMMAND"; + public static final String PERMISSION_VOICE_COMMAND = "com.pukka.jiecheng.permission.VOICE_COMMAND"; + public static final String APP_PACKAGE = "com.pukka.jiecheng"; + public static final String RECEIVER_COMPONENT = "com.pukka.jiecheng/.voice.receiver.VoiceCommandReceiver"; + public static final String LOG_TAG = "VoiceCommand"; + + public static final String EXTRA_REQUEST_ID = "requestId"; + public static final String EXTRA_SOURCE_APP = "sourceApp"; + public static final String EXTRA_SOURCE_PLATFORM = "sourcePlatform"; + public static final String EXTRA_COMMAND_TYPE = "commandType"; + public static final String EXTRA_PAYLOAD = "payload"; + public static final String EXTRA_CALLBACK_ACTION = "callbackAction"; + public static final String EXTRA_TIMESTAMP = "timestamp"; + + private VoiceProtocolConstants() {} +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/queue/CommandQueue.java b/app/src/main/java/com/pukka/jiecheng/voice/queue/CommandQueue.java new file mode 100644 index 0000000..da2680c --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/queue/CommandQueue.java @@ -0,0 +1,49 @@ +package com.pukka.jiecheng.voice.queue; + +import com.pukka.jiecheng.voice.protocol.VoiceCommand; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class CommandQueue { + + public static final int DEFAULT_CAPACITY = 32; + + private final int capacity; + private final LinkedList commands = new LinkedList<>(); + + public CommandQueue() { + this(DEFAULT_CAPACITY); + } + + public CommandQueue(int capacity) { + this.capacity = Math.max(1, capacity); + } + + public synchronized void enqueue(VoiceCommand cmd) { + if (cmd == null) return; + while (commands.size() >= capacity) { + commands.pollFirst(); + } + commands.addLast(cmd); + } + + public synchronized int size() { return commands.size(); } + + public synchronized boolean isEmpty() { return commands.isEmpty(); } + + public synchronized VoiceCommand peek() { return commands.peekFirst(); } + + public synchronized List snapshot() { + return new ArrayList<>(commands); + } + + public synchronized List drainAll() { + if (commands.isEmpty()) return Collections.emptyList(); + List all = new ArrayList<>(commands); + commands.clear(); + return all; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/receiver/BootCompletedReceiver.java b/app/src/main/java/com/pukka/jiecheng/voice/receiver/BootCompletedReceiver.java new file mode 100644 index 0000000..7f91db6 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/receiver/BootCompletedReceiver.java @@ -0,0 +1,78 @@ +/** + * @file BootCompletedReceiver.java + * @description 开机完成广播接收器。监听 BOOT_COMPLETED 与 LOCKED_BOOT_COMPLETED,拉起 :voice 进程的 VoiceKeepAliveService 前台服务,从设备开机起就持续监听 OEM 创维语音广播。 + * @author ainetv + * @date 2026-08-31 + */ +package com.pukka.jiecheng.voice.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.util.Log; + +import androidx.core.content.ContextCompat; + +import com.pukka.jiecheng.MainActivity; +import com.pukka.jiecheng.voice.service.VoiceKeepAliveService; + +/** + * 开机完成广播接收器。 + * + *

Manifest 静态注册,受系统权限 {@code RECEIVE_BOOT_COMPLETED} 保护。 + * 双重职责: + *

    + *
  1. 拉起 {@link VoiceKeepAliveService} 进入 :voice 进程,从设备开机起就持续 + * 监听 OEM 创维语音广播;
  2. + *
  3. 注册 {@link KeepAlivePingReceiver#scheduleFirstPing} 自广播兜底—— + * 一旦主路径因 STOPPED / ROM 拦截 / 5s ANR 失败, + * {@link KeepAlivePingReceiver} 将在 5 秒后接力重启 :voice 进程。
  4. + *
+ * + *

Android 8+ 关键约束:从 {@code BOOT_COMPLETED} 后台广播启服务必须走 + * {@link ContextCompat#startForegroundService(Context, Intent)},服务在 5 秒内必须 + * 调 {@code startForeground(...)},否则 ANR 崩溃。 + */ +public class BootCompletedReceiver extends BroadcastReceiver { + + private static final String LOG_TAG = "VoiceCommand"; + + @Override + public void onReceive(Context ctx, Intent intent) { + if (ctx == null || intent == null) return; + String action = intent.getAction(); + Log.i(LOG_TAG, "voice keepalive service start requested, action=" + action); + boolean isBootCompleted = Intent.ACTION_BOOT_COMPLETED.equals(action); + if (!isBootCompleted) { + return; + } + try { + // 启动顺序关键:必须先 cold-launch MainActivity,再启 FGS。 + // 1) Receiver 由系统启动,startActivity 不受 Android 12+ + // Background Activity Start 限制; + // 2) MainActivity 起来后 → IndexActivity 在栈顶 → 主进程 process state=HOME + // (前台),后续主进程内任何 startActivity 都合法; + // 3) 再启 VoiceKeepAliveService(manifest 已去 :voice 后缀,跑在主进程), + // FGS 内注册 SkyworthVoiceAdapter 监听 OEM 创维 ACTION_VOD 广播。 + // 如果先启 Service 再启 Activity:Service.onCreate 时主进程没前台 Activity, + // FGS 类型 specialUse 不在 Android 12+ 豁免列表,自己 startActivity 会被拦。 + Context appCtx = ctx.getApplicationContext(); + Context targetCtx = appCtx != null ? appCtx : ctx; + +// Intent launcher = new Intent(targetCtx, MainActivity.class); +// launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); +// ctx.startActivity(launcher); + + ContextCompat.startForegroundService(targetCtx, + new Intent(targetCtx, VoiceKeepAliveService.class)); + + // 自广播兜底:万一主路径因 STOPPED / 5s 超时 / ROM 拦截失败, + // 5s 后 KeepAlivePingReceiver 会自动接力重启 :voice。 + // 自广播是同包 explicit broadcast,不受 STOPPED 状态限制。 + KeepAlivePingReceiver.scheduleFirstPing(targetCtx); + } catch (Throwable t) { + Log.w(LOG_TAG, "voice keepalive startup failed: " + t.getMessage()); + } + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/voice/receiver/KeepAlivePingReceiver.java b/app/src/main/java/com/pukka/jiecheng/voice/receiver/KeepAlivePingReceiver.java new file mode 100644 index 0000000..f6cb4b5 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/receiver/KeepAlivePingReceiver.java @@ -0,0 +1,146 @@ +/** + * @file KeepAlivePingReceiver.java + * @description 自广播兜底接收器,处理 com.pukka.jiecheng.action.KEEPALIVE_BOOT 自定义 action。 + * 解决 STOPPED 状态 / ROM 拦截 BOOT_COMPLETED 时 :voice 进程无法自启的问题。 + * @author ainetv + * @date 2026-09-01 + */ +package com.pukka.jiecheng.voice.receiver; + +import android.app.ActivityManager; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.SystemClock; +import android.util.Log; + +import androidx.core.content.ContextCompat; + +import com.pukka.jiecheng.voice.service.VoiceKeepAliveService; + +/** + * 自广播兜底接收器。 + * + *

背景:系统 {@code BOOT_COMPLETED} 在以下场景下不会被派发: + *

    + *
  • 应用处于 STOPPED 状态(force-stop / 装机后从未启动 / 部分 ROM OTA 后)
  • + *
  • OEM ROM 白名单未包含本包名(机顶盒常见)
  • + *
  • Receiver 5 秒内未完成 {@code startForeground} 导致 ANR 崩溃
  • + *
+ * + *

解决路径:监听同包内 explicit broadcast {@link #ACTION_KEEPALIVE_BOOT}, + * 通过 {@code AlarmManager} 注册周期性 PendingIntent 给自己投递。 + * 同包 explicit broadcast 不受 STOPPED 状态限制(Android 官方文档 + * §Background execution limits),所以即便主路径失败,仍能由本 receiver 接力 + * 拉起 {@link VoiceKeepAliveService}。 + * + *

触发链路: + *

+ *   BOOT_COMPLETED 或 任意应用入口(MyApplication.onCreate 等)
+ *     └─ AlarmManager.setExactAndAllowWhileIdle → 5s 后
+ *          └─ KEEPALIVE_BOOT 自广播(同包显式投递)
+ *               └─ KeepAlivePingReceiver.onReceive
+ *                    ├─ 检查 :voice 进程,未存活则 startForegroundService
+ *                    └─ 注册下一次 24h 周期 ping 续命
+ * 
+ * + *

Manifest 静态注册,{@code exported=false}(不暴露给外部,仅接收同包显式 Intent)。 + */ +public class KeepAlivePingReceiver extends BroadcastReceiver { + + public static final String ACTION_KEEPALIVE_BOOT = + "com.pukka.jiecheng.action.KEEPALIVE_BOOT"; + + /** Ping 间隔:24 小时。AlarmManager 周期唤醒自广播保持 :voice 长期存活。 */ + public static final long PING_INTERVAL_MS = 24L * 60L * 60L * 1000L; + + /** 首次 ping 延迟:BOOT_COMPLETED 之后给系统留 5s 缓冲,避免与主路径抢进程。 */ + public static final long FIRST_PING_DELAY_MS = 5L * 1000L; + + private static final String LOG_TAG = "VoiceCommand"; + private static final int REQ_CODE_PING = 0x774B4150; // "vKAP" in ascii + + @Override + public void onReceive(Context ctx, Intent intent) { + if (ctx == null || intent == null) return; + if (!ACTION_KEEPALIVE_BOOT.equals(intent.getAction())) { + return; + } + Context appCtx = ctx.getApplicationContext(); + Context targetCtx = appCtx != null ? appCtx : ctx; + + // 1) 检查 :voice 进程是否存活;未存活则启动 FGS + if (!isVoiceProcessAlive(targetCtx)) { + Log.i(LOG_TAG, "keepalive ping: voice process dead, start FGS"); + try { + ContextCompat.startForegroundService(targetCtx, + new Intent(targetCtx, VoiceKeepAliveService.class)); + } catch (Throwable t) { + Log.w(LOG_TAG, "keepalive ping: start FGS failed: " + t.getMessage()); + } + } else { + Log.i(LOG_TAG, "keepalive ping: voice process alive, skip start"); + } + + // 2) 注册下一次 ping,保证链路自续 + scheduleNextPing(targetCtx, PING_INTERVAL_MS); + } + + /** + * 由 {@link BootCompletedReceiver} 或 {@code MyApplication} 调用,注册首次 ping。 + * 5s 后派发一次 KEEPALIVE_BOOT 自广播到本 receiver。 + */ + public static void scheduleFirstPing(Context ctx) { + scheduleNextPing(ctx, FIRST_PING_DELAY_MS); + } + + /** + * 注册下一次 ping。{@code AlarmManager.setExactAndAllowWhileIdle} 在 Doze 模式下 + * 也能保证触发,但 doze 期间有 ~9 分钟延迟——对 24h 周期而言可接受。 + */ + public static void scheduleNextPing(Context ctx, long delayMs) { + if (ctx == null) return; + try { + Intent ping = new Intent(ACTION_KEEPALIVE_BOOT); + ping.setPackage(ctx.getPackageName()); // 显式同包,绕过 STOPPED 限制 + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + PendingIntent pi = PendingIntent.getBroadcast( + ctx, REQ_CODE_PING, ping, flags); + AlarmManager am = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE); + if (am == null) return; + long triggerAt = SystemClock.elapsedRealtime() + delayMs; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + am.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, + triggerAt, pi); + } else { + am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAt, pi); + } + Log.i(LOG_TAG, "keepalive ping scheduled, delay=" + delayMs + "ms"); + } catch (Throwable t) { + Log.w(LOG_TAG, "keepalive ping schedule failed: " + t.getMessage()); + } + } + + /** + * 检测 :voice 进程是否在运行。失败保守返回 false,让上层尝试启动。 + */ + private boolean isVoiceProcessAlive(Context ctx) { + try { + ActivityManager am = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE); + if (am == null) return false; + String voiceProcess = ctx.getPackageName() + ":voice"; + for (ActivityManager.RunningAppProcessInfo p : am.getRunningAppProcesses()) { + if (voiceProcess.equals(p.processName)) return true; + } + return false; + } catch (Throwable t) { + return false; + } + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceCommandReceiver.java b/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceCommandReceiver.java new file mode 100644 index 0000000..f920f0b --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceCommandReceiver.java @@ -0,0 +1,104 @@ +package com.pukka.jiecheng.voice.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; +import android.util.Log; + +import com.pukka.jiecheng.voice.log.VoiceCommandLogger; +import com.pukka.jiecheng.voice.protocol.VoiceCommand; +import com.pukka.jiecheng.voice.protocol.VoiceProtocolConstants; +import com.pukka.jiecheng.voice.service.VoiceCommandService; +public class VoiceCommandReceiver extends BroadcastReceiver { + + /** + * 白名单(文档 §6.2)。 + *

MVP 阶段内置一个联调专用 sourceApp({@code com.test.voicehelper}), + * 便于 {@code test/voice_helper/} 脚本可联调;正式接入第三方语音厂商时按需扩展此列表 + * 并配合 manifest 中的 signature permission 拦截非白名单。 + *

+ */ + public static final String[] DEFAULT_WHITELIST = new String[]{ + "com.test.voicehelper", + }; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null) return; + String action = intent.getAction(); + if (!VoiceProtocolConstants.ACTION_VOICE_COMMAND.equals(action)) { + return; + } + + String requestId = intent.getStringExtra(VoiceProtocolConstants.EXTRA_REQUEST_ID); + String sourceApp = intent.getStringExtra(VoiceProtocolConstants.EXTRA_SOURCE_APP); + String sourcePlatform = intent.getStringExtra(VoiceProtocolConstants.EXTRA_SOURCE_PLATFORM); + String commandType = intent.getStringExtra(VoiceProtocolConstants.EXTRA_COMMAND_TYPE); + String payload = intent.getStringExtra(VoiceProtocolConstants.EXTRA_PAYLOAD); + String callbackAction = intent.getStringExtra(VoiceProtocolConstants.EXTRA_CALLBACK_ACTION); + long timestamp = intent.getLongExtra(VoiceProtocolConstants.EXTRA_TIMESTAMP, 0L); + + if (TextUtils.isEmpty(requestId) || TextUtils.isEmpty(commandType)) { + VoiceCommandLogger.dropped("missing required extras", "requestId=" + requestId); + return; + } + + VoiceCommandLogger.received(requestId, sourceApp, sourcePlatform, commandType, payload); + + Log.w(VoiceProtocolConstants.LOG_TAG, + "[VoiceCommandReceiver] onReceive: A "); +// if (!isWhitelisted(sourceApp)) { +// VoiceCommandLogger.dropped("sourceApp not in whitelist", +// "sourceApp=" + sourceApp + " requestId=" + requestId); +// return; +// } + + VoiceCommandService service = VoiceCommandService.getInstance(context); + VoiceCommand cmd; + try { + cmd = service.getCoordinator().parse(commandType, sourcePlatform, payload); + } catch (IllegalArgumentException ex) { + VoiceCommandLogger.dropped("parse failed", ex.getMessage()); + return; + } + Log.w(VoiceProtocolConstants.LOG_TAG, + "[VoiceCommandReceiver] onReceive: "); + VoiceCommand cmdWithExtras = VoiceCommand.builder() + .commandType(cmd.getCommandType()) + .requestId(requestId) + .sourceApp(sourceApp) + .sourcePlatform(sourcePlatform) + .callbackAction(callbackAction) + .timestamp(timestamp) + .payload(cmd.getPayload()) + .build(); + VoiceCommandLogger.parsed(cmdWithExtras); + + if (!service.isActivated()) { + service.enqueueForColdStart(cmdWithExtras); + try { + Intent launcher = new Intent(); + launcher.setClassName(context, + "com.pukka.jiecheng.index.IndexActivity"); + launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + context.startActivity(launcher); + } catch (Throwable t) { + Log.w(VoiceProtocolConstants.LOG_TAG, + "[VoiceCommandReceiver] wakeup failed: " + t.getMessage()); + } + } else { + service.handle(cmdWithExtras); + } + } + + private boolean isWhitelisted(String sourceApp) { + return true; +// if (TextUtils.isEmpty(sourceApp)) return false; +// for (String w : DEFAULT_WHITELIST) { +// if (w.equals(sourceApp)) return true; +// } +// return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceInternalReceiver.java b/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceInternalReceiver.java new file mode 100644 index 0000000..56f4156 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/receiver/VoiceInternalReceiver.java @@ -0,0 +1,82 @@ +package com.pukka.jiecheng.voice.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.pukka.jiecheng.voice.log.VoiceCommandLogger; +import com.pukka.jiecheng.voice.parser.SkyworthVodParser; +import com.pukka.jiecheng.voice.protocol.VoiceCommand; +import com.pukka.jiecheng.voice.protocol.VoiceExtras; +import com.pukka.jiecheng.voice.service.VoiceCommandService; + +/** + * 主进程内部接收器,转发 :voice 进程跨进程投递的语音命令。 + * + *

接收 :voice 进程 {@link SkyworthVodParser} 归一化后的原始 OEM 字段, + * 在主进程内完成解析 + 路由: + *

    + *
  • 主进程已激活({@code svc.isActivated() == true})→ 直接 {@code svc.handle(cmd)}
  • + *
  • 主进程未激活 → 仅 cold launch MainActivity(带 {@code __voice_pending} + 4 个 OEM extras), + * 由 MainActivity.goToActivity() 入队到 coldStartQueue(§4.1/§4.2 共用单点入队)
  • + *
+ * + *

设计要点(避免双入队):receiver 只 cold-launch,不再调用 + * {@code svc.enqueueForColdStart(cmd)}——把入队职责完全收敛到 MainActivity.goToActivity。 + * §4.2 hot path 在主进程已激活时直接 handle;§4.2 cold path(主进程活但未激活)通过 + * MainActivity 入队一次。两种 cold path 共用 MainActivity.enqueuePendingVoiceCommand + * 这一个入队点,避免双入队。 + * + *

详见 {@code docs/2026-08-28-语音冷启动链路设计方案.md} §4.2。 + */ +public class VoiceInternalReceiver extends BroadcastReceiver { + + /** 主进程内部广播 action,与 OEM action 完全解耦。 */ + public static final String ACTION_VOICE_FORWARD = + "com.pukka.jiecheng.voice.action.INTERNAL_FORWARD"; + + private static final String LOG_TAG = "VoiceCommand"; + private static final String MAIN_ACTIVITY_CLASS = "com.pukka.jiecheng.MainActivity"; + + @Override + public void onReceive(Context ctx, Intent intent) { + if (ctx == null || intent == null) return; + if (!ACTION_VOICE_FORWARD.equals(intent.getAction())) return; + + VoiceCommandService service = VoiceCommandService.getInstance(ctx); + VoiceCommand cmd; + try { + cmd = SkyworthVodParser.parseFromForward(service, intent); + } catch (Throwable t) { + Log.w(LOG_TAG, "voice internal parse failed: " + t.getMessage()); + return; + } + if (cmd == null) { + VoiceCommandLogger.dropped("voice internal parse returned null", ""); + return; + } + + if (service.isActivated()) { + service.handle(cmd); + return; + } + + // 主进程未激活:cold-launch MainActivity,4 个 OEM extras 全部透传。 + // 入队职责由 MainActivity.goToActivity.enqueuePendingVoiceCommand 单点承担。 + Intent launcher = new Intent(); + launcher.setClassName(ctx, MAIN_ACTIVITY_CLASS); + launcher.setPackage(ctx.getPackageName()); + launcher.putExtra(VoiceExtras.EXTRA_VOICE_PENDING, true); + launcher.putExtra(VoiceExtras.EXTRA_OEM_FILM_NAME, cmd.getPayloadValue("filmName")); + launcher.putExtra(VoiceExtras.EXTRA_OEM_AUTOPLAY, cmd.getPayloadValue("autoPlay")); + launcher.putExtra(VoiceExtras.EXTRA_OEM_REQUEST_ID, cmd.getRequestId()); + launcher.putExtra(VoiceExtras.EXTRA_OEM_SOURCE_APP, cmd.getSourceApp()); + launcher.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + try { + ctx.startActivity(launcher); + } catch (Throwable t) { + Log.w(LOG_TAG, "voice internal cold launch failed: " + t.getMessage()); + } + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/voice/router/BridgeRegistry.java b/app/src/main/java/com/pukka/jiecheng/voice/router/BridgeRegistry.java new file mode 100644 index 0000000..d40798f --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/router/BridgeRegistry.java @@ -0,0 +1,26 @@ +package com.pukka.jiecheng.voice.router; + +import java.util.concurrent.atomic.AtomicReference; + +public final class BridgeRegistry { + + private static final AtomicReference ACTIVE = new AtomicReference<>(); + + private BridgeRegistry() {} + + public static void register(PlaybackControlBridge bridge) { + ACTIVE.set(bridge); + } + + public static void unregister(PlaybackControlBridge bridge) { + ACTIVE.compareAndSet(bridge, null); + } + + public static PlaybackControlBridge get() { + return ACTIVE.get(); + } + + public static void clear() { + ACTIVE.set(null); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/router/CommandRouter.java b/app/src/main/java/com/pukka/jiecheng/voice/router/CommandRouter.java new file mode 100644 index 0000000..288454d --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/router/CommandRouter.java @@ -0,0 +1,143 @@ +package com.pukka.jiecheng.voice.router; + +import com.pukka.jiecheng.voice.guard.AppStateGuard; +import com.pukka.jiecheng.voice.protocol.VoiceCallbackResult; +import com.pukka.jiecheng.voice.protocol.VoiceCommand; +import com.pukka.jiecheng.voice.protocol.VoiceCommandType; + +public class CommandRouter { + + private final PageNavigator navigator; + private final PlaybackControlBridge bridge; + private final AppStateGuard guard; + + public CommandRouter(PageNavigator navigator, PlaybackControlBridge bridge, AppStateGuard guard) { + this.navigator = navigator; + this.bridge = bridge; + this.guard = guard; + } + + public RouteResult route(VoiceCommand cmd) { + if (cmd == null) { + return RouteResult.failure("", VoiceCallbackResult.CODE_INVALID_PAYLOAD, "command is null"); + } + String requestId = cmd.getRequestId() == null ? "" : cmd.getRequestId(); + + if (guard != null && !guard.isActivated()) { + return RouteResult.failure(requestId, + VoiceCallbackResult.CODE_DEVICE_NOT_ACTIVATED, + "device not activated"); + } + + VoiceCommandType type = cmd.getCommandType(); + if (type == null) { + return RouteResult.failure(requestId, + VoiceCallbackResult.CODE_INVALID_PAYLOAD, "commandType is null"); + } + + boolean needExitPlayback = isNavigation(type) && navigator != null && navigator.isPlaying(); + + switch (type) { + case OPEN_FILM_DETAIL: + if (navigator == null) { + return RouteResult.failure(requestId, VoiceCallbackResult.CODE_INVALID_PAYLOAD, + "navigator is null"); + } + if (needExitPlayback && bridge != null) bridge.exitPlayback(); + navigator.openFilmDetail(cmd); + return RouteResult.ok(requestId); + + case OPEN_CATEGORY: + if (navigator == null) { + return RouteResult.failure(requestId, VoiceCallbackResult.CODE_INVALID_PAYLOAD, + "navigator is null"); + } + if (needExitPlayback && bridge != null) bridge.exitPlayback(); + navigator.openCategory(cmd); + return RouteResult.ok(requestId); + + case OPEN_SEARCH: + case SEARCH_FILM: + if (navigator == null) { + return RouteResult.failure(requestId, VoiceCallbackResult.CODE_INVALID_PAYLOAD, + "navigator is null"); + } + if (needExitPlayback && bridge != null) bridge.exitPlayback(); + navigator.openSearch(cmd); + return RouteResult.ok(requestId); + + case GO_HOME: + if (navigator == null) { + return RouteResult.failure(requestId, VoiceCallbackResult.CODE_INVALID_PAYLOAD, + "navigator is null"); + } + if (needExitPlayback && bridge != null) bridge.exitPlayback(); + navigator.goHome(cmd); + return RouteResult.ok(requestId); + + case PLAYBACK_RESUME: + case PLAYBACK_PAUSE: + case PLAYBACK_ENTER_FULLSCREEN: + case PLAYBACK_EXIT_FULLSCREEN: + case PLAYBACK_SEEK_FORWARD: + case PLAYBACK_SEEK_BACKWARD: + case PLAYBACK_SEEK_TO: + case PLAYBACK_PREVIOUS: + case PLAYBACK_NEXT: + case PLAYBACK_SELECT_INDEX: + case PLAYBACK_PAGE_NEXT: + case PLAYBACK_PAGE_PREV: + case PLAYBACK_GOTO_PAGE: + if (bridge == null || !bridge.isAvailable()) { + return RouteResult.failure(requestId, + VoiceCallbackResult.CODE_NOT_IN_PLAYER, + "no active playback bridge"); + } + bridge.dispatch(cmd); + return RouteResult.ok(requestId); + + case OPEN_ACTOR: + case OPEN_DIRECTOR: + default: + return RouteResult.failure(requestId, + VoiceCallbackResult.CODE_INVALID_PAYLOAD, + "unsupported commandType: " + type); + } + } + + private static boolean isNavigation(VoiceCommandType type) { + switch (type) { + case OPEN_FILM_DETAIL: + case OPEN_CATEGORY: + case OPEN_ACTOR: + case OPEN_DIRECTOR: + case OPEN_SEARCH: + case SEARCH_FILM: + case GO_HOME: + return true; + default: + return false; + } + } + + public static final class RouteResult { + private final VoiceCallbackResult result; + + private RouteResult(VoiceCallbackResult result) { + this.result = result; + } + + public static RouteResult ok(String requestId) { + return new RouteResult(VoiceCallbackResult.success(requestId, + VoiceCallbackResult.CODE_OK, "ok")); + } + + public static RouteResult failure(String requestId, String code, String message) { + return new RouteResult(VoiceCallbackResult.failure(requestId, code, message)); + } + + public VoiceCallbackResult getResult() { return result; } + + public boolean isSuccess() { return result != null && result.isSuccess(); } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/router/DefaultPageNavigator.java b/app/src/main/java/com/pukka/jiecheng/voice/router/DefaultPageNavigator.java new file mode 100644 index 0000000..972b88b --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/router/DefaultPageNavigator.java @@ -0,0 +1,165 @@ +package com.pukka.jiecheng.voice.router; + +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; + +import com.migu.bean.SearchResultBean; +import com.migu.callback.AijiaCallback; +import com.migu.httputils.HomeHttpUtils; +import com.pukka.jiecheng.index.IndexActivity; +import com.pukka.jiecheng.list.ListActivity; +import com.pukka.jiecheng.search.SearchActivity; +import com.pukka.jiecheng.utils.ActivityMonitor; +import com.pukka.jiecheng.video.VideoDetailActivity; +import com.pukka.jiecheng.voice.protocol.VoiceCommand; + +import java.util.List; + +public class DefaultPageNavigator implements PageNavigator { + + public static final String EXTRA_CATEGORY_ID = "category_id"; + public static final String EXTRA_CATEGORY_NAME = "category_name"; + public static final String EXTRA_SEARCH_KEYWORD = "search_keyword"; + public static final String EXTRA_AUTO_SEARCH = "auto_search"; + + private final Context appContext; + + public DefaultPageNavigator(Context appContext) { + this.appContext = appContext; + } + + @Override + public void openFilmDetail(VoiceCommand cmd) { + String filmId = cmd.getPayloadValue("filmId"); + String filmName = cmd.getPayloadValue("filmName"); + boolean autoPlay = "true".equalsIgnoreCase(cmd.getPayloadValue("autoPlay")); + + exitPlaybackIfNeeded(); + + // 语音场景下第三方可能只给出影片名甚至把名字塞进 filmId,此时若透传给 + // VideoDetailActivity,详情页会用 /content/getcontentlist.do 按 contentcode 查找, + // 把"镖人"当 code 命中不到。改为先调 HomeHttpUtils.getVideoResult(/search/getsearchlist.do) + // 拿到真实 contentcode 再开详情;搜索无果时打开 SearchActivity 让用户从结果里挑选, + // 不再退化到 /content/getcontentlist.do 走错接口。 + String keyword = !TextUtils.isEmpty(filmName) ? filmName : filmId; + if (TextUtils.isEmpty(keyword)) { + return; + } + searchAndOpenFilmDetail(keyword, autoPlay); + } + + /** + * 按影片名调用 /search/getsearchlist.do 检索,取第一条结果的 contentcode 作为详情页 contentId; + * 命中为空或网络失败时打开 SearchActivity(自动搜索)兜底,全程不再走 /content/getcontentlist.do。 + */ + private void searchAndOpenFilmDetail(final String keyword, final boolean autoPlay) { + HomeHttpUtils.getVideoResult(keyword, "", 1, new AijiaCallback.VideoResultCallback() { + @Override + public void onResponse(SearchResultBean response) { + List list = + response == null ? null : response.getResult(); + if (list != null && !list.isEmpty()) { + SearchResultBean.ResultBean first = list.get(0); + String contentId = first.getContentcode(); + if (!TextUtils.isEmpty(contentId)) { + String resolvedName = TextUtils.isEmpty(first.getName()) + ? keyword : first.getName(); + startVideoDetail(contentId, resolvedName, autoPlay); + return; + } + } + // 搜索无结果或第一条 contentcode 为空:打开 SearchActivity 让用户从列表挑选 + openSearchActivity(keyword); + } + + @Override + public void onFail(String value) { + // 搜索失败:打开 SearchActivity 兜底,不再走错误的详情接口 + openSearchActivity(keyword); + } + }); + } + + private void startVideoDetail(String filmId, String filmName, boolean autoPlay) { + if (autoPlay) { + // 语音自动播片:正常进入详情页,数据加载完成后模拟点击全屏按钮 + // (走 fullVideo() 完整链路,替代原 startFull 的全屏启动模式) + VideoDetailActivity.startAutoFullScreen(appContext, filmId, "", + VideoDetailActivity.ModuleSourceType.SEARCH_PAGE); + } else { + VideoDetailActivity.start(appContext, filmId, ""); + } + } + + private void openSearchActivity(String keyword) { + Intent intent = new Intent(appContext, SearchActivity.class); + intent.putExtra(EXTRA_SEARCH_KEYWORD, keyword == null ? "" : keyword); + intent.putExtra(EXTRA_AUTO_SEARCH, true); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + appContext.startActivity(intent); + } + + @Override + public void openCategory(VoiceCommand cmd) { + String categoryId = cmd.getPayloadValue("categoryId"); + String categoryName = cmd.getPayloadValue("categoryName"); + + exitPlaybackIfNeeded(); + + Intent intent = new Intent(appContext, ListActivity.class); + intent.putExtra(EXTRA_CATEGORY_ID, categoryId == null ? "" : categoryId); + intent.putExtra(EXTRA_CATEGORY_NAME, categoryName == null ? "" : categoryName); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + appContext.startActivity(intent); + } + + @Override + public void openSearch(VoiceCommand cmd) { + String keyword = firstNonEmpty( + cmd.getPayloadValue("filmName"), + cmd.getPayloadValue("actorName"), + cmd.getPayloadValue("directorName"), + cmd.getPayloadValue("category")); + + exitPlaybackIfNeeded(); + + Intent intent = new Intent(appContext, SearchActivity.class); + intent.putExtra(EXTRA_SEARCH_KEYWORD, keyword == null ? "" : keyword); + intent.putExtra(EXTRA_AUTO_SEARCH, true); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + appContext.startActivity(intent); + } + + @Override + public void goHome(VoiceCommand cmd) { + exitPlaybackIfNeeded(); + Intent intent = new Intent(appContext, IndexActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + appContext.startActivity(intent); + } + + @Override + public boolean isPlaying() { + return ActivityMonitor.getInstance() != null + && ActivityMonitor.getInstance().getLastAct() instanceof VideoDetailActivity; + } + + @Override + public void exitPlaybackIfNeeded() { + if (isPlaying()) { + PlaybackControlBridge bridge = BridgeRegistry.get(); + if (bridge != null) { + bridge.exitPlayback(); + } + } + } + + private static String firstNonEmpty(String... values) { + if (values == null) return null; + for (String v : values) { + if (!TextUtils.isEmpty(v)) return v; + } + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/router/PageNavigator.java b/app/src/main/java/com/pukka/jiecheng/voice/router/PageNavigator.java new file mode 100644 index 0000000..5039758 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/router/PageNavigator.java @@ -0,0 +1,18 @@ +package com.pukka.jiecheng.voice.router; + +import com.pukka.jiecheng.voice.protocol.VoiceCommand; + +public interface PageNavigator { + + void openFilmDetail(VoiceCommand cmd); + + void openCategory(VoiceCommand cmd); + + void openSearch(VoiceCommand cmd); + + void goHome(VoiceCommand cmd); + + boolean isPlaying(); + + void exitPlaybackIfNeeded(); +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/router/PlaybackControlBridge.java b/app/src/main/java/com/pukka/jiecheng/voice/router/PlaybackControlBridge.java new file mode 100644 index 0000000..18d3486 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/router/PlaybackControlBridge.java @@ -0,0 +1,12 @@ +package com.pukka.jiecheng.voice.router; + +import com.pukka.jiecheng.voice.protocol.VoiceCommand; + +public interface PlaybackControlBridge { + + boolean isAvailable(); + + void dispatch(VoiceCommand cmd); + + void exitPlayback(); +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandCoordinator.java b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandCoordinator.java new file mode 100644 index 0000000..18c885e --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandCoordinator.java @@ -0,0 +1,90 @@ +package com.pukka.jiecheng.voice.service; + +import android.content.Context; + +import com.pukka.jiecheng.voice.guard.AppStateGuard; +import com.pukka.jiecheng.voice.guard.DefaultAppStateGuard; +import com.pukka.jiecheng.voice.parser.SkyworthVodParser; +import com.pukka.jiecheng.voice.parser.VendorAParser; +import com.pukka.jiecheng.voice.parser.VendorBParser; +import com.pukka.jiecheng.voice.parser.VoicePlatformParser; +import com.pukka.jiecheng.voice.protocol.VoiceCommand; +import com.pukka.jiecheng.voice.queue.CommandQueue; +import com.pukka.jiecheng.voice.router.BridgeRegistry; +import com.pukka.jiecheng.voice.router.CommandRouter; +import com.pukka.jiecheng.voice.router.DefaultPageNavigator; +import com.pukka.jiecheng.voice.router.PageNavigator; +import com.pukka.jiecheng.voice.router.PlaybackControlBridge; +import com.pukka.jiecheng.voice.throttle.CommandThrottle; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class VoiceCommandCoordinator { + + private final Context appContext; + private final VoicePlatformParser parserA; + private final VoicePlatformParser parserB; + private final VoicePlatformParser parserSkyworth; + private final AppStateGuard guard; + private final PageNavigator navigator; + private final CommandRouter router; + private final CommandQueue coldStartQueue; + private final CommandThrottle throttle; + + private final Map parsersByPlatform = new HashMap<>(); + + public VoiceCommandCoordinator(Context appContext) { + this(appContext, new DefaultPageNavigator(appContext), new DefaultAppStateGuard(appContext)); + } + + public VoiceCommandCoordinator(Context appContext, PageNavigator navigator, AppStateGuard guard) { + this.appContext = appContext; + this.navigator = navigator; + this.guard = guard; + this.parserA = new VendorAParser(); + this.parserB = new VendorBParser(); + this.parserSkyworth = new SkyworthVodParser(); + parsersByPlatform.put("vendorA", parserA); + parsersByPlatform.put("vendorB", parserB); + parsersByPlatform.put("skyworth", parserSkyworth); + + PlaybackControlBridge bridge = new RegistryBackedBridge(); + this.router = new CommandRouter(navigator, bridge, guard); + this.coldStartQueue = new CommandQueue(); + this.throttle = new CommandThrottle(); + } + + public VoicePlatformParser resolveParser(String platform) { + if (platform == null) return parserA; + VoicePlatformParser p = parsersByPlatform.get(platform); + return p != null ? p : parserA; + } + + public VoiceCommand parse(String commandType, String sourcePlatform, String payloadJson) { + return resolveParser(sourcePlatform).parse(commandType, payloadJson); + } + + public CommandRouter getRouter() { return router; } + public CommandQueue getColdStartQueue() { return coldStartQueue; } + public CommandThrottle getThrottle() { return throttle; } + public AppStateGuard getGuard() { return guard; } + public PageNavigator getNavigator() { return navigator; } + + public List drainColdStart() { + return coldStartQueue.drainAll(); + } + + private static class RegistryBackedBridge implements PlaybackControlBridge { + @Override public boolean isAvailable() { return BridgeRegistry.get() != null; } + @Override public void dispatch(VoiceCommand cmd) { + PlaybackControlBridge active = BridgeRegistry.get(); + if (active != null) active.dispatch(cmd); + } + @Override public void exitPlayback() { + PlaybackControlBridge active = BridgeRegistry.get(); + if (active != null) active.exitPlayback(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandService.java b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandService.java new file mode 100644 index 0000000..cddf954 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceCommandService.java @@ -0,0 +1,117 @@ +package com.pukka.jiecheng.voice.service; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; +import android.util.Log; + +import com.pukka.jiecheng.voice.log.VoiceCommandLogger; +import com.pukka.jiecheng.voice.protocol.VoiceCallbackResult; +import com.pukka.jiecheng.voice.protocol.VoiceCommand; +import com.pukka.jiecheng.voice.protocol.VoiceProtocolConstants; +import com.pukka.jiecheng.voice.router.CommandRouter; +import com.pukka.jiecheng.voice.throttle.CommandThrottle; + +import java.util.List; + +public class VoiceCommandService { + + private static final String TAG = VoiceProtocolConstants.LOG_TAG; + + private static volatile VoiceCommandService instance; + + private final Context appContext; + private final VoiceCommandCoordinator coordinator; + private final Handler mainHandler; + + private VoiceCommandService(Context appContext) { + this.appContext = appContext.getApplicationContext(); + this.coordinator = new VoiceCommandCoordinator(this.appContext); + this.mainHandler = new Handler(Looper.getMainLooper()); + } + + public static VoiceCommandService getInstance(Context appContext) { + if (instance == null) { + synchronized (VoiceCommandService.class) { + if (instance == null) { + instance = new VoiceCommandService(appContext); + } + } + } + return instance; + } + + public VoiceCommandCoordinator getCoordinator() { return coordinator; } + + public void handle(VoiceCommand cmd) { + if (cmd == null) return; + long start = System.currentTimeMillis(); + + CommandThrottle throttle = coordinator.getThrottle(); + VoiceCommand merged = throttle.accept(cmd); + + CommandRouter router = coordinator.getRouter(); + CommandRouter.RouteResult result = router.route(merged); + + VoiceCommandLogger.executing(merged); + VoiceCommandLogger.finished(merged, result.getResult(), + System.currentTimeMillis() - start, currentPage()); + + sendCallback(merged, result.getResult()); + } + + public void enqueueForColdStart(VoiceCommand cmd) { + if (cmd == null) return; + coordinator.getColdStartQueue().enqueue(cmd); + VoiceCommandLogger.enqueued(cmd.getRequestId()); + } + + public void replayColdStartQueue() { + List drained = coordinator.drainColdStart(); + for (VoiceCommand cmd : drained) { + VoiceCommandLogger.replaying(cmd.getRequestId()); + handle(cmd); + } + } + + public boolean isActivated() { + return coordinator.getGuard().isActivated(); + } + + private void sendCallback(VoiceCommand cmd, VoiceCallbackResult result) { + if (cmd == null || result == null) return; + String callbackAction = cmd.getCallbackAction(); + if (TextUtils.isEmpty(callbackAction)) { + Log.i(TAG, "[callback-skip] no callbackAction for requestId=" + cmd.getRequestId()); + return; + } + Intent intent = new Intent(callbackAction); + intent.putExtra("requestId", cmd.getRequestId()); + intent.putExtra("success", result.isSuccess()); + intent.putExtra("resultCode", result.getResultCode()); + intent.putExtra("resultMessage", result.getResultMessage()); + intent.putExtra("data", result.getData()); + intent.putExtra("payloadJson", result.toJsonString()); + intent.setPackage(VoiceProtocolConstants.APP_PACKAGE); + try { + appContext.sendBroadcast(intent); + } catch (Throwable t) { + Log.w(TAG, "sendCallback failed: " + t.getMessage()); + } + } + + private String currentPage() { + try { + Class clz = Class.forName("com.pukka.jiecheng.utils.ActivityMonitor"); + Object monitor = clz.getField("mAppStatusManager").get(null); + if (monitor == null) return ""; + Object activity = clz.getMethod("getLastAct").invoke(monitor); + if (activity == null) return ""; + return ((android.app.Activity) activity).getClass().getSimpleName(); + } catch (Throwable t) { + return ""; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceKeepAliveService.java b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceKeepAliveService.java new file mode 100644 index 0000000..7980dc3 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/service/VoiceKeepAliveService.java @@ -0,0 +1,103 @@ +package com.pukka.jiecheng.voice.service; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.core.app.NotificationCompat; + +import com.pukka.jiecheng.R; +import com.pukka.jiecheng.voice.adapter.SkyworthVoiceAdapter; +import com.pukka.jiecheng.voice.window.VoiceOverlayHelper; + +/** + * :voice 进程前台保活服务。 + * + *

持有 OEM 创维语音广播接收器({@link SkyworthVoiceAdapter}),进程独立于主进程, + * 主进程被 LMK 或 {@code appExit} 杀掉后仍然存活,确保语音接收链路不中断。 + * + *

进程职责:仅做"翻译 + 转发",所有业务执行都跑在主进程。 + * 详见 {@code docs/2026-08-28-语音冷启动链路设计方案.md}。 + * + *

生命周期: + *

    + *
  • {@code onCreate} → 建静音通知 channel → {@code startForeground}
  • + *
  • {@code onStartCommand} → 注册 OEM 接收器;返回 {@code START_STICKY} 保证系统杀后回拉
  • + *
  • {@code onDestroy} → 反注册接收器
  • + *
+ */ +public class VoiceKeepAliveService extends Service { + + private static final String LOG_TAG = "VoiceCommand"; + private static final int NOTIF_ID = 0x1001; + private static final String CHANNEL_ID = "voice_keepalive"; + + private SkyworthVoiceAdapter adapter; + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(getString(R.string.voice_keepalive_title)) + .setPriority(NotificationCompat.PRIORITY_MIN) + .build(); + Log.i(LOG_TAG, "voice keepalive foreground service started"); + startForeground(NOTIF_ID, notification); + // 让 :voice 进程始终持有 visible window,便于 SkyworthVoiceAdapter 收到广播 + // 后直接 ctx.startActivity 走 Background Activity Start 的 visible-window 豁免。 + // VoiceOverlayHelper 内部幂等:权限缺失 / OEM 拦截 → 沉默失败回退到通知兜底。 + VoiceOverlayHelper.showOverlay(this); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (adapter == null) { + adapter = new SkyworthVoiceAdapter(); + IntentFilter filter = new IntentFilter(SkyworthVoiceAdapter.ACTION_VOD); + registerReceiver(adapter, filter); + } + return START_STICKY; + } + + @Override + public void onDestroy() { + if (adapter != null) { + try { + unregisterReceiver(adapter); + } catch (Throwable t) { + Log.w(LOG_TAG, "voice keepalive receiver unregister failed: " + t.getMessage()); + } + adapter = null; + } + // 服务停止时清理悬浮窗,避免泄漏 1px View 占住系统 WindowManager 槽位 + VoiceOverlayHelper.hideOverlay(); + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + getString(R.string.voice_keepalive_channel), + NotificationManager.IMPORTANCE_MIN); + channel.setShowBadge(false); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + if (nm != null) { + nm.createNotificationChannel(channel); + } + } + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottle.java b/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottle.java new file mode 100644 index 0000000..f78b328 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottle.java @@ -0,0 +1,82 @@ +package com.pukka.jiecheng.voice.throttle; + +import com.pukka.jiecheng.voice.protocol.VoiceCommand; +import com.pukka.jiecheng.voice.protocol.VoiceCommandType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CommandThrottle { + + public static final long DEFAULT_WINDOW_MS = 2000L; + + private final long windowMs; + private final CommandThrottleClock clock; + + private final Map latestByType = new HashMap<>(); + + public CommandThrottle() { + this(DEFAULT_WINDOW_MS, new SystemClock()); + } + + public CommandThrottle(long windowMs) { + this(windowMs, new SystemClock()); + } + + public CommandThrottle(long windowMs, CommandThrottleClock clock) { + this.windowMs = windowMs; + this.clock = clock; + } + + public VoiceCommand accept(VoiceCommand cmd) { + if (cmd == null) return null; + latestByType.put(cmd.getCommandType(), new Entry(cmd, clock.now())); + return cmd; + } + + public VoiceCommand drain() { + if (latestByType.isEmpty()) return null; + Entry latest = null; + VoiceCommandType latestType = null; + for (Map.Entry e : latestByType.entrySet()) { + if (latest == null || e.getValue().acceptedAt > latest.acceptedAt) { + latest = e.getValue(); + latestType = e.getKey(); + } + } + if (latestType != null) latestByType.remove(latestType); + return latest == null ? null : latest.cmd; + } + + public List drainAll() { + if (latestByType.isEmpty()) return new ArrayList<>(); + List entries = new ArrayList<>(latestByType.values()); + entries.sort((a, b) -> Long.compare(b.acceptedAt, a.acceptedAt)); + latestByType.clear(); + List result = new ArrayList<>(entries.size()); + for (Entry e : entries) result.add(e.cmd); + return result; + } + + public static boolean isSameThrottleKey(VoiceCommandType a, VoiceCommandType b) { + return a == b; + } + + private static final class Entry { + final VoiceCommand cmd; + final long acceptedAt; + + Entry(VoiceCommand cmd, long acceptedAt) { + this.cmd = cmd; + this.acceptedAt = acceptedAt; + } + } + + private static final class SystemClock implements CommandThrottleClock { + @Override public long now() { + return System.currentTimeMillis(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottleClock.java b/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottleClock.java new file mode 100644 index 0000000..c60f17d --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/throttle/CommandThrottleClock.java @@ -0,0 +1,5 @@ +package com.pukka.jiecheng.voice.throttle; + +public interface CommandThrottleClock { + long now(); +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/voice/window/VoiceOverlayHelper.java b/app/src/main/java/com/pukka/jiecheng/voice/window/VoiceOverlayHelper.java new file mode 100644 index 0000000..2891f11 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/voice/window/VoiceOverlayHelper.java @@ -0,0 +1,141 @@ +package com.pukka.jiecheng.voice.window; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.os.Build; +import android.provider.Settings; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.WindowManager; + +/** + * 语音冷启动用 1px 悬浮窗助手。 + * + *

目的:在 :voice 进程挂一个 {@code TYPE_APPLICATION_OVERLAY} 1×1 透明 View, + * 让 AMS 判定该进程持有 visible window。Android 12+ 的 Background Activity Start + * 限制对"calling app has a visible window"显式豁免({@code ActivityTaskManager} + * 的 {@code isCallingUidHasAnyVisibleWindow} 判定),后续 + * {@link android.content.Context#startActivity(android.content.Intent)} 不再被 + * Abort。冷启动 Activity 完成后调用 {@link #hideOverlay()} 移除即可。 + * + *

设计原则: + *

    + *
  • 幂等:多次 {@link #showOverlay(Context)} 只 addView 一次
  • + *
  • 失败短路:首次 addView 失败(如 SYSTEM_ALERT_WINDOW 未授予/OEM 拦截) + * 缓存状态,后续不再重试,避免重复 Toast / 日志刷屏
  • + *
  • 不抢焦点:FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE | FLAG_LAYOUT_NO_LIMITS
  • + *
  • 零副作用:1×1 像素 + 透明 + 屏幕外(gravity=-1,-1),肉眼无感
  • + *
+ * + *

权限前置条件:{@code Settings.canDrawOverlays(ctx)}。该权限是标准权限, + * 不需要运行时申请;部分 OEM ROM 自动授予,未授予时该 helper 走沉默失败路径, + * 上层走 FullScreenIntent 通知兜底。 + */ +public final class VoiceOverlayHelper { + + private static final String LOG_TAG = "VoiceCommand"; + + private static volatile boolean overlayShown = false; + private static volatile boolean overlayDisabled = false; + + private static WindowManager wm; + private static View overlayView; + private static WindowManager.LayoutParams overlayLp; + + private VoiceOverlayHelper() {} + + /** + * 挂 1px 悬浮窗。幂等;已挂则直接返回 true。 + * + * @return true 表示当前 visible window 由本 helper 提供,可用于上层判定走 + * startActivity 优先路径;false 表示 SYSTEM_ALERT_WINDOW 未授权 / + * OEM 拦截,应回退到通知 FullScreenIntent 路径。 + */ + public static synchronized boolean showOverlay(Context ctx) { + if (overlayShown) return true; + if (overlayDisabled) return false; + Context appCtx = ctx.getApplicationContext(); + if (appCtx == null) { + overlayDisabled = true; + return false; + } + if (!Settings.canDrawOverlays(appCtx)) { + Log.w(LOG_TAG, "voice overlay: SYSTEM_ALERT_WINDOW not granted, fallback to notify"); + overlayDisabled = true; + return false; + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // TYPE_APPLICATION_OVERLAY 是 Android 6+ 才有的类型;低版本设备此 helper 无效 + overlayDisabled = true; + return false; + } + try { + wm = (WindowManager) appCtx.getSystemService(Context.WINDOW_SERVICE); + if (wm == null) { + overlayDisabled = true; + return false; + } + overlayLp = new WindowManager.LayoutParams( + 1, 1, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, + PixelFormat.TRANSLUCENT); + overlayLp.gravity = Gravity.TOP | Gravity.START; + overlayLp.x = -1; + overlayLp.y = -1; + + overlayView = new View(appCtx); + overlayView.setBackgroundColor(Color.TRANSPARENT); + + wm.addView(overlayView, overlayLp); + overlayShown = true; + Log.i(LOG_TAG, "voice overlay shown (1px TYPE_APPLICATION_OVERLAY)"); + return true; + } catch (Throwable t) { + Log.w(LOG_TAG, "voice overlay addView failed: " + t.getMessage()); + overlayShown = false; + overlayView = null; + overlayLp = null; + wm = null; + // SecurityException / WindowManager.BadTokenException 等都不要重试 + overlayDisabled = true; + return false; + } + } + + /** + * 移除悬浮窗。幂等;未挂 / 已失败过都安全返回。 + */ + public static synchronized void hideOverlay() { + if (!overlayShown || wm == null || overlayView == null) return; + try { + wm.removeView(overlayView); + } catch (Throwable t) { + // IllegalArgumentException 等:可能已被系统移除,吞掉即可 + Log.w(LOG_TAG, "voice overlay removeView failed: " + t.getMessage()); + } + overlayShown = false; + overlayView = null; + overlayLp = null; + wm = null; + } + + /** + * 供调试 / 上层判定。 + */ + public static boolean isShown() { + return overlayShown; + } + + /** + * 供上层判定 helper 是否因权限 / OEM 拦截被永久禁用。 + */ + public static boolean isDisabled() { + return overlayDisabled; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pukka/jiecheng/web/SimpleWebActivity.java b/app/src/main/java/com/pukka/jiecheng/web/SimpleWebActivity.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/ActivationCodeInputView.kt b/app/src/main/java/com/pukka/jiecheng/widget/ActivationCodeInputView.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/ActivationFocusRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/widget/ActivationFocusRecyclerView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/ActivationKeyItem.kt b/app/src/main/java/com/pukka/jiecheng/widget/ActivationKeyItem.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/ActivationKeypadAdapter.kt b/app/src/main/java/com/pukka/jiecheng/widget/ActivationKeypadAdapter.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/CornerBadgeView.java b/app/src/main/java/com/pukka/jiecheng/widget/CornerBadgeView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/CustomerCodeView.java b/app/src/main/java/com/pukka/jiecheng/widget/CustomerCodeView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/D3ContentLinearLayoutManager.java b/app/src/main/java/com/pukka/jiecheng/widget/D3ContentLinearLayoutManager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/D3NavLinerLayoutManagerTv.java b/app/src/main/java/com/pukka/jiecheng/widget/D3NavLinerLayoutManagerTv.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/D3RecyclerView.java b/app/src/main/java/com/pukka/jiecheng/widget/D3RecyclerView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/DeviceNotActivatedView.kt b/app/src/main/java/com/pukka/jiecheng/widget/DeviceNotActivatedView.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/DeviceUnregisteredView.java b/app/src/main/java/com/pukka/jiecheng/widget/DeviceUnregisteredView.java deleted file mode 100644 index bd8a087..0000000 --- a/app/src/main/java/com/pukka/jiecheng/widget/DeviceUnregisteredView.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.pukka.jiecheng.widget; - -import android.content.Context; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import com.pukka.jiecheng.R; - -public class DeviceUnregisteredView extends FrameLayout { - - private static final String TAG = "DeviceUnregisteredView"; - - private static final int ERROR_CODE_ACTIVATE_FAILED = 10500; - private static final int ERROR_CODE_DEVICE_UNBOUND = 10600; - private static final int ERROR_CODE_MEMBER_EXPIRED = 10700; - private static final int ERROR_CODE_DEVICE_OCCUPIED = 10800; - - private static final String DEFAULT_MESSAGE = "设备未注册,请联系第三方开通服务"; - - private TextView mMessageView; - private Button mExitButton; - - @Nullable - private OnExitListener mOnExitListener; - - public interface OnExitListener { - void onExit(); - } - - public DeviceUnregisteredView(Context context) { - this(context, null); - } - - public DeviceUnregisteredView(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public DeviceUnregisteredView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - initView(context); - } - - private void initView(Context context) { - LayoutInflater.from(context).inflate(R.layout.view_device_unregistered, this, true); - mMessageView = findViewById(R.id.id_tv_unregistered_message); - mExitButton = findViewById(R.id.id_btn_exit); - - mExitButton.setFocusable(true); - mExitButton.setFocusableInTouchMode(true); - mExitButton.setClickable(true); - mExitButton.setOnClickListener(v -> { - if (mOnExitListener != null) { - mOnExitListener.onExit(); - } - }); - - setVisibility(GONE); - } - - /** - * 根据 checkBenefits 返回的状态码设置阻断页提示文案。 - * - * @param errorCode 状态码 - * @param message 自定义提示文案,仅在状态码未命中预设映射时使用 - */ - public void setStatusInfo(int errorCode, @Nullable String message) { - String displayMessage; - switch (errorCode) { - case ERROR_CODE_ACTIVATE_FAILED: - displayMessage = "激活失败,请联系第三方重新开通"; - break; - case ERROR_CODE_DEVICE_UNBOUND: - displayMessage = "设备未绑定,请联系第三方开通服务"; - break; - case ERROR_CODE_MEMBER_EXPIRED: - displayMessage = "会员已过期,请联系第三方续费"; - break; - case ERROR_CODE_DEVICE_OCCUPIED: - displayMessage = "设备已被占用,请联系客服"; - break; - default: - displayMessage = !TextUtils.isEmpty(message) ? message : DEFAULT_MESSAGE; - break; - } - mMessageView.setText(displayMessage); - } - - public void setOnExitListener(@Nullable OnExitListener listener) { - mOnExitListener = listener; - } - - @Override - public void setVisibility(int visibility) { - super.setVisibility(visibility); - if (visibility == VISIBLE) { - mExitButton.requestFocus(); - } - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { - if (event.getAction() == KeyEvent.ACTION_UP && mOnExitListener != null) { - mOnExitListener.onExit(); - } - return true; - } - return super.dispatchKeyEvent(event); - } -} diff --git a/app/src/main/java/com/pukka/jiecheng/widget/FocusLinearLayout.java b/app/src/main/java/com/pukka/jiecheng/widget/FocusLinearLayout.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/FocusRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/widget/FocusRecyclerView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/FocusTextView.java b/app/src/main/java/com/pukka/jiecheng/widget/FocusTextView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/HomeNavLinearLayoutManagerTv.java b/app/src/main/java/com/pukka/jiecheng/widget/HomeNavLinearLayoutManagerTv.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/HorizontalGridViewPk.java b/app/src/main/java/com/pukka/jiecheng/widget/HorizontalGridViewPk.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/LinearSpacingItemDecoration.java b/app/src/main/java/com/pukka/jiecheng/widget/LinearSpacingItemDecoration.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/LocationCodeInputView.kt b/app/src/main/java/com/pukka/jiecheng/widget/LocationCodeInputView.kt old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/MarqueeTextView.java b/app/src/main/java/com/pukka/jiecheng/widget/MarqueeTextView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/MgRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/widget/MgRecyclerView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/QrCodeView.java b/app/src/main/java/com/pukka/jiecheng/widget/QrCodeView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/SettingItemView.kt b/app/src/main/java/com/pukka/jiecheng/widget/SettingItemView.kt new file mode 100644 index 0000000..4ca2a78 --- /dev/null +++ b/app/src/main/java/com/pukka/jiecheng/widget/SettingItemView.kt @@ -0,0 +1,78 @@ +package com.pukka.jiecheng.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.FrameLayout +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.appcompat.widget.SwitchCompat +import com.pukka.jiecheng.R +import com.pukka.jiecheng.setting.SettingItem + +/** + * 设置页单个 item 的容器视图。 + * + * 封装"标题 + 副标题 + SwitchCompat"的标准结构,外部通过 [bind] 绑定数据。 + * 外部通过 [setOnCheckedChangeListener] 监听开关变化。 + * + * 为什么不用 item layout 里的 id 直接 findViewById: + * RecyclerView 通过 ViewHolder 复用 itemView,多个 Holder 共享同一个 LayoutInflater + * 出来的 View 实例,外层 Activity / Adapter 直接持有这些 view 的引用会在复用时被覆盖; + * 封装成自定义 View 后,对外只暴露 [bind] / [setOnCheckedChangeListener], + * 每个 VH 拿到的是独立的 SettingItemView 实例,状态归属明确。 + */ +class SettingItemView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + private val tvTitle: TextView + private val tvSubtitle: TextView + private val switch: SwitchCompat + + init { + LayoutInflater.from(context).inflate(R.layout.item_setting_switch_inner, this, true) + tvTitle = findViewById(R.id.id_tv_setting_title) + tvSubtitle = findViewById(R.id.id_tv_setting_subtitle) + switch = findViewById(R.id.id_switch_setting) + } + + /** + * 暴露内部 SwitchCompat,便于外部在 RecyclerView 首次 bind 完成后主动请求焦点 + * (遥控器支持:进入页面焦点直接落到开关,OK 键立即可切换)。 + */ + fun getSwitch(): SwitchCompat = switch + + fun bind(item: SettingItem) { + tvTitle.text = item.title + if (item.subtitle.isNullOrEmpty()) { + tvSubtitle.visibility = GONE + } else { + tvSubtitle.visibility = VISIBLE + tvSubtitle.text = item.subtitle + } + // 关闭 listener 再 setChecked,避免 bind 阶段触发 onCheckedChange + switch.setOnCheckedChangeListener(null) + switch.isChecked = item.checked + switch.isEnabled = item.enabled + // 不在 bind() 里 requestFocus:RecyclerView 复用同一个 itemView 反复 bind, + // 在每个 item 上都 request 会让焦点频繁跳到最新可见的第一项,反而干扰遥控器上下键。 + // 焦点交给外层 Activity 在 onCreate / onResume 时主动下发一次即可。 + } + + /** + * 把 SwitchCompat 的开关变化透传给外层 Adapter。 + * 使用 lambda 形式避免直接引用 CompoundButton.OnCheckedChangeListener + * 这个跨包 Java 接口在 Kotlin 中 SAM 转换偶尔出问题。 + */ + fun setOnCheckedChangeListener(listener: (Boolean) -> Unit) { + switch.setOnCheckedChangeListener { _, isChecked -> listener.invoke(isChecked) } + } + + /** 兼容从 XML 直接配置标题的便捷方法 */ + fun setTitleText(@StringRes resId: Int) { + tvTitle.setText(resId) + } +} diff --git a/app/src/main/java/com/pukka/jiecheng/widget/SpaceItemDecoration.java b/app/src/main/java/com/pukka/jiecheng/widget/SpaceItemDecoration.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/SquareTextView.java b/app/src/main/java/com/pukka/jiecheng/widget/SquareTextView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/TopView.java b/app/src/main/java/com/pukka/jiecheng/widget/TopView.java old mode 100755 new mode 100644 index 7f388ac..0438ddf --- a/app/src/main/java/com/pukka/jiecheng/widget/TopView.java +++ b/app/src/main/java/com/pukka/jiecheng/widget/TopView.java @@ -28,8 +28,8 @@ import com.facebook.imagepipeline.image.ImageInfo; import com.facebook.imagepipeline.request.ImageRequest; import com.facebook.imagepipeline.request.ImageRequestBuilder; import com.migu.bean.GetTopButtonBean; +import com.pukka.jiecheng.BuildConfig; import com.pukka.jiecheng.R; -import com.pukka.jiecheng.config.AppConfiguration; import com.pukka.jiecheng.utils.MgRoute; import com.pukka.jiecheng.utils.TopStyleUtils; import com.pukka.libcore.boxShared.BoxSharedParamsUtils; @@ -160,10 +160,7 @@ public class TopView extends ConstraintLayout implements View.OnClickListener, V loadImgPre(focusHistoryImg,ContextUtils.getContext()); break; case 5: - if (AppConfiguration.getInstance().isThirdPartyVariant()) { - break; - } - if(BoxSharedParamsUtils.isJcHotel()){ + if(!BuildConfig.IS_HOME_EDITION){ break; } mIvOrder.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/pukka/jiecheng/widget/TvXRecyclerView.java b/app/src/main/java/com/pukka/jiecheng/widget/TvXRecyclerView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/UserAgreementView.java b/app/src/main/java/com/pukka/jiecheng/widget/UserAgreementView.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/VerticalRecyclerViewEx.java b/app/src/main/java/com/pukka/jiecheng/widget/VerticalRecyclerViewEx.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/VideoContainer.java b/app/src/main/java/com/pukka/jiecheng/widget/VideoContainer.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/Banner.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/Banner.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerConfig.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerConfig.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerScroller.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerScroller.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerViewPager.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/BannerViewPager.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/OnBannerListener.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/OnBannerListener.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/WeakHandler.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/WeakHandler.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/BaseImageLoader.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/BaseImageLoader.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/FrescoImageLoader.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/FrescoImageLoader.java old mode 100755 new mode 100644 diff --git a/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/ImageLoaderInterface.java b/app/src/main/java/com/pukka/jiecheng/widget/banner/loader/ImageLoaderInterface.java old mode 100755 new mode 100644 diff --git a/app/src/main/res/anim/activation_code_shake.xml b/app/src/main/res/anim/activation_code_shake.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/anim/rotate_anim.xml b/app/src/main/res/anim/rotate_anim.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/color/activation_action_text_selector.xml b/app/src/main/res/color/activation_action_text_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/color/activation_key_text_selector.xml b/app/src/main/res/color/activation_key_text_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/color/activation_view_text_selector.xml b/app/src/main/res/color/activation_view_text_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_about_submit.png b/app/src/main/res/drawable-hdpi/bg_about_submit.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_about_submit_focus.png b/app/src/main/res/drawable-hdpi/bg_about_submit_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_about_yhxy.png b/app/src/main/res/drawable-hdpi/bg_about_yhxy.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_about_yhxy_focus.png b/app/src/main/res/drawable-hdpi/bg_about_yhxy_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_about_ysxy.png b/app/src/main/res/drawable-hdpi/bg_about_ysxy.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_about_ysxy_focus.png b/app/src/main/res/drawable-hdpi/bg_about_ysxy_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_back_search.png b/app/src/main/res/drawable-hdpi/bg_back_search.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_back_search_focus.png b/app/src/main/res/drawable-hdpi/bg_back_search_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_back_top.png b/app/src/main/res/drawable-hdpi/bg_back_top.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_back_top_focus.png b/app/src/main/res/drawable-hdpi/bg_back_top_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_check_vr_btn_focus.png b/app/src/main/res/drawable-hdpi/bg_check_vr_btn_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_check_vr_btn_nor.png b/app/src/main/res/drawable-hdpi/bg_check_vr_btn_nor.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_hotel_about.png b/app/src/main/res/drawable-hdpi/bg_hotel_about.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_hotel_deviceinfo.png b/app/src/main/res/drawable-hdpi/bg_hotel_deviceinfo.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_hotel_setting.png b/app/src/main/res/drawable-hdpi/bg_hotel_setting.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_hotel_update.png b/app/src/main/res/drawable-hdpi/bg_hotel_update.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_hotel_user.png b/app/src/main/res/drawable-hdpi/bg_hotel_user.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_short_mark.png b/app/src/main/res/drawable-hdpi/bg_short_mark.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_short_playing_check.png b/app/src/main/res/drawable-hdpi/bg_short_playing_check.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_short_vod.png b/app/src/main/res/drawable-hdpi/bg_short_vod.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_short_vod_focus.png b/app/src/main/res/drawable-hdpi/bg_short_vod_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_user_equit.png b/app/src/main/res/drawable-hdpi/bg_user_equit.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_useragreement.png b/app/src/main/res/drawable-hdpi/bg_useragreement.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_useragreement_submit.png b/app/src/main/res/drawable-hdpi/bg_useragreement_submit.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_useragreement_submit_focus.png b/app/src/main/res/drawable-hdpi/bg_useragreement_submit_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_useragreenmen_quit_focus.png b/app/src/main/res/drawable-hdpi/bg_useragreenmen_quit_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/bg_useragreenment_quit_unfocus.png b/app/src/main/res/drawable-hdpi/bg_useragreenment_quit_unfocus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/btn_1_foc.png b/app/src/main/res/drawable-hdpi/btn_1_foc.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/btn_1_nor.png b/app/src/main/res/drawable-hdpi/btn_1_nor.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/btn_device_unactivit_focus.png b/app/src/main/res/drawable-hdpi/btn_device_unactivit_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/btn_focus.png b/app/src/main/res/drawable-hdpi/btn_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/btn_nor.png b/app/src/main/res/drawable-hdpi/btn_nor.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/btn_progress.png b/app/src/main/res/drawable-hdpi/btn_progress.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/btn_short_playing_submit.png b/app/src/main/res/drawable-hdpi/btn_short_playing_submit.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/device_bg.png b/app/src/main/res/drawable-hdpi/device_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/device_check_update.png b/app/src/main/res/drawable-hdpi/device_check_update.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/device_check_update_focus.png b/app/src/main/res/drawable-hdpi/device_check_update_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/device_info_bg.png b/app/src/main/res/drawable-hdpi/device_info_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/gamepad.png b/app/src/main/res/drawable-hdpi/gamepad.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/help_bg.png b/app/src/main/res/drawable-hdpi/help_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/home_btn_down_foc.png b/app/src/main/res/drawable-hdpi/home_btn_down_foc.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/home_btn_down_nor.png b/app/src/main/res/drawable-hdpi/home_btn_down_nor.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/ic_check_vr_no.png b/app/src/main/res/drawable-hdpi/ic_check_vr_no.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/ic_check_vr_yes.png b/app/src/main/res/drawable-hdpi/ic_check_vr_yes.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/ic_full_vr_tip.png b/app/src/main/res/drawable-hdpi/ic_full_vr_tip.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/ic_short_playing.gif b/app/src/main/res/drawable-hdpi/ic_short_playing.gif old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/ic_weixin.png b/app/src/main/res/drawable-hdpi/ic_weixin.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/ic_zhifubao.png b/app/src/main/res/drawable-hdpi/ic_zhifubao.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/icon_detail_back_normal.png b/app/src/main/res/drawable-hdpi/icon_detail_back_normal.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/icon_error_prompt.png b/app/src/main/res/drawable-hdpi/icon_error_prompt.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/index_bg.png b/app/src/main/res/drawable-hdpi/index_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/index_my_user_bg_focus.png b/app/src/main/res/drawable-hdpi/index_my_user_bg_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/jst_logo_right.webp b/app/src/main/res/drawable-hdpi/jst_logo_right.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/keyboard_clear.png b/app/src/main/res/drawable-hdpi/keyboard_clear.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/keyboard_delete.png b/app/src/main/res/drawable-hdpi/keyboard_delete.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/list_bg.png b/app/src/main/res/drawable-hdpi/list_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/list_bg_tag_focus.png b/app/src/main/res/drawable-hdpi/list_bg_tag_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/list_bg_tag_unfocus.png b/app/src/main/res/drawable-hdpi/list_bg_tag_unfocus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/list_search_ic.png b/app/src/main/res/drawable-hdpi/list_search_ic.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/my_order_bg.png b/app/src/main/res/drawable-hdpi/my_order_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/my_order_jilu_bg.webp b/app/src/main/res/drawable-hdpi/my_order_jilu_bg.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/my_order_page_select.png b/app/src/main/res/drawable-hdpi/my_order_page_select.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/my_order_page_unselect.png b/app/src/main/res/drawable-hdpi/my_order_page_unselect.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/order_bg1.webp b/app/src/main/res/drawable-hdpi/order_bg1.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/order_bg_qr_code.png b/app/src/main/res/drawable-hdpi/order_bg_qr_code.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/order_btn_focus_bg.png b/app/src/main/res/drawable-hdpi/order_btn_focus_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/order_btn_focused_bg.png b/app/src/main/res/drawable-hdpi/order_btn_focused_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/order_btn_unfocus_bg.png b/app/src/main/res/drawable-hdpi/order_btn_unfocus_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/order_center_keyboard_btn_focus.png b/app/src/main/res/drawable-hdpi/order_center_keyboard_btn_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/order_center_keyboard_btn_unfocus.png b/app/src/main/res/drawable-hdpi/order_center_keyboard_btn_unfocus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/order_center_vip_text_bg.png b/app/src/main/res/drawable-hdpi/order_center_vip_text_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/order_pre_tip.png b/app/src/main/res/drawable-hdpi/order_pre_tip.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/ordercenter_item_focus_bg.png b/app/src/main/res/drawable-hdpi/ordercenter_item_focus_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/ordercenter_item_unfoces_bg.png b/app/src/main/res/drawable-hdpi/ordercenter_item_unfoces_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/progress.png b/app/src/main/res/drawable-hdpi/progress.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/progress_loading.png b/app/src/main/res/drawable-hdpi/progress_loading.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/short_ok_game_tip.png b/app/src/main/res/drawable-hdpi/short_ok_game_tip.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/short_ok_tip.png b/app/src/main/res/drawable-hdpi/short_ok_tip.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/short_up_game_tip.png b/app/src/main/res/drawable-hdpi/short_up_game_tip.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/short_up_tip.png b/app/src/main/res/drawable-hdpi/short_up_tip.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/try_see_pay_tip.webp b/app/src/main/res/drawable-hdpi/try_see_pay_tip.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/update_dialog_bg.png b/app/src/main/res/drawable-hdpi/update_dialog_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/weilai_btn_error_focus.png b/app/src/main/res/drawable-hdpi/weilai_btn_error_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/weilai_btn_error_nofocus.png b/app/src/main/res/drawable-hdpi/weilai_btn_error_nofocus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-hdpi/weilai_error_bg.webp b/app/src/main/res/drawable-hdpi/weilai_error_bg.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xhdpi/progress_loading.png b/app/src/main/res/drawable-xhdpi/progress_loading.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading00.png b/app/src/main/res/drawable-xxhdpi/loading00.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading000.png b/app/src/main/res/drawable-xxhdpi/loading000.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading01.png b/app/src/main/res/drawable-xxhdpi/loading01.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading02.png b/app/src/main/res/drawable-xxhdpi/loading02.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading03.png b/app/src/main/res/drawable-xxhdpi/loading03.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading04.png b/app/src/main/res/drawable-xxhdpi/loading04.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading05.png b/app/src/main/res/drawable-xxhdpi/loading05.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading06.png b/app/src/main/res/drawable-xxhdpi/loading06.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading07.png b/app/src/main/res/drawable-xxhdpi/loading07.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading08.png b/app/src/main/res/drawable-xxhdpi/loading08.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading09.png b/app/src/main/res/drawable-xxhdpi/loading09.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading10.png b/app/src/main/res/drawable-xxhdpi/loading10.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading11.png b/app/src/main/res/drawable-xxhdpi/loading11.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading12.png b/app/src/main/res/drawable-xxhdpi/loading12.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading13.png b/app/src/main/res/drawable-xxhdpi/loading13.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading14.png b/app/src/main/res/drawable-xxhdpi/loading14.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading15.png b/app/src/main/res/drawable-xxhdpi/loading15.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading16.png b/app/src/main/res/drawable-xxhdpi/loading16.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading17.png b/app/src/main/res/drawable-xxhdpi/loading17.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading18.png b/app/src/main/res/drawable-xxhdpi/loading18.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading19.png b/app/src/main/res/drawable-xxhdpi/loading19.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading20.png b/app/src/main/res/drawable-xxhdpi/loading20.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading21.png b/app/src/main/res/drawable-xxhdpi/loading21.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/loading22.png b/app/src/main/res/drawable-xxhdpi/loading22.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/progress_loading.png b/app/src/main/res/drawable-xxhdpi/progress_loading.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/switch_keyboard_bg.png b/app/src/main/res/drawable-xxhdpi/switch_keyboard_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxxhdpi/progress_loading.png b/app/src/main/res/drawable-xxxhdpi/progress_loading.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxxhdpi/switch_keyboard_bg.png b/app/src/main/res/drawable-xxxhdpi/switch_keyboard_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/background_search.xml b/app/src/main/res/drawable/background_search.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/background_splash.webp b/app/src/main/res/drawable/background_splash.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_2.webp b/app/src/main/res/drawable/bg_2.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_about_submit_selector.xml b/app/src/main/res/drawable/bg_about_submit_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_about_yhxy_selector.xml b/app/src/main/res/drawable/bg_about_yhxy_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_about_ysxy_selector.xml b/app/src/main/res/drawable/bg_about_ysxy_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_activation_action_selector.xml b/app/src/main/res/drawable/bg_activation_action_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_activation_code_box.xml b/app/src/main/res/drawable/bg_activation_code_box.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_activation_device_panel.xml b/app/src/main/res/drawable/bg_activation_device_panel.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_activation_gradient.xml b/app/src/main/res/drawable/bg_activation_gradient.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_activation_input_preview.xml b/app/src/main/res/drawable/bg_activation_input_preview.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_activation_key_selector.xml b/app/src/main/res/drawable/bg_activation_key_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_activation_loading_panel.xml b/app/src/main/res/drawable/bg_activation_loading_panel.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_avatar_white_border.xml b/app/src/main/res/drawable/bg_avatar_white_border.xml new file mode 100644 index 0000000..e7e3427 --- /dev/null +++ b/app/src/main/res/drawable/bg_avatar_white_border.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_bottom_round_corner.xml b/app/src/main/res/drawable/bg_bottom_round_corner.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_bottom_white_round_corner.xml b/app/src/main/res/drawable/bg_bottom_white_round_corner.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_btn_confirm.xml b/app/src/main/res/drawable/bg_btn_confirm.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_btn_err_prompt.xml b/app/src/main/res/drawable/bg_btn_err_prompt.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_btn_retention_prompt.xml b/app/src/main/res/drawable/bg_btn_retention_prompt.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_btn_trysee_open_focus.xml b/app/src/main/res/drawable/bg_btn_trysee_open_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_btn_useragreenment_focus.xml b/app/src/main/res/drawable/bg_btn_useragreenment_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_btn_useragreenment_selector.xml b/app/src/main/res/drawable/bg_btn_useragreenment_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_btn_useragreenment_unfocus.xml b/app/src/main/res/drawable/bg_btn_useragreenment_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_check_8k_selector.xml b/app/src/main/res/drawable/bg_check_8k_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_check_vr_selector.xml b/app/src/main/res/drawable/bg_check_vr_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_content_c7_focus.xml b/app/src/main/res/drawable/bg_content_c7_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_content_c7_selector.xml b/app/src/main/res/drawable/bg_content_c7_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_content_focus.xml b/app/src/main/res/drawable/bg_content_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_content_selector.xml b/app/src/main/res/drawable/bg_content_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_content_t_focus.xml b/app/src/main/res/drawable/bg_content_t_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_content_t_selector.xml b/app/src/main/res/drawable/bg_content_t_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_content_unfocus.xml b/app/src/main/res/drawable/bg_content_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_deman_table_page_selector.xml b/app/src/main/res/drawable/bg_deman_table_page_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_demand_table_page_selector.xml b/app/src/main/res/drawable/bg_demand_table_page_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_demandbg_table_page_selector.xml b/app/src/main/res/drawable/bg_demandbg_table_page_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_back_selector.xml b/app/src/main/res/drawable/bg_detail_back_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_broadcast_label.xml b/app/src/main/res/drawable/bg_detail_broadcast_label.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_btn_order_selector.xml b/app/src/main/res/drawable/bg_detail_btn_order_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_btn_selector.xml b/app/src/main/res/drawable/bg_detail_btn_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_btn_video_selector.xml b/app/src/main/res/drawable/bg_detail_btn_video_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_btn_vip_selector.xml b/app/src/main/res/drawable/bg_detail_btn_vip_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_def_selector.xml b/app/src/main/res/drawable/bg_detail_def_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_episodes_mask.xml b/app/src/main/res/drawable/bg_detail_episodes_mask.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_episodes_pp_line.xml b/app/src/main/res/drawable/bg_detail_episodes_pp_line.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_more_selector.xml b/app/src/main/res/drawable/bg_detail_more_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_program_table_item_selector.xml b/app/src/main/res/drawable/bg_detail_program_table_item_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_table_page_selector.xml b/app/src/main/res/drawable/bg_detail_table_page_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_try_see_order.xml b/app/src/main/res/drawable/bg_detail_try_see_order.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_try_see_order2.xml b/app/src/main/res/drawable/bg_detail_try_see_order2.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_try_see_order_bottom.xml b/app/src/main/res/drawable/bg_detail_try_see_order_bottom.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_video_selector.xml b/app/src/main/res/drawable/bg_detail_video_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_video_selector2.xml b/app/src/main/res/drawable/bg_detail_video_selector2.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_detail_vip_label.xml b/app/src/main/res/drawable/bg_detail_vip_label.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_device_activate_btn.xml b/app/src/main/res/drawable/bg_device_activate_btn.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_device_activate_input.xml b/app/src/main/res/drawable/bg_device_activate_input.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_device_activate_panel.xml b/app/src/main/res/drawable/bg_device_activate_panel.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_device_check_selector.xml b/app/src/main/res/drawable/bg_device_check_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_dialog_activation.xml b/app/src/main/res/drawable/bg_dialog_activation.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_filter_table_page_selector.xml b/app/src/main/res/drawable/bg_filter_table_page_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_full_vr_player_tip.xml b/app/src/main/res/drawable/bg_full_vr_player_tip.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_full_window_bottom_mask.xml b/app/src/main/res/drawable/bg_full_window_bottom_mask.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_full_window_top_mask.xml b/app/src/main/res/drawable/bg_full_window_top_mask.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_fulldialogclose.png b/app/src/main/res/drawable/bg_fulldialogclose.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_gd.webp b/app/src/main/res/drawable/bg_gd.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_gx_no_circle.png b/app/src/main/res/drawable/bg_gx_no_circle.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_his_selector.xml b/app/src/main/res/drawable/bg_his_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_his_unfocus.xml b/app/src/main/res/drawable/bg_his_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_history_clear_selector.xml b/app/src/main/res/drawable/bg_history_clear_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_history_clear_unfocus.xml b/app/src/main/res/drawable/bg_history_clear_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_history_selector.xml b/app/src/main/res/drawable/bg_history_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_history_selector1.xml b/app/src/main/res/drawable/bg_history_selector1.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_history_selector2.xml b/app/src/main/res/drawable/bg_history_selector2.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_history_selector3.xml b/app/src/main/res/drawable/bg_history_selector3.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_index_sub_nav.xml b/app/src/main/res/drawable/bg_index_sub_nav.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_item_match_focus.xml b/app/src/main/res/drawable/bg_item_match_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_list_tag_selector.xml b/app/src/main/res/drawable/bg_list_tag_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_login.webp b/app/src/main/res/drawable/bg_login.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_login_bt.webp b/app/src/main/res/drawable/bg_login_bt.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_login_nber_no.xml b/app/src/main/res/drawable/bg_login_nber_no.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_login_nber_w.xml b/app/src/main/res/drawable/bg_login_nber_w.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_login_top.png b/app/src/main/res/drawable/bg_login_top.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_login_top_selector.xml b/app/src/main/res/drawable/bg_login_top_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_main_agree_selector.xml b/app/src/main/res/drawable/bg_main_agree_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_match_item_selector.xml b/app/src/main/res/drawable/bg_match_item_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_media_focus.xml b/app/src/main/res/drawable/bg_media_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_media_list_selector.xml b/app/src/main/res/drawable/bg_media_list_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_media_selector.xml b/app/src/main/res/drawable/bg_media_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_media_unfocus.xml b/app/src/main/res/drawable/bg_media_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_order_btn_selector.xml b/app/src/main/res/drawable/bg_order_btn_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_order_center_selector.xml b/app/src/main/res/drawable/bg_order_center_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_order_focus.xml b/app/src/main/res/drawable/bg_order_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_order_focus_down.xml b/app/src/main/res/drawable/bg_order_focus_down.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_order_unfocus.xml b/app/src/main/res/drawable/bg_order_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_order_xufei_selector.xml b/app/src/main/res/drawable/bg_order_xufei_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_ordergame_selector.xml b/app/src/main/res/drawable/bg_ordergame_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_orderlist.xml b/app/src/main/res/drawable/bg_orderlist.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_qr_code_card.xml b/app/src/main/res/drawable/bg_qr_code_card.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_qr_code_dialog_card.xml b/app/src/main/res/drawable/bg_qr_code_dialog_card.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_qr_code_image_rounded.xml b/app/src/main/res/drawable/bg_qr_code_image_rounded.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_search_back_btn_selector.xml b/app/src/main/res/drawable/bg_search_back_btn_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_search_history_selector.xml b/app/src/main/res/drawable/bg_search_history_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_setting_item_selector.xml b/app/src/main/res/drawable/bg_setting_item_selector.xml new file mode 100644 index 0000000..1c3c375 --- /dev/null +++ b/app/src/main/res/drawable/bg_setting_item_selector.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_shap1_selector.xml b/app/src/main/res/drawable/bg_shap1_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_shap1_unfocus.xml b/app/src/main/res/drawable/bg_shap1_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_shap_focus.xml b/app/src/main/res/drawable/bg_shap_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_shap_selector.xml b/app/src/main/res/drawable/bg_shap_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_shap_unfocus.xml b/app/src/main/res/drawable/bg_shap_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_shap_useragreenment.xml b/app/src/main/res/drawable/bg_shap_useragreenment.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_short_video_selector.xml b/app/src/main/res/drawable/bg_short_video_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_splash.webp b/app/src/main/res/drawable/bg_splash.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_suggest_item_selector.xml b/app/src/main/res/drawable/bg_suggest_item_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_trysee_open.xml b/app/src/main/res/drawable/bg_trysee_open.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_update_tv_selector.xml b/app/src/main/res/drawable/bg_update_tv_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_useragreen_selector.xml b/app/src/main/res/drawable/bg_useragreen_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_video_btn.xml b/app/src/main/res/drawable/bg_video_btn.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_video_key_prompt_tips.xml b/app/src/main/res/drawable/bg_video_key_prompt_tips.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bg_vr_check_bottom_mask.xml b/app/src/main/res/drawable/bg_vr_check_bottom_mask.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bgc_content_focus.xml b/app/src/main/res/drawable/bgc_content_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/bgc_content_selector.xml b/app/src/main/res/drawable/bgc_content_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/btn_common_top_selector.xml b/app/src/main/res/drawable/btn_common_top_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/btn_home_logout_selector.xml b/app/src/main/res/drawable/btn_home_logout_selector.xml new file mode 100644 index 0000000..3c88a72 --- /dev/null +++ b/app/src/main/res/drawable/btn_home_logout_selector.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/btn_my_back_top_selector.xml b/app/src/main/res/drawable/btn_my_back_top_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/btn_my_order_page_selector.xml b/app/src/main/res/drawable/btn_my_order_page_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/btn_my_search_selector.xml b/app/src/main/res/drawable/btn_my_search_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/btn_my_user_selector.xml b/app/src/main/res/drawable/btn_my_user_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/btn_orderlist.xml b/app/src/main/res/drawable/btn_orderlist.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/btn_to_top_selector.xml b/app/src/main/res/drawable/btn_to_top_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/btn_user_exit_selector.xml b/app/src/main/res/drawable/btn_user_exit_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/code_bg.webp b/app/src/main/res/drawable/code_bg.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/common_matchtext_bg_focus.xml b/app/src/main/res/drawable/common_matchtext_bg_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/common_matchtext_bg_unfocus.xml b/app/src/main/res/drawable/common_matchtext_bg_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/common_matchtextlive_bg_unfocus.xml b/app/src/main/res/drawable/common_matchtextlive_bg_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/common_notice_bg.xml b/app/src/main/res/drawable/common_notice_bg.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/common_set_btn_bg_focus.xml b/app/src/main/res/drawable/common_set_btn_bg_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/common_text_bg_cir_focus.xml b/app/src/main/res/drawable/common_text_bg_cir_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/common_text_bg_focus.xml b/app/src/main/res/drawable/common_text_bg_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/common_text_bg_round_focus.xml b/app/src/main/res/drawable/common_text_bg_round_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/common_text_color_selector.xml b/app/src/main/res/drawable/common_text_color_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/common_top_btn_bg_focus.xml b/app/src/main/res/drawable/common_top_btn_bg_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/cursor_color.xml b/app/src/main/res/drawable/cursor_color.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/detail_program_page_divider.xml b/app/src/main/res/drawable/detail_program_page_divider.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/dialog_order_bg.png b/app/src/main/res/drawable/dialog_order_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/downapkbg.png b/app/src/main/res/drawable/downapkbg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/filter_mask_bg.xml b/app/src/main/res/drawable/filter_mask_bg.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/gameorder.webp b/app/src/main/res/drawable/gameorder.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/hisroty_bg.webp b/app/src/main/res/drawable/hisroty_bg.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/history_del_focus.webp b/app/src/main/res/drawable/history_del_focus.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/history_del_nor.png b/app/src/main/res/drawable/history_del_nor.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/history_delete.png b/app/src/main/res/drawable/history_delete.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/history_no_data.png b/app/src/main/res/drawable/history_no_data.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/history_text_bg_selector.xml b/app/src/main/res/drawable/history_text_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/history_top_bg_selector.xml b/app/src/main/res/drawable/history_top_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/hotcontent_item_selector.xml b/app/src/main/res/drawable/hotcontent_item_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/ic_detail_item_playing.gif b/app/src/main/res/drawable/ic_detail_item_playing.gif old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/icon_common_top_search_selector.xml b/app/src/main/res/drawable/icon_common_top_search_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/icon_team_logo_default.xml b/app/src/main/res/drawable/icon_team_logo_default.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/icon_to_top_selector.xml b/app/src/main/res/drawable/icon_to_top_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/icon_video_play.png b/app/src/main/res/drawable/icon_video_play.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/icon_vip.png b/app/src/main/res/drawable/icon_vip.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/index_my_history_mask_bg.xml b/app/src/main/res/drawable/index_my_history_mask_bg.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/index_my_seekbar_style.xml b/app/src/main/res/drawable/index_my_seekbar_style.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/index_my_user_bg.xml b/app/src/main/res/drawable/index_my_user_bg.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/index_nav_item_bg_selector.xml b/app/src/main/res/drawable/index_nav_item_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/index_set_item_bg_selector.xml b/app/src/main/res/drawable/index_set_item_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/indicator_demand_tab.xml b/app/src/main/res/drawable/indicator_demand_tab.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/indicator_detail_tab.xml b/app/src/main/res/drawable/indicator_detail_tab.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/keyboard_item_selector.xml b/app/src/main/res/drawable/keyboard_item_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/loading_player_animation.xml b/app/src/main/res/drawable/loading_player_animation.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/loadingicon.png b/app/src/main/res/drawable/loadingicon.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_bg.webp b/app/src/main/res/drawable/login_bg.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_cancle_bg.png b/app/src/main/res/drawable/login_cancle_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_code_bg.png b/app/src/main/res/drawable/login_code_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_code_bg_focus.png b/app/src/main/res/drawable/login_code_bg_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_code_bg_selector.xml b/app/src/main/res/drawable/login_code_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_codebtn_bg_selector.xml b/app/src/main/res/drawable/login_codebtn_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_codebtn_focus.png b/app/src/main/res/drawable/login_codebtn_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_codebtn_nor.png b/app/src/main/res/drawable/login_codebtn_nor.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_getcode_btn_bg.webp b/app/src/main/res/drawable/login_getcode_btn_bg.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_icon_bg.webp b/app/src/main/res/drawable/login_icon_bg.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_item_bg_focus.png b/app/src/main/res/drawable/login_item_bg_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_item_bg_nor.png b/app/src/main/res/drawable/login_item_bg_nor.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_line_1.webp b/app/src/main/res/drawable/login_line_1.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_phone_bg.png b/app/src/main/res/drawable/login_phone_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_phone_bg_focus.png b/app/src/main/res/drawable/login_phone_bg_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_phone_bg_selector.xml b/app/src/main/res/drawable/login_phone_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_show_bg.xml b/app/src/main/res/drawable/login_show_bg.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_tolin_btn_bg.png b/app/src/main/res/drawable/login_tolin_btn_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_tologin_bg_focus.png b/app/src/main/res/drawable/login_tologin_bg_focus.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/login_tologin_bg_selector.xml b/app/src/main/res/drawable/login_tologin_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/logout_text_color_selector.xml b/app/src/main/res/drawable/logout_text_color_selector.xml new file mode 100644 index 0000000..26806f9 --- /dev/null +++ b/app/src/main/res/drawable/logout_text_color_selector.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/match_all_tv_selector.xml b/app/src/main/res/drawable/match_all_tv_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/match_list_all_selector.xml b/app/src/main/res/drawable/match_list_all_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/match_list_all_unfocus.xml b/app/src/main/res/drawable/match_list_all_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/match_list_bg.xml b/app/src/main/res/drawable/match_list_bg.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/match_list_focus.xml b/app/src/main/res/drawable/match_list_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/match_list_focus_sport.xml b/app/src/main/res/drawable/match_list_focus_sport.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/match_list_selector.xml b/app/src/main/res/drawable/match_list_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/match_list_selector_sport.xml b/app/src/main/res/drawable/match_list_selector_sport.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/matchlist_no_data_focus.xml b/app/src/main/res/drawable/matchlist_no_data_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/matchlist_no_data_selector.xml b/app/src/main/res/drawable/matchlist_no_data_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/mb.png b/app/src/main/res/drawable/mb.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/no_data_img.png b/app/src/main/res/drawable/no_data_img.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/order_bg.webp b/app/src/main/res/drawable/order_bg.webp old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/order_center_keyboader_bg_focus.xml b/app/src/main/res/drawable/order_center_keyboader_bg_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/order_center_keyboard_bg_selector.xml b/app/src/main/res/drawable/order_center_keyboard_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/order_center_xufei_bg_focus.xml b/app/src/main/res/drawable/order_center_xufei_bg_focus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/order_center_xufei_bg_selector.xml b/app/src/main/res/drawable/order_center_xufei_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/order_center_xufei_bg_unfocus.xml b/app/src/main/res/drawable/order_center_xufei_bg_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/order_keyboard_item_selector.xml b/app/src/main/res/drawable/order_keyboard_item_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/ordercenter_item_bigfoces_bg.png b/app/src/main/res/drawable/ordercenter_item_bigfoces_bg.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/progress_bar_custom.xml b/app/src/main/res/drawable/progress_bar_custom.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/progress_circle.xml b/app/src/main/res/drawable/progress_circle.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/progressbar_end.xml b/app/src/main/res/drawable/progressbar_end.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/progressbar_start.xml b/app/src/main/res/drawable/progressbar_start.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/progressbar_update_h.xml b/app/src/main/res/drawable/progressbar_update_h.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/progressbar_update_other.xml b/app/src/main/res/drawable/progressbar_update_other.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/rect_no.png b/app/src/main/res/drawable/rect_no.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/rect_ok.png b/app/src/main/res/drawable/rect_ok.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/saomakuang.png b/app/src/main/res/drawable/saomakuang.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/saosao.png b/app/src/main/res/drawable/saosao.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/search_keyboard_bg_selector.xml b/app/src/main/res/drawable/search_keyboard_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/selector_bg_activate_page_btn.xml b/app/src/main/res/drawable/selector_bg_activate_page_btn.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/selector_btn_update_now.xml b/app/src/main/res/drawable/selector_btn_update_now.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/selector_txt_update_now.xml b/app/src/main/res/drawable/selector_txt_update_now.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/switch_keyboard_bg_selector.xml b/app/src/main/res/drawable/switch_keyboard_bg_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/switch_keyboard_bg_unfocus.xml b/app/src/main/res/drawable/switch_keyboard_bg_unfocus.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/sxtitle.png b/app/src/main/res/drawable/sxtitle.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/tryseeoverbg.xml b/app/src/main/res/drawable/tryseeoverbg.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/update_tv_selector.xml b/app/src/main/res/drawable/update_tv_selector.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/video_bg_img.xml b/app/src/main/res/drawable/video_bg_img.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/weilai_btn_error.xml b/app/src/main/res/drawable/weilai_btn_error.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable/weilai_btn_error_textcolor.xml b/app/src/main/res/drawable/weilai_btn_error_textcolor.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/layout-land/device_not_activated_view.xml b/app/src/main/res/layout-land/device_not_activated_view.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/layout/activity_about_my.xml b/app/src/main/res/layout/activity_about_my.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/layout/activity_device.xml b/app/src/main/res/layout/activity_device.xml old mode 100755 new mode 100644 index dbda194..9811359 --- a/app/src/main/res/layout/activity_device.xml +++ b/app/src/main/res/layout/activity_device.xml @@ -203,15 +203,63 @@ android:textColor="@color/white6" android:textSize="@dimen/mg_sp_24" /> + + + + + + + + + + + + + + @@ -80,6 +80,7 @@ android:layout_marginTop="@dimen/qb_px_125" app:layout_constraintLeft_toRightOf="@id/id_cl_orderCenter" android:focusable="true" + android:visibility="gone" android:background="@drawable/order_btn_unfocus_bg" app:layout_constraintTop_toTopOf="parent"> @@ -156,7 +157,9 @@ android:layout_width="match_parent" android:layout_height="0dp" android:focusable="false" + android:id="@+id/cl_center_zone" app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginBottom="@dimen/qb_px_95" app:layout_constraintTop_toBottomOf="@id/id_cl_myOrder"> @@ -220,14 +222,23 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:focusable="false" - android:layout_marginBottom="@dimen/qb_px_76" - android:layout_marginLeft="@dimen/qb_px_827" + android:layout_marginLeft="@dimen/qb_px_798" app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintBottom_toBottomOf="parent" - android:text="版权渠道:橘视厅 备案号:视听备【2025】A2580" + app:layout_constraintTop_toBottomOf="@+id/cl_center_zone" + android:text="版权渠道:橘视厅 备案号:" android:textSize="@dimen/mg_sp_16" android:textColor="@color/white30" /> + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_test.xml b/app/src/main/res/layout/activity_test.xml old mode 100755 new mode 100644 diff --git a/app/src/main/res/layout/activity_thirdparty_entry.xml b/app/src/main/res/layout/activity_thirdparty_entry.xml deleted file mode 100644 index ba96d3c..0000000 --- a/app/src/main/res/layout/activity_thirdparty_entry.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - -