From 96a3e641ce319dfb020a6f2f8595d6e418556535 Mon Sep 17 00:00:00 2001
From: renhaoting <370797079@qq.com>
Date: Wed, 17 Dec 2025 13:41:41 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Ebill=E6=A8=A1=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/build.gradle | 2 +-
app/config_debug.gradle | 49 +
app/config_release.gradle | 48 +
base/.gitignore | 1 +
base/build.gradle.kts | 85 ++
base/consumer-rules.pro | 0
base/proguard-rules.pro | 21 +
.../com/remax/base/ExampleInstrumentedTest.kt | 24 +
base/src/main/AndroidManifest.xml | 14 +
.../com/remax/base/ads/AdRevenueReporter.kt | 124 ++
.../SystemPageNavigationController.kt | 118 ++
.../base/controller/UserChannelController.kt | 217 ++++
.../java/com/remax/base/ext/KvDelegate.kt | 83 ++
.../base/ext/NotificationPermissionExt.kt | 84 ++
.../remax/base/ext/StoragePermissionExt.kt | 51 +
.../java/com/remax/base/ext/ViewBindUtil.kt | 110 ++
.../com/remax/base/ext/WindowInsetsExt.kt | 214 ++++
.../java/com/remax/base/log/BaseLogger.kt | 159 +++
.../remax/base/provider/BaseModuleProvider.kt | 59 +
.../com/remax/base/report/DataReporter.kt | 134 ++
.../java/com/remax/base/temlate/BaseAct.kt | 136 ++
.../com/remax/base/temlate/BaseBindAct.kt | 17 +
.../com/remax/base/temlate/BaseBindFrag.kt | 37 +
.../java/com/remax/base/temlate/BaseFrag.kt | 50 +
.../com/remax/base/utils/ActivityLauncher.kt | 65 +
.../com/remax/base/utils/ContextProvider.kt | 50 +
.../java/com/remax/base/utils/FileScanner.kt | 1139 +++++++++++++++++
.../java/com/remax/base/utils/ImageLoader.kt | 164 +++
.../remax/base/utils/LanguageController.kt | 111 ++
.../java/com/remax/base/utils/LottieUtils.kt | 168 +++
.../remax/base/utils/RemoteConfigManager.kt | 267 ++++
.../utils/RoundedCornersTransformation.kt | 57 +
.../base/view/GridSpacingItemDecoration.kt | 95 ++
.../base/view/LinearDividerItemDecoration.kt | 176 +++
.../remax/base/view/RecyclerViewExtensions.kt | 70 +
.../base/view/RemaxRoundedFrameLayout.kt | 141 ++
base/src/main/res/values-night/themes.xml | 2 +
base/src/main/res/values/attrs.xml | 16 +
base/src/main/res/values/colors.xml | 3 +
base/src/main/res/values/strings.xml | 3 +
base/src/main/res/values/themes.xml | 2 +
.../java/com/remax/base/ExampleUnitTest.kt | 17 +
bill/.gitignore | 1 +
bill/build.gradle.kts | 143 +++
bill/consumer-rules.pro | 0
bill/proguard-rules.pro | 21 +
.../com/remax/bill/ExampleInstrumentedTest.kt | 24 +
bill/src/main/AndroidManifest.xml | 35 +
bill/src/main/assets/ad_config.json | 48 +
.../remax/bill/ads/AdActivityInterceptor.kt | 111 ++
.../java/com/remax/bill/ads/AdMobManager.kt | 112 ++
.../main/java/com/remax/bill/ads/AdResult.kt | 43 +
.../com/remax/bill/ads/AppOpenAdController.kt | 509 ++++++++
.../com/remax/bill/ads/BannerAdController.kt | 595 +++++++++
.../bill/ads/FullScreenNativeAdController.kt | 639 +++++++++
.../bill/ads/InterstitialAdController.kt | 546 ++++++++
.../com/remax/bill/ads/NativeAdController.kt | 582 +++++++++
.../com/remax/bill/ads/PreloadController.kt | 192 +++
.../remax/bill/ads/RewardedAdController.kt | 594 +++++++++
.../bill/ads/bidding/AdSourceController.kt | 85 ++
.../bidding/AdSourceSelectionBottomSheet.kt | 104 ++
.../ads/bidding/AppOpenBiddingInitializer.kt | 91 ++
.../bill/ads/bidding/AppOpenBiddingManager.kt | 124 ++
.../bill/ads/bidding/BannerBiddingManager.kt | 124 ++
.../ads/bidding/BiddingPlatformController.kt | 108 ++
.../remax/bill/ads/bidding/BiddingWinner.kt | 5 +
.../bidding/FullScreenNativeBiddingManager.kt | 124 ++
.../ads/bidding/InterstitialBiddingManager.kt | 128 ++
.../bill/ads/bidding/NativeBiddingManager.kt | 124 ++
.../ads/bidding/RewardedBiddingManager.kt | 116 ++
.../com/remax/bill/ads/config/AdConfig.kt | 177 +++
.../com/remax/bill/ads/config/AdConfigData.kt | 43 +
.../remax/bill/ads/config/AdConfigManager.kt | 339 +++++
.../java/com/remax/bill/ads/ext/AdShowExt.kt | 278 ++++
.../bill/ads/interceptor/AdInterceptor.kt | 201 +++
.../java/com/remax/bill/ads/log/AdLogger.kt | 159 +++
.../ads/pangle/PangleAppOpenAdController.kt | 555 ++++++++
.../ads/pangle/PangleBannerAdController.kt | 637 +++++++++
.../PangleFullScreenNativeAdController.kt | 546 ++++++++
.../pangle/PangleInterstitialAdController.kt | 594 +++++++++
.../remax/bill/ads/pangle/PangleManager.kt | 149 +++
.../ads/pangle/PangleNativeAdController.kt | 625 +++++++++
.../ads/pangle/PangleRewardedAdController.kt | 522 ++++++++
.../bill/ads/provider/AdModuleProvider.kt | 59 +
.../bill/ads/topon/TopOnBannerAdController.kt | 579 +++++++++
.../TopOnFullScreenNativeAdController.kt | 655 ++++++++++
.../topon/TopOnInterstitialAdController.kt | 613 +++++++++
.../com/remax/bill/ads/topon/TopOnManager.kt | 148 +++
.../bill/ads/topon/TopOnNativeAdController.kt | 629 +++++++++
.../ads/topon/TopOnRewardedAdController.kt | 627 +++++++++
.../bill/ads/topon/TopOnSplashAdController.kt | 554 ++++++++
.../bill/ads/util/AdmobReflectionUtil.kt | 152 +++
.../com/remax/bill/ads/util/PositionGet.kt | 26 +
.../java/com/remax/bill/ui/BannerAdView.kt | 92 ++
.../bill/ui/FullScreenNativeAdActivity.kt | 202 +++
.../remax/bill/ui/FullScreenNativeAdView.kt | 162 +++
.../java/com/remax/bill/ui/NativeAdStyle.kt | 31 +
.../java/com/remax/bill/ui/NativeAdView.kt | 227 ++++
.../remax/bill/ui/dialog/ADLoadingDialog.kt | 100 ++
.../PangleFullScreenNativeAdActivity.kt | 163 +++
.../ui/pangle/PangleFullScreenNativeAdView.kt | 153 +++
.../bill/ui/pangle/PangleNativeAdStyle.kt | 26 +
.../bill/ui/pangle/PangleNativeAdView.kt | 194 +++
.../remax/bill/ui/topon/ToponBannerAdView.kt | 78 ++
.../topon/ToponFullScreenNativeAdActivity.kt | 164 +++
.../ui/topon/ToponFullScreenNativeAdView.kt | 225 ++++
.../remax/bill/ui/topon/ToponNativeAdStyle.kt | 33 +
.../remax/bill/ui/topon/ToponNativeAdView.kt | 259 ++++
.../main/res/drawable/bg_ad_cta_button.xml | 13 +
.../res/drawable/bg_ad_cta_button_green.xml | 10 +
.../res/drawable/bg_ad_label_enhanced.xml | 14 +
.../drawable/bg_ad_label_enhanced_green.xml | 14 +
.../main/res/drawable/bg_ad_label_gray.xml | 9 +
.../res/drawable/bg_button_gray_rounded.xml | 6 +
.../main/res/drawable/bg_button_rounded.xml | 6 +
.../main/res/drawable/bg_native_ad_card.xml | 14 +
bill/src/main/res/drawable/ic_full_close.xml | 12 +
bill/src/main/res/drawable/ic_star_empty.xml | 14 +
bill/src/main/res/drawable/ic_star_filled.xml | 13 +
.../res/drawable/ic_star_filled_green.xml | 13 +
bill/src/main/res/drawable/ic_star_half.xml | 16 +
.../main/res/drawable/ic_star_half_green.xml | 16 +
.../main/res/drawable/progress_background.xml | 10 +
.../src/main/res/drawable/progress_custom.xml | 24 +
.../layout/activity_full_screen_native_ad.xml | 39 +
.../activity_pangle_full_screen_native_ad.xml | 37 +
.../activity_topon_full_screen_native_ad.xml | 38 +
.../res/layout/dialog_ad_source_selection.xml | 34 +
bill/src/main/res/layout/item_ad_source.xml | 35 +
.../res/layout/layout_ad_dialog_loading.xml | 47 +
.../src/main/res/layout/layout_ad_loading.xml | 22 +
.../res/layout/layout_banner_container.xml | 14 +
.../main/res/layout/layout_banner_loading.xml | 23 +
.../res/layout/layout_fullscreen_loading.xml | 34 +
.../layout/layout_fullscreen_native_ad.xml | 116 ++
.../main/res/layout/layout_native_ad_card.xml | 157 +++
.../res/layout/layout_native_ad_card2.xml | 157 +++
.../src/main/res/layout/layout_native_ads.xml | 143 +++
.../layout_pangle_fullscreen_native_ad.xml | 117 ++
.../res/layout/layout_pangle_native_ads.xml | 106 ++
.../layout/layout_pangle_native_ads_large.xml | 124 ++
.../layout_topon_fullscreen_native_ad.xml | 108 ++
.../res/layout/layout_topon_native_ads.xml | 98 ++
.../layout/layout_topon_native_ads_large.xml | 112 ++
bill/src/main/res/values-night/themes.xml | 2 +
bill/src/main/res/values/colors.xml | 5 +
bill/src/main/res/values/strings.xml | 5 +
bill/src/main/res/values/themes.xml | 14 +
.../java/com/remax/bill/ExampleUnitTest.kt | 17 +
build.gradle | 11 +
gradle/libs.versions.toml | 41 +-
settings.gradle | 15 +
152 files changed, 22558 insertions(+), 4 deletions(-)
create mode 100644 app/config_debug.gradle
create mode 100644 app/config_release.gradle
create mode 100644 base/.gitignore
create mode 100644 base/build.gradle.kts
create mode 100644 base/consumer-rules.pro
create mode 100644 base/proguard-rules.pro
create mode 100644 base/src/androidTest/java/com/remax/base/ExampleInstrumentedTest.kt
create mode 100644 base/src/main/AndroidManifest.xml
create mode 100644 base/src/main/java/com/remax/base/ads/AdRevenueReporter.kt
create mode 100644 base/src/main/java/com/remax/base/controller/SystemPageNavigationController.kt
create mode 100644 base/src/main/java/com/remax/base/controller/UserChannelController.kt
create mode 100644 base/src/main/java/com/remax/base/ext/KvDelegate.kt
create mode 100644 base/src/main/java/com/remax/base/ext/NotificationPermissionExt.kt
create mode 100644 base/src/main/java/com/remax/base/ext/StoragePermissionExt.kt
create mode 100644 base/src/main/java/com/remax/base/ext/ViewBindUtil.kt
create mode 100644 base/src/main/java/com/remax/base/ext/WindowInsetsExt.kt
create mode 100644 base/src/main/java/com/remax/base/log/BaseLogger.kt
create mode 100644 base/src/main/java/com/remax/base/provider/BaseModuleProvider.kt
create mode 100644 base/src/main/java/com/remax/base/report/DataReporter.kt
create mode 100644 base/src/main/java/com/remax/base/temlate/BaseAct.kt
create mode 100644 base/src/main/java/com/remax/base/temlate/BaseBindAct.kt
create mode 100644 base/src/main/java/com/remax/base/temlate/BaseBindFrag.kt
create mode 100644 base/src/main/java/com/remax/base/temlate/BaseFrag.kt
create mode 100644 base/src/main/java/com/remax/base/utils/ActivityLauncher.kt
create mode 100644 base/src/main/java/com/remax/base/utils/ContextProvider.kt
create mode 100644 base/src/main/java/com/remax/base/utils/FileScanner.kt
create mode 100644 base/src/main/java/com/remax/base/utils/ImageLoader.kt
create mode 100644 base/src/main/java/com/remax/base/utils/LanguageController.kt
create mode 100644 base/src/main/java/com/remax/base/utils/LottieUtils.kt
create mode 100644 base/src/main/java/com/remax/base/utils/RemoteConfigManager.kt
create mode 100644 base/src/main/java/com/remax/base/utils/RoundedCornersTransformation.kt
create mode 100644 base/src/main/java/com/remax/base/view/GridSpacingItemDecoration.kt
create mode 100644 base/src/main/java/com/remax/base/view/LinearDividerItemDecoration.kt
create mode 100644 base/src/main/java/com/remax/base/view/RecyclerViewExtensions.kt
create mode 100644 base/src/main/java/com/remax/base/view/RemaxRoundedFrameLayout.kt
create mode 100644 base/src/main/res/values-night/themes.xml
create mode 100644 base/src/main/res/values/attrs.xml
create mode 100644 base/src/main/res/values/colors.xml
create mode 100644 base/src/main/res/values/strings.xml
create mode 100644 base/src/main/res/values/themes.xml
create mode 100644 base/src/test/java/com/remax/base/ExampleUnitTest.kt
create mode 100644 bill/.gitignore
create mode 100644 bill/build.gradle.kts
create mode 100644 bill/consumer-rules.pro
create mode 100644 bill/proguard-rules.pro
create mode 100644 bill/src/androidTest/java/com/remax/bill/ExampleInstrumentedTest.kt
create mode 100644 bill/src/main/AndroidManifest.xml
create mode 100644 bill/src/main/assets/ad_config.json
create mode 100644 bill/src/main/java/com/remax/bill/ads/AdActivityInterceptor.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/AdMobManager.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/AdResult.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/AppOpenAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/BannerAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/FullScreenNativeAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/InterstitialAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/NativeAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/PreloadController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/RewardedAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/AdSourceController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/AdSourceSelectionBottomSheet.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/AppOpenBiddingInitializer.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/AppOpenBiddingManager.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/BannerBiddingManager.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/BiddingPlatformController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/BiddingWinner.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/FullScreenNativeBiddingManager.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/InterstitialBiddingManager.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/NativeBiddingManager.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/bidding/RewardedBiddingManager.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/config/AdConfig.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/config/AdConfigData.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/config/AdConfigManager.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/ext/AdShowExt.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/interceptor/AdInterceptor.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/log/AdLogger.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/pangle/PangleAppOpenAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/pangle/PangleBannerAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/pangle/PangleFullScreenNativeAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/pangle/PangleInterstitialAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/pangle/PangleManager.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/pangle/PangleNativeAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/pangle/PangleRewardedAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/provider/AdModuleProvider.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/topon/TopOnBannerAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/topon/TopOnFullScreenNativeAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/topon/TopOnInterstitialAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/topon/TopOnManager.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/topon/TopOnNativeAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/topon/TopOnRewardedAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/topon/TopOnSplashAdController.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/util/AdmobReflectionUtil.kt
create mode 100644 bill/src/main/java/com/remax/bill/ads/util/PositionGet.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/BannerAdView.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/FullScreenNativeAdActivity.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/FullScreenNativeAdView.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/NativeAdStyle.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/NativeAdView.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/dialog/ADLoadingDialog.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/pangle/PangleFullScreenNativeAdActivity.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/pangle/PangleFullScreenNativeAdView.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/pangle/PangleNativeAdStyle.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/pangle/PangleNativeAdView.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/topon/ToponBannerAdView.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/topon/ToponFullScreenNativeAdActivity.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/topon/ToponFullScreenNativeAdView.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/topon/ToponNativeAdStyle.kt
create mode 100644 bill/src/main/java/com/remax/bill/ui/topon/ToponNativeAdView.kt
create mode 100644 bill/src/main/res/drawable/bg_ad_cta_button.xml
create mode 100644 bill/src/main/res/drawable/bg_ad_cta_button_green.xml
create mode 100644 bill/src/main/res/drawable/bg_ad_label_enhanced.xml
create mode 100644 bill/src/main/res/drawable/bg_ad_label_enhanced_green.xml
create mode 100644 bill/src/main/res/drawable/bg_ad_label_gray.xml
create mode 100644 bill/src/main/res/drawable/bg_button_gray_rounded.xml
create mode 100644 bill/src/main/res/drawable/bg_button_rounded.xml
create mode 100644 bill/src/main/res/drawable/bg_native_ad_card.xml
create mode 100644 bill/src/main/res/drawable/ic_full_close.xml
create mode 100644 bill/src/main/res/drawable/ic_star_empty.xml
create mode 100644 bill/src/main/res/drawable/ic_star_filled.xml
create mode 100644 bill/src/main/res/drawable/ic_star_filled_green.xml
create mode 100644 bill/src/main/res/drawable/ic_star_half.xml
create mode 100644 bill/src/main/res/drawable/ic_star_half_green.xml
create mode 100644 bill/src/main/res/drawable/progress_background.xml
create mode 100644 bill/src/main/res/drawable/progress_custom.xml
create mode 100644 bill/src/main/res/layout/activity_full_screen_native_ad.xml
create mode 100644 bill/src/main/res/layout/activity_pangle_full_screen_native_ad.xml
create mode 100644 bill/src/main/res/layout/activity_topon_full_screen_native_ad.xml
create mode 100644 bill/src/main/res/layout/dialog_ad_source_selection.xml
create mode 100644 bill/src/main/res/layout/item_ad_source.xml
create mode 100644 bill/src/main/res/layout/layout_ad_dialog_loading.xml
create mode 100644 bill/src/main/res/layout/layout_ad_loading.xml
create mode 100644 bill/src/main/res/layout/layout_banner_container.xml
create mode 100644 bill/src/main/res/layout/layout_banner_loading.xml
create mode 100644 bill/src/main/res/layout/layout_fullscreen_loading.xml
create mode 100644 bill/src/main/res/layout/layout_fullscreen_native_ad.xml
create mode 100644 bill/src/main/res/layout/layout_native_ad_card.xml
create mode 100644 bill/src/main/res/layout/layout_native_ad_card2.xml
create mode 100644 bill/src/main/res/layout/layout_native_ads.xml
create mode 100644 bill/src/main/res/layout/layout_pangle_fullscreen_native_ad.xml
create mode 100644 bill/src/main/res/layout/layout_pangle_native_ads.xml
create mode 100644 bill/src/main/res/layout/layout_pangle_native_ads_large.xml
create mode 100644 bill/src/main/res/layout/layout_topon_fullscreen_native_ad.xml
create mode 100644 bill/src/main/res/layout/layout_topon_native_ads.xml
create mode 100644 bill/src/main/res/layout/layout_topon_native_ads_large.xml
create mode 100644 bill/src/main/res/values-night/themes.xml
create mode 100644 bill/src/main/res/values/colors.xml
create mode 100644 bill/src/main/res/values/strings.xml
create mode 100644 bill/src/main/res/values/themes.xml
create mode 100644 bill/src/test/java/com/remax/bill/ExampleUnitTest.kt
diff --git a/app/build.gradle b/app/build.gradle
index 97910e5..3860370 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -70,7 +70,7 @@ dependencies {
implementation(project(":core:architecture"))
//api(project(":core:architecture-reflect"))
implementation(project(":core:network"))
-
+ implementation(project(":bill"))
implementation(project(":youtube:core"))
implementation(project(":youtube:custom-ui"))
diff --git a/app/config_debug.gradle b/app/config_debug.gradle
new file mode 100644
index 0000000..1a4f8b6
--- /dev/null
+++ b/app/config_debug.gradle
@@ -0,0 +1,49 @@
+ext {
+ // AdMob配置
+ admob = [applicationId: "ca-app-pub-3940256099942544~3347511713", // 测试应用ID,请替换为实际的AdMob应用ID
+ adUnitIds : [banner : "ca-app-pub-3940256099942544/9214589741", // 横幅广告测试ID
+ interstitial: "ca-app-pub-3940256099942544/1033173712", // 插页广告测试ID
+ splash : "ca-app-pub-3940256099942544/9257395921", // 开屏广告测试ID
+ native : "ca-app-pub-3940256099942544/2247696110", // 原生广告测试ID
+ full_native : "ca-app-pub-3940256099942544/2247696110", // 全屏原生广告测试ID
+ rewarded : "ca-app-pub-3940256099942544/5224354917" // 激励广告测试ID
+ ]]
+
+ // Pangle配置
+ pangle = [applicationId: "8025677", // Pangle测试应用ID
+ adUnitIds : [splash : "890000078", // 开屏广告测试ID(竖屏)
+ banner : "980099802", // 横幅广告测试ID(320x50)
+ interstitial: "980088188", // 插页广告测试ID(竖屏)
+ native : "980088216", // 原生广告测试ID
+ full_native : "980088216", // 全屏原生广告测试ID
+ rewarded : "980088192" // 激励视频测试ID(竖屏)
+ ]]
+
+ // TopOn配置
+ topon = [applicationId: "a5aa1f9deda26d", // TopOn 应用 ID(需替换为实际值)
+ appKey : "4f7b9ac17decb9babec83aac078742c7", // TopOn 应用密钥(需替换为实际值)
+ adUnitIds : [interstitial: "b5baca53984692", // 插页广告位 ID(需替换为实际值)
+ rewarded : "b5b449fb3d89d7", // 激励广告位 ID
+ native : "b5aa1fa2cae775", // 原生广告位 ID(需替换为实际值)
+ splash : "b5f73fe0c5db29", // 开屏广告位 ID(需替换为实际值)
+ full_native : "b5aa1fa501d9f6", // 全屏原生广告位 ID(需替换为实际值)
+ banner : "b5baca4f74c3d8"] // 横幅广告位 ID(需替换为实际值)
+ ]
+
+ // 应用版本配置
+ app = [applicationId: "com.remax.video.recovery",
+ compileSdk : 35,
+ minSdk : libs.versions.minSdk.get().toInteger(),
+ targetSdk : 35,
+ versionCode : 1,
+ versionName : "1.0.2"]
+
+ url = [privacyUrl: "https://www.google.com",
+ teamUrl : "https://www.google.com",]
+
+ // 统计归因配置
+ analytics = [adjustAppToken: "h6qax9dxv7cw", // Adjust App Token
+ thinkingDataAppId: "61b7ef0186b74b76b301b67184b7b48b", // 数数 SDK APP ID
+ thinkingDataServerUrl: "https://xray.alifmd.com", // 数数上报域名
+ defaultUserChannel: "paid"] // 默认用户渠道,internal默认paid
+}
\ No newline at end of file
diff --git a/app/config_release.gradle b/app/config_release.gradle
new file mode 100644
index 0000000..e434022
--- /dev/null
+++ b/app/config_release.gradle
@@ -0,0 +1,48 @@
+ext {
+ // AdMob配置 - Play 市场版本
+ admob = [applicationId: "ca-app-pub-1350364678590045~1984631821", // Play市场AdMob应用ID
+ adUnitIds : [banner : "ca-app-pub-1350364678590045/8582717815", // 横幅广告正式ID
+ interstitial: "ca-app-pub-1350364678590045/5193588407", // 插页广告正式ID
+ splash : "ca-app-pub-1350364678590045/5568653612", // 开屏广告正式ID
+ native : "ca-app-pub-1350364678590045/8003245260", // 原生广告正式ID
+ full_native : "ca-app-pub-1350364678590045/1219233112" // 原生广告正式ID
+ ]]
+
+ // Pangle配置
+ pangle = [applicationId: "8750604", // Pangle测试应用ID
+ adUnitIds : [splash : "", // 开屏广告测试ID(竖屏)
+ banner : "", // 横幅广告测试ID(320x50)
+ interstitial: "982604080", // 插页广告测试ID(竖屏)
+ native : "", // 原生广告测试ID
+ full_native : "", // 全屏原生广告测试ID
+ rewarded : "" // 激励视频测试ID(竖屏)
+ ]]
+
+ // TopOn配置
+ topon = [applicationId: "h1gq3c2vm973ma", // TopOn 应用 ID(需替换为实际值)
+ appKey : "a96bffecc1c32132c6984a3e97512b5f9", // TopOn 应用密钥(需替换为实际值)
+ adUnitIds : [interstitial: "n1gq3c2vobnfr5", // 插页广告位 ID(需替换为实际值)
+ rewarded : "", // 激励广告位 ID
+ native : "n1gq3c2vpbobmj", // 原生广告位 ID(需替换为实际值)
+ splash : "n1gq3c2vppegpc", // 开屏广告位 ID(需替换为实际值)
+ full_native : "n1gq3c2volk4ad", // 全屏原生广告位 ID(需替换为实际值)
+ banner : "n1gq3c2vq30fsn"] // 横幅广告位 ID(需替换为实际值)
+ ]
+
+ // 应用版本配置 - Play 市场版本
+ app = [applicationId: "com.files.restore.recovery.tool.deleted.document.photo.video.audio.app.mobile.scan.utility",
+ compileSdk : 35,
+ minSdk : libs.versions.minSdk.get().toInteger(),
+ targetSdk : 35,
+ versionCode : 3,
+ versionName : "1.0.2"]
+
+ url = [privacyUrl: "https://alifmd.com/privacy.html",
+ teamUrl : "https://alifmd.com/privacy.html",]
+
+ // 统计归因配置
+ analytics = [adjustAppToken: "h6qax9dxv7cw", // Adjust App Token
+ thinkingDataAppId: "61b7ef0186b74b76b301b67184b7b48b", // 数数 SDK APP ID
+ thinkingDataServerUrl: "https://xray.alifmd.com",// 数数上报域名
+ defaultUserChannel: "natural"] //默认渠道
+}
\ No newline at end of file
diff --git a/base/.gitignore b/base/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/base/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/base/build.gradle.kts b/base/build.gradle.kts
new file mode 100644
index 0000000..69c79b5
--- /dev/null
+++ b/base/build.gradle.kts
@@ -0,0 +1,85 @@
+plugins {
+ alias(libs.plugins.androidLibrary)
+ alias(libs.plugins.kotlinAndroid)
+ //alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.room)
+ kotlin("kapt")
+}
+
+apply {
+ from("../app/config_debug.gradle")
+}
+
+val appConfig = findProperty("app") as Map<*, *>
+val analyticsConfig = findProperty("analytics") as Map<*, *>
+
+
+android {
+ namespace = "com.remax.base"
+ compileSdk = appConfig["compileSdk"] as Int
+
+ defaultConfig {
+ minSdk = appConfig["minSdk"] as Int
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ buildConfigField("String", "DEFAULT_USER_CHANNEL", "\"${analyticsConfig["defaultUserChannel"]}\"")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+ kotlinOptions {
+ jvmTarget = "11"
+ }
+ buildFeatures {
+ viewBinding = true
+ buildConfig = true
+ }
+}
+
+room {
+ schemaDirectory("$projectDir/schemas")
+}
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.appcompat)
+ implementation(libs.material)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.test.ext.junit)
+ androidTestImplementation(libs.espresso.core)
+
+ api(libs.utilcodex)
+ api(libs.androidx.core.ktx)
+ api(libs.androidx.lifecycle.runtime.ktx)
+ api(libs.androidx.room.runtime)
+ api(libs.androidx.databinding.runtime)
+ api(libs.constraintlayout)
+ api(libs.material)
+ kapt(libs.androidx.room.compiler)
+ api(libs.androidx.room.ktx)
+ api(libs.androidx.fragment.ktx)
+ api(libs.androidx.work.runtime)
+ api(libs.kotlinx.coroutines.core)
+ api(libs.kotlinx.coroutines.android)
+
+ // Glide图片加载库
+ api(libs.glide)
+ kapt(libs.glide.compiler)
+
+ // Lottie动画库
+ api(libs.lottie)
+
+ api(platform(libs.firebase.bom))
+ api(libs.firebase.config)
+ api(libs.firebase.analytics)
+ api(libs.firebase.crashlytics)
+
+ // Gson for JSON parsing
+ api(libs.gson)
+}
\ No newline at end of file
diff --git a/base/consumer-rules.pro b/base/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/base/proguard-rules.pro b/base/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/base/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/base/src/androidTest/java/com/remax/base/ExampleInstrumentedTest.kt b/base/src/androidTest/java/com/remax/base/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..f39e1b8
--- /dev/null
+++ b/base/src/androidTest/java/com/remax/base/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.remax.base
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.remax.base.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8696e30
--- /dev/null
+++ b/base/src/main/AndroidManifest.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/base/src/main/java/com/remax/base/ads/AdRevenueReporter.kt b/base/src/main/java/com/remax/base/ads/AdRevenueReporter.kt
new file mode 100644
index 0000000..55d148a
--- /dev/null
+++ b/base/src/main/java/com/remax/base/ads/AdRevenueReporter.kt
@@ -0,0 +1,124 @@
+package com.remax.base.ads
+
+/**
+ * 广告收益数据
+ * @param revenue 单次展示收益(值和货币)
+ * @param adRevenueNetwork 广告平台名称
+ * @param adRevenueUnit 广告位ID
+ * @param adRevenuePlacement 广告源ID
+ * @param adFormat 广告格式
+ */
+data class AdRevenueData(
+ val revenue: RevenueInfo,
+ val adRevenueNetwork: String,
+ val adRevenueUnit: String,
+ val adRevenuePlacement: String,
+ val adFormat: String
+)
+
+/**
+ * 收益信息
+ * @param value 收益值(USD)
+ * @param currencyCode 货币代码
+ */
+data class RevenueInfo(
+ val value: Double,
+ val currencyCode: String
+)
+
+/**
+ * 广告收益上报管理器接口
+ * 用于上报广告收益数据到第三方分析平台
+ */
+interface AdRevenueReporter {
+
+ /**
+ * 上报广告收益数据
+ * @param adRevenueData 广告收益数据
+ */
+ fun reportAdRevenue(adRevenueData: AdRevenueData)
+}
+
+/**
+ * 广告收益上报管理器
+ * 单例模式,支持外部注入具体实现
+ */
+object AdRevenueManager {
+
+ private val reporters = mutableListOf()
+
+ /**
+ * 设置广告收益上报器实现
+ * @param reporter 具体的上报器实现
+ */
+ fun setReporter(reporter: AdRevenueReporter) {
+ reporters.clear()
+ reporters.add(reporter)
+ }
+
+ /**
+ * 设置多个广告收益上报器实现
+ * @param reporters 上报器实现集合
+ */
+ fun setReporters(reporters: Collection) {
+ this.reporters.clear()
+ this.reporters.addAll(reporters)
+ }
+
+ /**
+ * 添加广告收益上报器实现
+ * @param reporter 具体的上报器实现
+ */
+ fun addReporter(reporter: AdRevenueReporter) {
+ if (!reporters.contains(reporter)) {
+ reporters.add(reporter)
+ }
+ }
+
+ /**
+ * 移除广告收益上报器实现
+ * @param reporter 要移除的上报器实现
+ */
+ fun removeReporter(reporter: AdRevenueReporter) {
+ reporters.remove(reporter)
+ }
+
+ /**
+ * 上报广告收益数据
+ * @param adRevenueData 广告收益数据
+ */
+ fun reportAdRevenue(adRevenueData: AdRevenueData) {
+ try {
+ reporters.forEach { reporter ->
+ try {
+ reporter.reportAdRevenue(adRevenueData)
+ } catch (e: Exception) {
+ // 单个上报器异常不影响其他上报器
+ e.printStackTrace()
+ }
+ }
+ } catch (e: Exception) {
+ // 静默处理异常,避免影响广告正常展示
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * 清除所有上报器
+ */
+ fun clearReporters() {
+ reporters.clear()
+ }
+
+ /**
+ * 获取当前上报器数量
+ * @return 上报器数量
+ */
+ fun getReporterCount(): Int = reporters.size
+
+ /**
+ * 检查是否有上报器
+ * @return 是否有上报器
+ */
+ fun hasReporters(): Boolean = reporters.isNotEmpty()
+}
diff --git a/base/src/main/java/com/remax/base/controller/SystemPageNavigationController.kt b/base/src/main/java/com/remax/base/controller/SystemPageNavigationController.kt
new file mode 100644
index 0000000..007f2d5
--- /dev/null
+++ b/base/src/main/java/com/remax/base/controller/SystemPageNavigationController.kt
@@ -0,0 +1,118 @@
+package com.remax.base.controller
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.provider.Settings
+import android.util.Log
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicLong
+
+/**
+ * System Page Navigation State Controller
+ * Used to manage state when returning from system pages, preventing splash screen from launching
+ */
+object SystemPageNavigationController {
+
+ private const val TAG = "SystemPageNavigation"
+
+ // Whether currently in system page
+ private val isInSystemPage = AtomicBoolean(false)
+
+ // Timestamp when entering system page
+ private val enterSystemPageTime = AtomicLong(0)
+
+ // System page timeout (milliseconds) - 5 minutes
+ private const val SYSTEM_PAGE_TIMEOUT = 5 * 60 * 1000L
+
+ // System page types
+ enum class SystemPageType {
+ SETTINGS, // App settings page
+ Notification_PERMISSION, // Permission settings page
+ STORAGE_ACCESS, // Storage access permission page
+ UNKNOWN // Unknown page
+ }
+
+ /**
+ * Mark entering system page
+ */
+ fun markEnterSystemPage(type: SystemPageType = SystemPageType.UNKNOWN) {
+ isInSystemPage.set(true)
+ enterSystemPageTime.set(System.currentTimeMillis())
+ Log.d(TAG, "Marked entering system page: $type")
+ }
+
+ /**
+ * Mark leaving system page
+ */
+ fun markLeaveSystemPage() {
+ isInSystemPage.set(false)
+ enterSystemPageTime.set(0)
+ Log.d(TAG, "Marked leaving system page")
+ }
+
+ /**
+ * Check if currently in system page
+ */
+ fun isInSystemPage(): Boolean {
+ return isInSystemPage.get()
+ }
+
+ /**
+ * Check if splash screen should be started
+ * If just returned from system page, don't start
+ */
+ fun shouldStartSplashActivity(): Boolean {
+ val inSystemPage = isInSystemPage.get()
+ if (!inSystemPage) {
+ return true
+ }
+
+ // Check if timeout
+ val currentTime = System.currentTimeMillis()
+ val enterTime = enterSystemPageTime.get()
+ if (enterTime > 0 && (currentTime - enterTime) > SYSTEM_PAGE_TIMEOUT) {
+ Log.w(TAG, "System page timeout, resetting state")
+ markLeaveSystemPage()
+ return true
+ }
+
+ Log.d(TAG, "Just returned from system page, not starting splash screen")
+ return false
+ }
+
+ /**
+ * Mark entering storage access permission page (Android 11+)
+ * Used for permission requests in StoragePermissionExt
+ */
+ fun markEnterStorageAccessPage() {
+ markEnterSystemPage(SystemPageType.STORAGE_ACCESS)
+ }
+
+ fun markEnterNotificationAccessPage() {
+ markEnterSystemPage(SystemPageType.Notification_PERMISSION)
+ }
+
+ /**
+ * Check if splash screen should be started (AppLifecycleObserver specific)
+ * If just returned from system page, don't start
+ */
+ fun shouldStartSplashForAppLifecycle(): Boolean {
+ val inSystemPage = isInSystemPage.get()
+ if (!inSystemPage) {
+ return true
+ }
+
+ // Check if timeout
+ val currentTime = System.currentTimeMillis()
+ val enterTime = enterSystemPageTime.get()
+ if (enterTime > 0 && (currentTime - enterTime) > SYSTEM_PAGE_TIMEOUT) {
+ Log.w(TAG, "System page timeout, resetting state")
+ markLeaveSystemPage()
+ return true
+ }
+
+ Log.d(TAG, "Just returned from system page, not starting splash screen")
+ return false
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/remax/base/controller/UserChannelController.kt b/base/src/main/java/com/remax/base/controller/UserChannelController.kt
new file mode 100644
index 0000000..2f11597
--- /dev/null
+++ b/base/src/main/java/com/remax/base/controller/UserChannelController.kt
@@ -0,0 +1,217 @@
+package com.remax.base.controller
+
+import com.remax.base.BuildConfig
+import com.remax.base.ext.KvBoolDelegate
+import com.remax.base.ext.KvStringDelegate
+import com.remax.base.log.BaseLogger
+
+/**
+ * 用户渠道控制器
+ * 统一管理用户渠道类型,提供渠道设置和监听功能
+ */
+object UserChannelController {
+
+ private const val TAG = "UserChannelController"
+ private const val KEY_USER_CHANNEL = "user_channel_type"
+ private const val KEY_CHANNEL_SET_ONCE = "user_channel_set_once"
+
+ /**
+ * 用户渠道类型枚举
+ */
+ enum class UserChannelType(val value: String) {
+ NATURAL("natural"), // 自然渠道
+ PAID("paid") // 买量渠道
+ }
+
+ /**
+ * 渠道变化监听接口
+ */
+ interface ChannelChangeListener {
+ /**
+ * 渠道类型变化回调
+ * @param oldChannel 旧渠道类型
+ * @param newChannel 新渠道类型
+ */
+ fun onChannelChanged(oldChannel: UserChannelType, newChannel: UserChannelType)
+ }
+
+ // 使用KvStringDelegate进行持久化存储
+ private var channelTypeString by KvStringDelegate(KEY_USER_CHANNEL, getDefaultChannelValue())
+ private var channelSetOnce by KvBoolDelegate(KEY_CHANNEL_SET_ONCE, false)
+
+ // 监听器列表
+ private val listeners = mutableListOf()
+
+ /**
+ * 获取默认渠道值
+ * 优先使用BuildConfig中的配置,如果没有则使用NATURAL作为默认值
+ * @return 默认渠道值
+ */
+ private fun getDefaultChannelValue(): String {
+ return try {
+ // 尝试从BuildConfig获取默认渠道配置
+ val defaultChannel = BuildConfig.DEFAULT_USER_CHANNEL
+
+ // 验证配置值是否有效
+ if (UserChannelType.values().any { it.value == defaultChannel }) {
+ BaseLogger.d("使用BuildConfig默认渠道: %s", defaultChannel)
+ defaultChannel
+ } else {
+ BaseLogger.w("BuildConfig默认渠道无效: %s,使用NATURAL", defaultChannel)
+ UserChannelType.NATURAL.value
+ }
+ } catch (e: Exception) {
+ // 如果无法获取BuildConfig或字段不存在,使用默认值
+ BaseLogger.e("获取BuildConfig默认渠道失败,使用NATURAL", e)
+ UserChannelType.NATURAL.value
+ }
+ }
+
+ /**
+ * 获取当前用户渠道类型
+ * @return 当前渠道类型,默认为自然渠道
+ */
+ fun getCurrentChannel(): UserChannelType {
+ return try {
+ val currentChannelString = channelTypeString
+ if (currentChannelString.isNullOrEmpty()) {
+ BaseLogger.w("渠道字符串为空,使用默认NATURAL")
+ return UserChannelType.NATURAL
+ }
+
+ UserChannelType.values().find { it.value == currentChannelString }
+ ?: run {
+ BaseLogger.w("无效的渠道字符串: %s,使用默认NATURAL", currentChannelString)
+ UserChannelType.NATURAL
+ }
+ } catch (e: Exception) {
+ BaseLogger.e("获取当前渠道失败,使用默认NATURAL", e)
+ UserChannelType.NATURAL
+ }
+ }
+
+ /**
+ * 设置用户渠道类型
+ * @param channelType 新的渠道类型
+ * @return 是否成功设置(如果已经设置过则返回false)
+ */
+ fun setChannel(channelType: UserChannelType): Boolean {
+ // 如果已经设置过,则不再允许修改
+ if (channelSetOnce) {
+ BaseLogger.w("用户渠道已设置过,无法修改: %s", getCurrentChannel().value)
+ return false
+ }
+
+ val oldChannel = getCurrentChannel()
+ if (oldChannel != channelType) {
+ channelTypeString = channelType.value
+ channelSetOnce = true // 标记为已设置
+
+ BaseLogger.d("用户渠道设置成功: %s -> %s", oldChannel.value, channelType.value)
+
+ // 通知所有监听器
+ notifyChannelChanged(oldChannel, channelType)
+ } else {
+ BaseLogger.d("用户渠道未变化,保持: %s", channelType.value)
+ }
+ return true
+ }
+
+ /**
+ * 添加渠道变化监听器
+ * @param listener 监听器
+ */
+ fun addChannelChangeListener(listener: ChannelChangeListener) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener)
+ }
+ }
+
+ /**
+ * 移除渠道变化监听器
+ * @param listener 监听器
+ */
+ fun removeChannelChangeListener(listener: ChannelChangeListener) {
+ listeners.remove(listener)
+ }
+
+ /**
+ * 清除所有监听器
+ */
+ fun clearListeners() {
+ listeners.clear()
+ }
+
+ /**
+ * 通知所有监听器渠道变化
+ * @param oldChannel 旧渠道类型
+ * @param newChannel 新渠道类型
+ */
+ private fun notifyChannelChanged(oldChannel: UserChannelType, newChannel: UserChannelType) {
+ BaseLogger.d("通知渠道变化监听器,监听器数量: %d", listeners.size)
+ listeners.forEach { listener ->
+ try {
+ listener.onChannelChanged(oldChannel, newChannel)
+ } catch (e: Exception) {
+ // 忽略监听器异常,避免影响其他监听器
+ BaseLogger.e("渠道变化监听器异常", e)
+ }
+ }
+ }
+
+ /**
+ * 检查是否为自然渠道
+ * @return 是否为自然渠道
+ */
+ fun isNaturalChannel(): Boolean {
+ return getCurrentChannel() == UserChannelType.NATURAL
+ }
+
+ /**
+ * 检查是否为买量渠道
+ * @return 是否为买量渠道
+ */
+ fun isPaidChannel(): Boolean {
+ return getCurrentChannel() == UserChannelType.PAID
+ }
+
+ /**
+ * 检查是否已经设置过渠道
+ * @return 是否已经设置过
+ */
+ fun isChannelSetOnce(): Boolean {
+ return channelSetOnce
+ }
+
+ /**
+ * 重置渠道设置状态(仅用于测试或特殊情况)
+ * 注意:此方法会清除已设置标记,允许重新设置渠道
+ */
+ fun resetChannelSetting() {
+ channelSetOnce = false
+ }
+
+ /**
+ * 强制设置渠道类型(忽略已设置标记)
+ * 注意:此方法仅用于特殊情况,如测试或数据迁移
+ * @param channelType 新的渠道类型
+ */
+ fun forceSetChannel(channelType: UserChannelType) {
+ val oldChannel = getCurrentChannel()
+ channelTypeString = channelType.value
+ channelSetOnce = true
+
+ BaseLogger.d("强制设置用户渠道: %s -> %s", oldChannel.value, channelType.value)
+
+ // 通知所有监听器
+ notifyChannelChanged(oldChannel, channelType)
+ }
+
+ /**
+ * 获取渠道类型字符串(用于日志等)
+ * @return 渠道类型字符串
+ */
+ fun getChannelString(): String {
+ return getCurrentChannel().value
+ }
+}
diff --git a/base/src/main/java/com/remax/base/ext/KvDelegate.kt b/base/src/main/java/com/remax/base/ext/KvDelegate.kt
new file mode 100644
index 0000000..5aabd53
--- /dev/null
+++ b/base/src/main/java/com/remax/base/ext/KvDelegate.kt
@@ -0,0 +1,83 @@
+package com.remax.base.ext
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.remax.base.utils.ContextProvider
+
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+
+val kv: SharedPreferences by lazy {
+ ContextProvider.getAppContext().getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
+}
+
+val kvEditor: SharedPreferences.Editor by lazy {
+ kv.edit()
+}
+
+class KvStringDelegate(private val key: String, private val def: String? = null) :
+ ReadWriteProperty {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): String? {
+ return kv.getString(key, def)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
+ kvEditor.putString(key, value).apply()
+ }
+}
+
+class KvLongDelegate(private val key: String, private val def: Long = 0L) :
+ ReadWriteProperty {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Long {
+ return kv.getLong(key, def)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Long) {
+ kvEditor.putLong(key, value).apply()
+ }
+}
+
+class KvBoolDelegate(private val key: String, private val def: Boolean = false) :
+ ReadWriteProperty {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean {
+ return kv.getBoolean(key, def)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
+ kvEditor.putBoolean(key, value).apply()
+ }
+}
+
+class KvFloatDelegate(private val key: String, private val def: Float = 0f) :
+ ReadWriteProperty {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Float {
+ return kv.getFloat(key, def)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Float) {
+ kvEditor.putFloat(key, value).apply()
+ }
+}
+
+
+class KvIntDelegate(private val key: String, private val def: Int = 0) :
+ ReadWriteProperty {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
+ return kv.getInt(key, def)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
+ kvEditor.putInt(key, value).apply()
+ }
+}
+
+class KvStringSetDelegate(private val key: String, private val def: Set = setOf()) :
+ ReadWriteProperty?> {
+ override fun getValue(thisRef: Any?, property: KProperty<*>): Set? {
+ return kv.getStringSet(key, def)
+ }
+
+ override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set?) {
+ kvEditor.putStringSet(key, value).apply()
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/remax/base/ext/NotificationPermissionExt.kt b/base/src/main/java/com/remax/base/ext/NotificationPermissionExt.kt
new file mode 100644
index 0000000..8305860
--- /dev/null
+++ b/base/src/main/java/com/remax/base/ext/NotificationPermissionExt.kt
@@ -0,0 +1,84 @@
+package com.remax.base.ext
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationManagerCompat
+import com.remax.base.utils.ActivityLauncher
+
+/**
+ * 检查是否可以发送通知(权限 + 系统设置)
+ */
+fun Context.canSendNotification(): Boolean {
+ // 检查权限
+ val hasPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
+ } else {
+ true // Android 13 以下默认有权限
+ }
+
+ // 检查系统设置
+ val isEnabled = try {
+ NotificationManagerCompat.from(this).areNotificationsEnabled()
+ } catch (e: Exception) {
+ true // 异常时默认返回 true
+ }
+
+ return hasPermission && isEnabled
+}
+
+/**
+ * 请求通知权限
+ * @param launcher ActivityLauncher 实例
+ * @param result 权限结果回调
+ */
+fun Context. requestNotificationPermission(
+ launcher: ActivityLauncher,
+ result: (flag: Boolean) -> Unit
+) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ // Android 13+ 需要请求通知权限
+ launcher.launch(arrayOf(Manifest.permission.POST_NOTIFICATIONS)) { permissions ->
+ val isGranted = permissions[Manifest.permission.POST_NOTIFICATIONS] ?: false
+ result(isGranted)
+ }
+ } else {
+ // Android 13 以下版本默认有权限,但需要检查系统设置
+ result(canSendNotification())
+ }
+}
+
+/**
+ * 跳转到系统通知设置页面
+ * @param launcher ActivityLauncher 实例
+ * @param result 设置结果回调
+ */
+fun Context.openNotificationSettings(
+ launcher: ActivityLauncher,
+ result: (flag: Boolean) -> Unit
+) {
+ val intent = Intent().apply {
+ when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> {
+ // Android 8.0+ 使用应用通知设置
+ action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
+ putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
+ }
+ else -> {
+ // Android 8.0 以下使用通用通知设置
+ action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+ data = Uri.fromParts("package", packageName, null)
+ }
+ }
+ }
+
+ launcher.launch(intent) { activityResult ->
+ // 检查设置后的状态
+ result(canSendNotification())
+ }
+}
diff --git a/base/src/main/java/com/remax/base/ext/StoragePermissionExt.kt b/base/src/main/java/com/remax/base/ext/StoragePermissionExt.kt
new file mode 100644
index 0000000..8e3a7ec
--- /dev/null
+++ b/base/src/main/java/com/remax/base/ext/StoragePermissionExt.kt
@@ -0,0 +1,51 @@
+package com.remax.base.ext
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.Settings
+import androidx.core.app.ActivityCompat
+import com.remax.base.utils.ActivityLauncher
+import com.remax.base.controller.SystemPageNavigationController
+
+fun Context.checkStorePermission(): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Environment.isExternalStorageManager()
+ } else {
+ val readPermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
+ val writePermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ return readPermission == PackageManager.PERMISSION_GRANTED && writePermission == PackageManager.PERMISSION_GRANTED;
+ }
+}
+
+fun Context.requestStorePermission(
+ launcher: ActivityLauncher,
+ jumpAction: (() -> Unit)? = null,
+ result: (flag: Boolean) -> Unit
+) {
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ // 标记进入系统页面
+ SystemPageNavigationController.markEnterStorageAccessPage()
+
+ val intent =
+ Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
+ intent.addCategory("android.intent.category.DEFAULT")
+ intent.data = Uri.parse("package:${packageName}")
+ jumpAction?.invoke()
+ launcher.launch(intent) {
+ // 权限检查完成后,标记离开系统页面
+ SystemPageNavigationController.markLeaveSystemPage()
+ result.invoke(checkStorePermission())
+ }
+ } else {
+ launcher.launch(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { map ->
+ result(map.values.all { it })
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/remax/base/ext/ViewBindUtil.kt b/base/src/main/java/com/remax/base/ext/ViewBindUtil.kt
new file mode 100644
index 0000000..ce3f274
--- /dev/null
+++ b/base/src/main/java/com/remax/base/ext/ViewBindUtil.kt
@@ -0,0 +1,110 @@
+package com.example.base.extention
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.databinding.ViewDataBinding
+import androidx.fragment.app.Fragment
+import androidx.viewbinding.ViewBinding
+import java.lang.reflect.InvocationTargetException
+import java.lang.reflect.ParameterizedType
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+
+@JvmName("inflateWithGeneric")
+fun AppCompatActivity.inflateBindingWithGeneric(layoutInflater: LayoutInflater): VB =
+ withGenericBindingClass(this) { clazz ->
+ clazz.getMethod("inflate", LayoutInflater::class.java).invoke(null, layoutInflater) as VB
+ }.also { binding ->
+ if (binding is ViewDataBinding) {
+ binding.lifecycleOwner = this
+ }
+ }
+
+@JvmName("inflateWithGeneric")
+fun Fragment.inflateBindingWithGeneric(
+ layoutInflater: LayoutInflater,
+ parent: ViewGroup?,
+ attachToParent: Boolean
+): VB {
+ return inflateBindingWithGeneric(null, layoutInflater, parent, attachToParent)
+}
+
+fun Fragment.inflateBindingWithGeneric(
+ targetClass: Class? = null,
+ layoutInflater: LayoutInflater,
+ parent: ViewGroup?,
+ attachToParent: Boolean
+): VB =
+ withGenericBindingClass(this, targetClass) { clazz ->
+ clazz.getMethod(
+ "inflate",
+ LayoutInflater::class.java,
+ ViewGroup::class.java,
+ Boolean::class.java
+ )
+ .invoke(null, layoutInflater, parent, attachToParent) as VB
+ }.also { binding ->
+ if (binding is ViewDataBinding) {
+ binding.lifecycleOwner = viewLifecycleOwner
+ }
+ }
+
+private fun withGenericBindingClass(
+ any: Any,
+ targetClass: Class? = null,
+ block: (Class) -> VB
+): VB {
+ var genericSuperclass = any::class.java.genericSuperclass
+ var superclass = any::class.java.superclass
+ while (superclass != null) {
+ if (genericSuperclass is ParameterizedType && (targetClass == null || targetClass == superclass)) {
+ try {
+ val type = if (genericSuperclass.actualTypeArguments.size > 1) {
+ genericSuperclass.actualTypeArguments[1]
+ } else {
+ genericSuperclass.actualTypeArguments[0]
+ }
+ return block.invoke(type as Class)
+ } catch (e: NoSuchMethodException) {
+ } catch (e: ClassCastException) {
+ } catch (e: InvocationTargetException) {
+ throw e.targetException
+ }
+ }
+ genericSuperclass = superclass.genericSuperclass
+ superclass = superclass.superclass
+ }
+ throw IllegalArgumentException("There is no generic of ViewBinding.")
+}
+
+inline fun ViewGroup.viewBinding() =
+ ViewBindingDelegate(T::class.java, this)
+
+class ViewBindingDelegate(
+ private val bindingClass: Class,
+ val view: ViewGroup
+) : ReadOnlyProperty {
+ private var binding: T? = null
+
+ override fun getValue(thisRef: ViewGroup, property: KProperty<*>): T {
+ binding?.let { return it }
+
+ @Suppress("UNCHECKED_CAST")
+ binding = try {
+ val inflateMethod =
+ bindingClass.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java)
+ inflateMethod.invoke(null, LayoutInflater.from(thisRef.context), thisRef) as T
+ } catch (e: NoSuchMethodException) {
+ val inflateMethod = bindingClass.getMethod(
+ "inflate",
+ LayoutInflater::class.java,
+ ViewGroup::class.java,
+ Boolean::class.java
+ )
+ inflateMethod.invoke(null, LayoutInflater.from(thisRef.context), thisRef, true) as T
+ }
+
+ return binding ?: throw IllegalStateException("Binding should have been initialized")
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/remax/base/ext/WindowInsetsExt.kt b/base/src/main/java/com/remax/base/ext/WindowInsetsExt.kt
new file mode 100644
index 0000000..a5e3bf6
--- /dev/null
+++ b/base/src/main/java/com/remax/base/ext/WindowInsetsExt.kt
@@ -0,0 +1,214 @@
+package com.example.base.extention
+
+import android.app.Activity
+import android.graphics.Color
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.ComponentActivity
+import androidx.activity.enableEdgeToEdge
+import androidx.core.graphics.Insets
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
+import androidx.annotation.ColorInt
+
+/**
+ * WindowInsets相关扩展函数
+ * 用于处理Android 15边到边显示的适配
+ */
+
+/**
+ * WindowInsets缓存单例
+ * 避免重复设置监听器,提高性能
+ */
+object WindowInsetsCache {
+ private var cachedSystemBars: Insets? = null
+ private var isInitialized = false
+ private val pendingActions = mutableListOf<(Insets) -> Unit>()
+
+ fun getSystemBars(): Insets? = cachedSystemBars
+
+ fun initIfNeeded(view: View, onReady: ((Insets) -> Unit)? = null) {
+ if (!isInitialized) {
+ // 找到根View来设置监听器
+ val rootView = view.rootView
+ ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, windowInsets ->
+ cachedSystemBars = windowInsets.getInsets(
+ WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
+ )
+ isInitialized = true
+
+ // 执行所有等待的操作
+ executePendingActions()
+
+ windowInsets
+ }
+ ViewCompat.requestApplyInsets(rootView)
+ }
+
+ // 处理回调
+ if (cachedSystemBars != null && onReady != null) {
+ // 已经有缓存值,立即执行
+ onReady(cachedSystemBars!!)
+ } else if (onReady != null) {
+ // 还没有缓存值,加入等待列表
+ pendingActions.add(onReady)
+ }
+ }
+
+ private fun executePendingActions() {
+ cachedSystemBars?.let { systemBars ->
+ pendingActions.forEach { action ->
+ action(systemBars)
+ }
+ pendingActions.clear()
+ }
+ }
+
+ /**
+ * 重置缓存(主要用于测试或特殊情况)
+ */
+ fun reset() {
+ cachedSystemBars = null
+ isInitialized = false
+ pendingActions.clear()
+ }
+}
+
+/**
+ * 为View设置顶部边距以适配状态栏
+ * 直接使用缓存版本实现
+ */
+fun View.appendStatusBarMarginTop() {
+ WindowInsetsCache.initIfNeeded(this) { systemBars ->
+ updateLayoutParams {
+ topMargin += systemBars.top
+ }
+ }
+}
+
+/**
+ * 为View添加底部边距,避免被导航栏遮挡
+ * 用于确保内容不延伸到导航栏下方
+ */
+fun View.appendNavigationBarMarginBottom() {
+ WindowInsetsCache.initIfNeeded(this) { systemBars ->
+ updateLayoutParams {
+ bottomMargin += systemBars.bottom
+ }
+ }
+}
+
+/**
+ * 为View设置顶部Padding以适配状态栏
+ */
+fun View.appendStatusBarPaddingTop() {
+ WindowInsetsCache.initIfNeeded(this) { systemBars ->
+ setPadding(paddingLeft, paddingTop + systemBars.top, paddingRight, paddingBottom)
+ }
+}
+
+/**
+ * 为View设置底部Padding以适配导航栏
+ * 用于确保内容不被导航栏遮挡
+ */
+fun View.appendNavigationBarPaddingBottom() {
+ WindowInsetsCache.initIfNeeded(this) { systemBars ->
+ setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom + systemBars.bottom)
+ }
+}
+
+fun View.appendNavigationBarPaddingVertical() {
+ WindowInsetsCache.initIfNeeded(this) { systemBars ->
+ setPadding(
+ paddingLeft,
+ paddingTop + systemBars.top,
+ paddingRight,
+ paddingBottom + systemBars.bottom
+ )
+ }
+}
+
+/**
+ * 设置状态栏外观
+ * @param isDarkFont true=暗色字体(亮色背景), false=亮色字体(暗色背景)
+ */
+fun Activity.setStatusBarAppearance(isDarkFont: Boolean) {
+ val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
+ windowInsetsController.isAppearanceLightStatusBars = isDarkFont
+}
+
+/**
+ * 现代方式:使用WindowInsetsController设置导航栏样式
+ * 推荐使用此方法替代直接设置颜色的过时API
+ * @param isLightBackground true=亮色背景(深色按钮), false=暗色背景(白色按钮)
+ * @param enforceContrast 是否强制系统对比度
+ */
+fun Activity.setNavigationBarAppearance(
+ isLightBackground: Boolean = false,
+ enforceContrast: Boolean = true
+) {
+ val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
+ windowInsetsController.isAppearanceLightNavigationBars = isLightBackground
+
+ // 设置对比度强制策略
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
+ window.isNavigationBarContrastEnforced = enforceContrast
+ }
+}
+
+/**
+ * 设置导航栏背景颜色 (现代API版本)
+ * 适用于边到边显示模式下的导航栏颜色自定义
+ * @param color 导航栏背景颜色,支持透明度
+ * @param lightButtons true=深色按钮(亮色背景), false=白色按钮(暗色背景)
+ */
+fun Activity.setNavigationBarColor(@ColorInt color: Int) {
+ // 使用现代API设置导航栏颜色
+ window.apply {
+ // 确保设置了正确的标志
+ addFlags(android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+
+ // 设置颜色(尽管已废弃,但仍需要用于兼容性)
+ @Suppress("DEPRECATION")
+ navigationBarColor = color
+ }
+}
+
+/**
+ * 设置状态栏背景颜色 (现代API版本)
+ * 适用于边到边显示模式下的状态栏颜色自定义
+ * @param color 状态栏背景颜色,支持透明度
+ */
+fun Activity.setStatusBarColor(@ColorInt color: Int) {
+ // 使用现代API设置状态栏颜色
+ window.apply {
+ // 确保设置了正确的标志
+ addFlags(android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+
+ // 设置颜色(尽管已废弃,但仍需要用于兼容性)
+ @Suppress("DEPRECATION")
+ statusBarColor = color
+ }
+}
+
+/**
+ * 启用边到边显示并设置默认样式
+ * 这是推荐的现代方式,替代直接设置导航栏颜色的过时API
+ */
+fun ComponentActivity.edgeToEdge() {
+ enableEdgeToEdge()// 状态栏、导航栏显示内容,并且透明
+ setStatusBarAppearance(isDarkFont = true)
+ setNavigationBarColor(Color.WHITE)
+ setNavigationBarAppearance(isLightBackground = true, enforceContrast = true)
+}
+
+/**
+ * 让 View 忽略系统 WindowInsets(不自动适配系统栏的 padding)
+ */
+fun View.ignoreWindowInsets() {
+ ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
+ insets
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/remax/base/log/BaseLogger.kt b/base/src/main/java/com/remax/base/log/BaseLogger.kt
new file mode 100644
index 0000000..e4c59b2
--- /dev/null
+++ b/base/src/main/java/com/remax/base/log/BaseLogger.kt
@@ -0,0 +1,159 @@
+package com.remax.base.log
+
+import android.util.Log
+import com.remax.base.BuildConfig
+
+/**
+ * Base模块日志工具类
+ * 提供统一的日志输出控制和管理
+ */
+object BaseLogger {
+ private const val TAG = "BaseModule"
+
+ /**
+ * 日志开关,默认为true
+ */
+ private var isLogEnabled = BuildConfig.DEBUG
+
+ /**
+ * 设置日志开关
+ * @param enabled 是否启用日志
+ */
+ fun setLogEnabled(enabled: Boolean) {
+ isLogEnabled = enabled
+ }
+
+ /**
+ * 获取日志开关状态
+ * @return 是否启用日志
+ */
+ fun isLogEnabled(): Boolean = isLogEnabled
+
+ /**
+ * Debug日志
+ * @param message 日志消息
+ */
+ fun d(message: String) {
+ if (isLogEnabled) {
+ Log.d(TAG, message)
+ }
+ }
+
+ /**
+ * Debug日志(带参数)
+ * @param message 日志消息模板
+ * @param args 参数列表
+ */
+ fun d(message: String, vararg args: Any?) {
+ if (isLogEnabled) {
+ Log.d(TAG, message.format(*args))
+ }
+ }
+
+ /**
+ * Warning日志
+ * @param message 日志消息
+ */
+ fun w(message: String) {
+ if (isLogEnabled) {
+ Log.w(TAG, message)
+ }
+ }
+
+ /**
+ * Warning日志(带参数)
+ * @param message 日志消息模板
+ * @param args 参数列表
+ */
+ fun w(message: String, vararg args: Any?) {
+ if (isLogEnabled) {
+ Log.w(TAG, message.format(*args))
+ }
+ }
+
+ /**
+ * Error日志
+ * @param message 日志消息
+ */
+ fun e(message: String) {
+ if (isLogEnabled) {
+ Log.e(TAG, message)
+ }
+ }
+
+ /**
+ * Error日志(带异常)
+ * @param message 日志消息
+ * @param throwable 异常对象
+ */
+ fun e(message: String, throwable: Throwable?) {
+ if (isLogEnabled) {
+ Log.e(TAG, message, throwable)
+ }
+ }
+
+ /**
+ * Error日志(带参数)
+ * @param message 日志消息模板
+ * @param args 参数列表
+ */
+ fun e(message: String, vararg args: Any?) {
+ if (isLogEnabled) {
+ Log.e(TAG, message.format(*args))
+ }
+ }
+
+ /**
+ * Error日志(带参数和异常)
+ * @param message 日志消息模板
+ * @param throwable 异常对象
+ * @param args 参数列表
+ */
+ fun e(message: String, throwable: Throwable?, vararg args: Any?) {
+ if (isLogEnabled) {
+ Log.e(TAG, message.format(*args), throwable)
+ }
+ }
+
+ /**
+ * Info日志
+ * @param message 日志消息
+ */
+ fun i(message: String) {
+ if (isLogEnabled) {
+ Log.i(TAG, message)
+ }
+ }
+
+ /**
+ * Info日志(带参数)
+ * @param message 日志消息模板
+ * @param args 参数列表
+ */
+ fun i(message: String, vararg args: Any?) {
+ if (isLogEnabled) {
+ Log.i(TAG, message.format(*args))
+ }
+ }
+
+ /**
+ * Verbose日志
+ * @param message 日志消息
+ */
+ fun v(message: String) {
+ if (isLogEnabled) {
+ Log.v(TAG, message)
+ }
+ }
+
+ /**
+ * Verbose日志(带参数)
+ * @param message 日志消息模板
+ * @param args 参数列表
+ */
+ fun v(message: String, vararg args: Any?) {
+ if (isLogEnabled) {
+ Log.v(TAG, message.format(*args))
+ }
+ }
+}
diff --git a/base/src/main/java/com/remax/base/provider/BaseModuleProvider.kt b/base/src/main/java/com/remax/base/provider/BaseModuleProvider.kt
new file mode 100644
index 0000000..bc51385
--- /dev/null
+++ b/base/src/main/java/com/remax/base/provider/BaseModuleProvider.kt
@@ -0,0 +1,59 @@
+package com.remax.base.provider
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.net.Uri
+import com.remax.base.log.BaseLogger
+
+/**
+ * Base模块内容提供者
+ * 用于在模块初始化时获取 Context 并提供给其他模块使用
+ * 优先级最高,确保在其他模块初始化之前就准备好
+ */
+class BaseModuleProvider : ContentProvider() {
+
+ companion object {
+ private var applicationContext: android.content.Context? = null
+
+ /**
+ * 获取应用上下文
+ * 替代 Utils.getApp() 的依赖
+ */
+ fun getApplicationContext(): android.content.Context? = applicationContext
+ }
+
+ override fun onCreate(): Boolean {
+ applicationContext = context?.applicationContext
+ applicationContext?.let { ctx ->
+ try {
+ BaseLogger.d("BaseModuleProvider 初始化完成,Context 已准备就绪")
+ } catch (e: Exception) {
+ BaseLogger.e("BaseModuleProvider 初始化失败", e)
+ }
+ }
+
+ return true
+ }
+
+ override fun query(
+ uri: Uri,
+ projection: Array?,
+ selection: String?,
+ selectionArgs: Array?,
+ sortOrder: String?
+ ): Cursor? = null
+
+ override fun getType(uri: Uri): String? = null
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? = null
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array?
+ ): Int = 0
+}
diff --git a/base/src/main/java/com/remax/base/report/DataReporter.kt b/base/src/main/java/com/remax/base/report/DataReporter.kt
new file mode 100644
index 0000000..1686993
--- /dev/null
+++ b/base/src/main/java/com/remax/base/report/DataReporter.kt
@@ -0,0 +1,134 @@
+package com.remax.base.report
+
+/**
+ * 数据上报接口
+ * 提供统一的数据上报功能
+ */
+interface DataReporter {
+ /**
+ * 获取上报器名称
+ * @return 上报器名称
+ */
+ fun getName(): String
+
+ /**
+ * 上报数据
+ * @param eventName 事件名称
+ * @param data 数据Map,key为参数名,value为参数值
+ */
+ fun reportData(eventName: String, data: Map)
+
+ /**
+ * 设置公共参数
+ * @param params 公共参数Map,key为参数名,value为参数值
+ */
+ fun setCommonParams(params: Map)
+
+ /**
+ * 设置用户参数
+ * @param params 用户参数Map,key为参数名,value为参数值
+ */
+ fun setUserParams(params: Map)
+}
+
+/**
+ * 数据上报管理器
+ * 管理数据上报器的注入和调用
+ */
+object DataReportManager {
+ private var reporters: MutableList = mutableListOf()
+
+ /**
+ * 设置数据上报器集合
+ * @param reporters 数据上报器实现集合
+ */
+ fun setReporters(reporters: Collection) {
+ this.reporters.clear()
+ this.reporters.addAll(reporters)
+ }
+
+ /**
+ * 设置公共参数
+ * @param params 公共参数Map
+ */
+ fun setCommonParams(params: Map) {
+ // 同时设置所有上报器的公共参数
+ reporters.forEach { reporter ->
+ try {
+ reporter.setCommonParams(params)
+ } catch (e: Exception) {
+ // 单个上报器失败不影响其他上报器
+ e.printStackTrace()
+ }
+ }
+ }
+
+ /**
+ * 设置用户参数
+ * @param params 用户参数Map
+ */
+ fun setUserParams(params: Map) {
+ // 同时设置所有上报器的用户参数
+ reporters.forEach { reporter ->
+ try {
+ reporter.setUserParams(params)
+ } catch (e: Exception) {
+ // 单个上报器失败不影响其他上报器
+ e.printStackTrace()
+ }
+ }
+ }
+
+ /**
+ * 上报数据(自动合并公共参数和用户参数)
+ * @param eventName 事件名称
+ * @param data 数据Map
+ */
+ fun reportData(eventName: String, data: Map) {
+ try {
+ // 遍历所有上报器进行上报
+ reporters.forEach { reporter ->
+ try {
+ reporter.reportData(eventName, data)
+ } catch (e: Exception) {
+ // 单个上报器失败不影响其他上报器
+ e.printStackTrace()
+ }
+ }
+ } catch (e: Exception) {
+ // 静默处理异常,避免影响主流程
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * 按名称上报数据到指定上报器
+ * @param reporterName 上报器名称
+ * @param eventName 事件名称
+ * @param data 数据Map
+ */
+ fun reportDataByName(reporterName: String, eventName: String, data: Map) {
+ try {
+ // 查找指定名称的上报器
+ val targetReporter = reporters.find { it.getName() == reporterName }
+ if (targetReporter != null) {
+ try {
+ targetReporter.reportData(eventName, data)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ } catch (e: Exception) {
+ // 静默处理异常,避免影响主流程
+ e.printStackTrace()
+ }
+ }
+
+ /**
+ * 检查是否已初始化
+ * @return 是否已设置上报器
+ */
+ fun isInitialized(): Boolean {
+ return reporters.isNotEmpty()
+ }
+}
diff --git a/base/src/main/java/com/remax/base/temlate/BaseAct.kt b/base/src/main/java/com/remax/base/temlate/BaseAct.kt
new file mode 100644
index 0000000..d756c29
--- /dev/null
+++ b/base/src/main/java/com/remax/base/temlate/BaseAct.kt
@@ -0,0 +1,136 @@
+package com.remax.base.temlate
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Lifecycle
+import com.blankj.utilcode.util.LanguageUtils
+import com.example.base.extention.appendNavigationBarPaddingBottom
+import com.example.base.extention.edgeToEdge
+import com.remax.base.report.DataReportManager
+
+abstract class BaseAct : AppCompatActivity() {
+
+
+ abstract fun xGetIdXml(): Int
+
+ abstract fun xLoadViewFromXml(savedInstanceState: Bundle?)
+
+ open fun prepareData() {
+
+ }
+
+ open var isSaveInstance = true
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ isSaveInstance = true
+ }
+
+ override fun onResume() {
+ isSaveInstance = false
+ super.onResume()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ isSaveInstance = false
+
+ super.onCreate(savedInstanceState)
+
+ edgeToEdge()
+
+ supportActionBar?.hide()
+
+ prepareBindView()?.let {
+ setContentView(it)
+ } ?: run {
+ setContentView(xGetIdXml())
+ }
+ runTemp(savedInstanceState)
+ }
+
+
+ private fun runTemp(savedInstanceState: Bundle?) {
+
+ //函数抽取
+ xLoadViewFromXml(savedInstanceState)
+ prepareData()
+
+ }
+
+ override fun onStart() {
+ super.onStart()
+ DataReportManager.reportDataByName("ThinkingData","pageView",mapOf(
+ "pageName" to this::class.java.simpleName,
+ "pageEvent" to "onStart"))
+ }
+
+ override fun onStop() {
+ super.onStop()
+ DataReportManager.reportDataByName("ThinkingData","pageView",mapOf(
+ "pageName" to this::class.java.simpleName,
+ "pageEvent" to "onStop"))
+ }
+
+
+ open fun prepareBindView(): View? {
+ return null
+ }
+
+ fun replaceFragment(layoutId: Int, fragment: Fragment?) {
+ if (fragment == null) return
+ val fragmentManager = supportFragmentManager
+ val transaction = fragmentManager.beginTransaction()
+ transaction.replace(layoutId, fragment)
+ transaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
+ .commitAllowingStateLoss()
+ }
+
+ open fun showFragment(layoutId: Int, fragment: Fragment?) {
+ if (fragment == null) return
+ val fragmentManager = supportFragmentManager
+ val transaction = fragmentManager.beginTransaction()
+ if (!fragment.isAdded) {
+ transaction.add(layoutId, fragment)
+ } else {
+ transaction.show(fragment)
+ }
+ transaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
+ .commitAllowingStateLoss()
+ }
+
+ open fun showFragment(fragment: Fragment?) {
+ if (fragment == null) return
+ val fragmentManager = supportFragmentManager
+ val transaction = fragmentManager.beginTransaction()
+ transaction.show(fragment)
+ .setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
+ .commitAllowingStateLoss()
+ }
+
+ open fun hideFragment(fragment: Fragment?) {
+ if (fragment == null) return
+ val fragmentManager = supportFragmentManager
+ val transaction = fragmentManager.beginTransaction()
+ transaction.hide(fragment)
+ .setMaxLifecycle(fragment, Lifecycle.State.STARTED)
+ .commitAllowingStateLoss()
+ }
+
+
+ open fun removeFragment(fragment: Fragment?) {
+ if (fragment == null || !fragment.isAdded) return
+ val fragmentManager = supportFragmentManager
+ val transaction = fragmentManager.beginTransaction()
+ transaction.remove(fragment).commitAllowingStateLoss()
+ }
+
+ override fun attachBaseContext(newBase: Context?) {
+ super.attachBaseContext(LanguageUtils.attachBaseContext(newBase))
+
+ }
+
+
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/remax/base/temlate/BaseBindAct.kt b/base/src/main/java/com/remax/base/temlate/BaseBindAct.kt
new file mode 100644
index 0000000..65110fb
--- /dev/null
+++ b/base/src/main/java/com/remax/base/temlate/BaseBindAct.kt
@@ -0,0 +1,17 @@
+package com.remax.base.temlate
+
+import android.view.View
+import androidx.viewbinding.ViewBinding
+import com.example.base.extention.inflateBindingWithGeneric
+
+abstract class BaseBindAct : BaseAct() {
+
+ override fun xGetIdXml(): Int = 0
+
+ lateinit var selfBindView: VB
+
+ override fun prepareBindView(): View? {
+ selfBindView = inflateBindingWithGeneric(layoutInflater)
+ return selfBindView.root
+ }
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/remax/base/temlate/BaseBindFrag.kt b/base/src/main/java/com/remax/base/temlate/BaseBindFrag.kt
new file mode 100644
index 0000000..27e3881
--- /dev/null
+++ b/base/src/main/java/com/remax/base/temlate/BaseBindFrag.kt
@@ -0,0 +1,37 @@
+package com.remax.base.temlate
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.viewbinding.ViewBinding
+import com.example.base.extention.inflateBindingWithGeneric
+
+abstract class BaseBindFrag : BaseFrag() {
+
+ override fun xmlIdView() = 0
+
+ private var _binding: VB? = null
+ val selfBindView: VB? get() = _binding
+
+ val mViewBindNoNull: VB get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ _binding = inflateBindingWithGeneric(inflater, container, false)
+ return selfBindView?.root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ fun isCreated(): Boolean {
+ return _binding != null
+ }
+
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/remax/base/temlate/BaseFrag.kt b/base/src/main/java/com/remax/base/temlate/BaseFrag.kt
new file mode 100644
index 0000000..e21d6e8
--- /dev/null
+++ b/base/src/main/java/com/remax/base/temlate/BaseFrag.kt
@@ -0,0 +1,50 @@
+package com.remax.base.temlate
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+
+abstract class BaseFrag : Fragment() {
+ abstract fun xmlIdView(): Int
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(xmlIdView(), container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ //函数抽取
+ onReadView(savedInstanceState)
+
+ }
+
+
+ /** 初始化view */
+ abstract fun onReadView(savedInstanceState: Bundle?)
+
+ override fun onResume() {
+ super.onResume()
+ loadDelay()
+ }
+
+ private var inflated = false
+
+ private fun loadDelay() {
+ if (inflated) {
+ return
+ }
+ inflated = true
+ viewOnInitForDelay()
+ }
+
+ open fun viewOnInitForDelay() {
+ }
+
+}
\ No newline at end of file
diff --git a/base/src/main/java/com/remax/base/utils/ActivityLauncher.kt b/base/src/main/java/com/remax/base/utils/ActivityLauncher.kt
new file mode 100644
index 0000000..e2aba29
--- /dev/null
+++ b/base/src/main/java/com/remax/base/utils/ActivityLauncher.kt
@@ -0,0 +1,65 @@
+package com.remax.base.utils
+
+import android.content.Intent
+import android.net.Uri
+import androidx.activity.result.ActivityResult
+import androidx.activity.result.ActivityResultCallback
+import androidx.activity.result.ActivityResultCaller
+import androidx.activity.result.contract.ActivityResultContracts
+
+class ActivityLauncher(activityResultCaller: ActivityResultCaller) {
+
+ //region 权限
+ private var permissionCallback: ActivityResultCallback