Selaa lähdekoodia

feat: 增加Web模块

DoggyZhang 3 kuukautta sitten
vanhempi
sitoutus
10434a4d70
61 muutettua tiedostoa jossa 3192 lisäystä ja 328 poistoa
  1. 6 0
      app/src/main/AndroidManifest.xml
  2. 21 17
      app/src/main/java/com/adealink/weparty/App.kt
  3. 0 1
      app/src/main/java/com/adealink/weparty/commonui/widget/nestedscrolling/RVNestedScrollingLayout.kt
  4. 4 1
      app/src/main/java/com/adealink/weparty/module/webview/Router.kt
  5. 3 1
      app/src/main/java/com/adealink/weparty/storage/Constants.kt
  6. 7 261
      app/src/main/java/com/adealink/weparty/url/UrlConfig.kt
  7. 674 0
      app/src/main/java/com/adealink/weparty/webview/BaseWebView.kt
  8. 79 0
      app/src/main/java/com/adealink/weparty/webview/CommonWebChromeClient.kt
  9. 214 0
      app/src/main/java/com/adealink/weparty/webview/CommonWebViewClient.kt
  10. 34 0
      app/src/main/java/com/adealink/weparty/webview/IWebView.kt
  11. 76 0
      app/src/main/java/com/adealink/weparty/webview/JSBridgeConfig.kt
  12. 13 0
      app/src/main/java/com/adealink/weparty/webview/JSBridgePrefs.kt
  13. 61 0
      app/src/main/java/com/adealink/weparty/webview/WeNextWebView.kt
  14. 56 0
      app/src/main/java/com/adealink/weparty/webview/WebModule.kt
  15. 28 0
      app/src/main/java/com/adealink/weparty/webview/WebResourceConfig.kt
  16. 113 0
      app/src/main/java/com/adealink/weparty/webview/WebViewActivity.kt
  17. 126 0
      app/src/main/java/com/adealink/weparty/webview/WebViewDialogFragment.kt
  18. 320 0
      app/src/main/java/com/adealink/weparty/webview/WebViewFragment.kt
  19. 51 0
      app/src/main/java/com/adealink/weparty/webview/callback/IWebViewCallback.kt
  20. 15 0
      app/src/main/java/com/adealink/weparty/webview/callback/IWebViewFragmentCallback.kt
  21. 48 0
      app/src/main/java/com/adealink/weparty/webview/component/ErrorComp.kt
  22. 4 0
      app/src/main/java/com/adealink/weparty/webview/constant/Constant.kt
  23. 12 0
      app/src/main/java/com/adealink/weparty/webview/constant/Data.kt
  24. 2 0
      app/src/main/java/com/adealink/weparty/webview/constant/Error.kt
  25. 15 0
      app/src/main/java/com/adealink/weparty/webview/constant/Tags.kt
  26. 33 0
      app/src/main/java/com/adealink/weparty/webview/datasource/local/WebLocalService.kt
  27. 15 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/JSBridge.kt
  28. 202 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/JSBridgeImpl.kt
  29. 15 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/callback/JSBridgeCallback.kt
  30. 13 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/config/IJSBridgeConfig.kt
  31. 47 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/data/JSRequest.kt
  32. 28 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/data/JSResponse.kt
  33. 19 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/data/NativeMessage.kt
  34. 13 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/manager/IJSBridgeManager.kt
  35. 40 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/manager/JSBridgeManager.kt
  36. 25 0
      app/src/main/java/com/adealink/weparty/webview/jsbridge/method/JSNativeMethod.kt
  37. 7 0
      app/src/main/java/com/adealink/weparty/webview/jsnativemethod/JsNativeMethodExt.kt
  38. 72 0
      app/src/main/java/com/adealink/weparty/webview/loader/IResourceLoader.kt
  39. 29 0
      app/src/main/java/com/adealink/weparty/webview/loader/IWebResourceConfig.kt
  40. 8 0
      app/src/main/java/com/adealink/weparty/webview/loader/LoaderData.kt
  41. 136 0
      app/src/main/java/com/adealink/weparty/webview/loader/OkHttpResourceLoader.kt
  42. 9 0
      app/src/main/java/com/adealink/weparty/webview/loader/ResourceRequest.kt
  43. 46 0
      app/src/main/java/com/adealink/weparty/webview/loader/WebResourceLoader.kt
  44. 15 0
      app/src/main/java/com/adealink/weparty/webview/payermax/PayerMaxHelper.kt
  45. 34 0
      app/src/main/java/com/adealink/weparty/webview/payermax/SpWebChromeClient.kt
  46. 105 0
      app/src/main/java/com/adealink/weparty/webview/payermax/SpWebViewClient.kt
  47. 55 0
      app/src/main/java/com/adealink/weparty/webview/stat/WebViewStatEvent.kt
  48. 40 0
      app/src/main/java/com/adealink/weparty/webview/util/SSLErrorHandleUtil.kt
  49. 61 0
      app/src/main/java/com/adealink/weparty/webview/util/WebUtil.kt
  50. 25 0
      app/src/main/res/layout/activity_webview.xml
  51. 15 0
      app/src/main/res/layout/dialog_webview.xml
  52. 48 0
      app/src/main/res/layout/fragment_webview.xml
  53. 6 9
      app/src/main/res/values/colors.xml
  54. 1 0
      app/src/main/res/values/strings.xml
  55. 26 21
      module/account/src/main/java/com/adealink/weparty/account/login/LoginDialog.kt
  56. 0 3
      module/image/src/main/java/com/adealink/weparty/image/data/Tags.kt
  57. 0 3
      module/image/src/main/java/com/adealink/weparty/image/preview/VideoPreviewActivity.kt
  58. 0 3
      module/image/src/main/java/com/adealink/weparty/image/preview/VideoPreviewFragment.kt
  59. 0 3
      module/image/src/main/java/com/adealink/weparty/image/viewmodel/VideoViewModel.kt
  60. 22 4
      module/setting/src/main/java/com/adealink/weparty/setting/about/AboutActivity.kt
  61. 0 1
      native/wenext_jni/src/main/cpp/wenext_jni.c

+ 6 - 0
app/src/main/AndroidManifest.xml

@@ -182,6 +182,12 @@
             android:name="com.adealink.weparty.imageselect.takePhoto.CameraActivity"
             android:screenOrientation="portrait" />
 
+        <activity
+            android:name="com.adealink.weparty.webview.WebViewActivity"
+            android:screenOrientation="portrait"
+            android:theme="@style/AppTheme"
+            android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
+
         <!-- Trigger Google Play services to install the backported photo picker module. -->
         <service
             android:name="com.google.android.gms.metadata.ModuleDependencies"

+ 21 - 17
app/src/main/java/com/adealink/weparty/App.kt

@@ -89,6 +89,11 @@ import com.adealink.weparty.security.SecurityConfig
 import com.adealink.weparty.share.ShareConfig
 import com.adealink.weparty.stat.StatConfig
 import com.adealink.weparty.storage.config.StorageConfig
+import com.adealink.weparty.webview.JSBridgeConfig
+import com.adealink.weparty.webview.WebResourceConfig
+import com.adealink.weparty.webview.jsbridge.manager.createJSBridgeManager
+import com.adealink.weparty.webview.jsbridge.manager.initJSBridgeManager
+import com.adealink.weparty.webview.loader.initWebResourceLoader
 import com.facebook.FacebookSdk
 import com.facebook.LoggingBehavior
 import com.google.android.play.core.splitcompat.SplitCompatApplication
@@ -117,7 +122,7 @@ class App : SplitCompatApplication(), ActivityLifecycleCallbacksExt {
     val googleService by lazy { createGoogleService(GoogleServiceConfig()) }
     val deviceIdService by lazy { createDeviceIdService(DeviceIdServiceConfig()) }
 
-    //    val jsBridgeManager by lazy { createJSBridgeManager(JSBridgeConfig()) }
+    val jsBridgeManager by lazy { createJSBridgeManager(JSBridgeConfig()) }
     val mediaService by lazy { createMediaService(MediaConfig) }
     val mediaManager: IMediaManager by lazy { MediaManager() }
     val languageManager by lazy { createLanguageManager(LanguageConfig()) }
@@ -165,9 +170,8 @@ class App : SplitCompatApplication(), ActivityLifecycleCallbacksExt {
             addAppStartTask(InitGoogleService())
             addAppStartTask(InitStorageService())
             addAppStartTask(InitShareManager())
-//            addAppStartTask(InitOssService())
-//            addAppStartTask(InitJSBridgeManager())
-//            addAppStartTask(InitWebResourceLoader())
+            addAppStartTask(InitJSBridgeManager())
+            addAppStartTask(InitWebResourceLoader())
             addAppStartTask(InitDownloadService())
             addAppStartTask(InitEffect())
             addAppStartTask(InitFirebaseAnalytics())
@@ -301,20 +305,20 @@ class App : SplitCompatApplication(), ActivityLifecycleCallbacksExt {
 //        }
 //    }
 
-//    inner class InitJSBridgeManager : SubWaitStartUpTask() {
-//        override fun run() {
-//            initJSBridgeManager { jsBridgeManager }
-//        }
-//    }
+    inner class InitJSBridgeManager : SubWaitStartUpTask() {
+        override fun run() {
+            initJSBridgeManager { jsBridgeManager }
+        }
+    }
 
-//    inner class InitWebResourceLoader : SubWaitStartUpTask() {
-//        override fun run() {
-//            initWebResourceLoader(WebResourceConfig())
-//        }
-//
-//        override val dependsTaskList: List<Class<out StartUpTask>>
-//            get() = listOf(InitNetwork::class.java)
-//    }
+    inner class InitWebResourceLoader : SubWaitStartUpTask() {
+        override fun run() {
+            initWebResourceLoader(WebResourceConfig())
+        }
+
+        override val dependsTaskList: List<Class<out StartUpTask>>
+            get() = listOf(InitNetwork::class.java)
+    }
 
     inner class InitDownloadService : SubWaitStartUpTask() {
         override fun run() {

+ 0 - 1
app/src/main/java/com/adealink/weparty/commonui/widget/nestedscrolling/RVNestedScrollingLayout.kt

@@ -10,7 +10,6 @@ import androidx.core.view.ViewCompat
 import androidx.recyclerview.widget.RecyclerView
 
 /**
- * Created by sunxiaodong on 2022/8/27.
  */
 class RVNestedScrollingLayout @JvmOverloads constructor(
     context: Context,

+ 4 - 1
app/src/main/java/com/adealink/weparty/module/webview/Router.kt

@@ -7,7 +7,10 @@ interface Web {
         companion object {
             const val EXTRA_URL = "extra_url"
             const val EXTRA_LOADING_URL = "extra_loading_url"
-            const val EXTRA_OFFLINE_H5_GAME_INFO= "extra_offline_h5_game_info"
+            const val EXTRA_HEIGHT = "height"
+            const val EXTRA_TRANSPARENT = "extra_transparent"
+            const val EXTRA_TOP_RADIUS = "extra_top_radius"
+            const val EXTRA_DIALOG_TAG = "dialog_tag" //dialog的tag
         }
 
     }

+ 3 - 1
app/src/main/java/com/adealink/weparty/storage/Constants.kt

@@ -16,4 +16,6 @@ const val PREF_PROFILE = "pref_profile"
 const val PREF_SEARCH = "pref_search"
 const val PREF_SETTING = "pref_setting"
 const val PREF_SHARE = "pref_share"
-const val PREF_WALLET = "pref_wallet"
+const val PREF_WALLET = "pref_wallet"
+
+const val PREF_WEB = "pref_web"

+ 7 - 261
app/src/main/java/com/adealink/weparty/url/UrlConfig.kt

@@ -13,296 +13,42 @@ object UrlConfig {
     private val isProdEnv =
         AppBase.isRelease || RELEASE_HOSTS.contains(Uri.parse(DebugPrefs.httpUrl).host)
 
-     val thirdPayUrl = when {
+    val thirdPayUrl = when {
         isProdEnv -> "https://web.wenext.chat/web/yoki-wallet"
         else -> "http://web-test.wenext.chat/web/yoki-wallet"
     }
 
-     val faqGameCoinsUrl = when {
-        isProdEnv -> "https://web.yoki.chat/coin-faq?projectname=yoki-room-game-help"
-        else -> "https://web-test.yoki.chat/coin-faq?projectname=yoki-room-game-help"
-    }
-     val luckyFruit = when {
-        isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-lucky-fruit&aspect_ratio=1.604"
-        else -> "http://web-test.yoki.chat/index/?projectname=yoki-lucky-fruit&aspect_ratio=1.604"
-    }
-
-     val serviceTerms = when {
+    val serviceTerms = when {
         isProdEnv -> "https://web.yoki.chat/service_terms/0?projectname=yoki-terms"
         else -> "http://web-test.yoki.chat/service_terms/0?projectname=yoki-terms"
     }
 
-     val privacyPolicy = when {
+    val privacyPolicy = when {
         isProdEnv -> "https://web.yoki.chat/privacy_policy/0?projectname=yoki-terms"
         else -> "http://web-test.yoki.chat/privacy_policy/0?projectname=yoki-terms"
     }
 
-     val policies = when {
+    val policies = when {
         isProdEnv -> "https://web.yoki.chat/policies/0?projectname=yoki-terms"
         else -> "http://web-test.yoki.chat/policies/0?projectname=yoki-terms"
     }
 
-     val vip = when {
-        isProdEnv -> "https://web.yoki.chat/web/yoki-vip?hideAppBar=true"
-        else -> "http://web-test.yoki.chat/web/yoki-vip?hideAppBar=true"
-    }
-
-     val sellCoins = when {
-        isProdEnv -> "https://web.yoki.chat/coin-agent-center?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
-        else -> "http://web-test.yoki.chat/coin-agent-center?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
-    }
-
-     val superGift = when {
-        isProdEnv -> "https://web.wenext.chat/web/h5-super-gift"
-        else -> "http://web-test.wenext.chat/web/h5-super-gift"
-    }
-
-     val slot = when {
-        isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-jackpot"
-        else -> "http://web-test.yoki.chat/index?projectname=yoki-jackpot"
-    }
-
-     val coupleRules = when {
-        isProdEnv -> "https://web.wenext.chat/web/yoki-couple-privilege"
-        else -> "http://web-test.wenext.chat/web/yoki-couple-privilege"
-    }
-
-     val coupleProtectRules = when {
-        isProdEnv -> "https://web.wenext.chat/web/yoki-couple-rule/protect"
-        else -> "http://web-test.wenext.chat/web/yoki-couple-rule/protect"
-    }
-
-     val familyRule = when {
-        isProdEnv -> "https://web.wenext.chat/web/h5-family-rule"
-        else -> "http://web-test.wenext.chat/web/h5-family-rule"
-    }
-
-     val luckyGiftRule = when {
-        isProdEnv -> "https://web.yoki.chat/lucky-gift-rule?projectname=yoki-common-page&hideAppBar=true"
-        else -> "http://web-test.yoki.chat/lucky-gift-rule?projectname=yoki-common-page&hideAppBar=true"
-    }
-
-     val medalRule = when {
-        isProdEnv -> "https://web.wenext.chat/web/h5-medal-rule"
-        else -> "http://web-test.wenext.chat/web/h5-medal-rule"
-    }
-
-     val treasureGiftRule = when {
-        isProdEnv -> "https://web.wenext.chat/web/h5-treasure-gift-rule"
-        else -> "http://web-test.wenext.chat/web/h5-treasure-gift-rule"
-    }
-
-     val micGrabRule = when {
-        isProdEnv -> "https://web.wenext.chat/web/h5-mic-grab-rule"
-        else -> "http://web-test.wenext.chat/web/h5-mic-grab-rule"
-    }
-
-     val singerTerms = when {
-        isProdEnv -> "https://web.wenext.chat/web/h5-mic-grab-rule/singer_terms"
-        else -> "http://web-test.wenext.chat/web/h5-mic-grab-rule/singer_terms"
-    }
-
-     val teenPatti = when {
-        isProdEnv -> "https://web.wenext.chat/web/game-teen-patti"
-        else -> "http://web-test.wenext.chat/web/game-teen-patti"
-    }
-
-     val greedyPro = when {
-        isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-greedy-pro&aspect_ratio=1.571"
-        else -> "http://web-test.yoki.chat/index/?projectname=yoki-greedy-pro&aspect_ratio=1.571"
-    }
-     val jackpot = when {
-        isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-jackpot&aspect_ratio=1.733"
-        else -> "http://web-test.yoki.chat/index/?projectname=yoki-jackpot&aspect_ratio=1.733"
-    }
-     val adminActivity = when {
-        isProdEnv -> "https://web.wenext.chat/web/h5-activity"
-        else -> "http://web-test.wenext.chat/web/h5-activity"
-    }
-
-     val svip = when {
-        isProdEnv -> "https://web.yoki.chat/web/yoki-vip/index/svip?hideAppBar=true"
-        else -> "http://web-test.yoki.chat/web/yoki-vip/index/svip?hideAppBar=true"
-    }
-
-     val familyTaskList = when {
-        isProdEnv -> "https://web.wenext.chat/web/yoki-family-task"
-        else -> "http://web-test.wenext.chat/web/yoki-family-task"
-    }
-
-     val familyMemberRankList = when {
-        isProdEnv -> "https://web.wenext.chat/web/yoki-family-member"
-        else -> "http://web-test.wenext.chat/web/yoki-family-member"
-    }
-
-     val familyLevel = when {
-        isProdEnv -> "https://web.wenext.chat/web/yoki-family-level"
-        else -> "http://web-test.wenext.chat/web/yoki-family-level"
-    }
-
-     val weddingPrivilege = when {
-        isProdEnv -> "https://web.wenext.chat/web/h5-wedding-popularity?roomId="
-        else -> "http://web-test.wenext.chat/web/h5-wedding-popularity?roomId="
-    }
-
-     val luckyPro = when {
-        isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-lucky-pro&aspect_ratio=1.604"
-        else -> "http://web-test.yoki.chat/index/?projectname=yoki-lucky-pro&aspect_ratio=1.604"
-    }
-     val unoRule = when (AppBase.isProdEnv) {
-        true -> "https://web.wenext.chat/web/h5-uno-rule"
-        else -> "http://web-test.wenext.chat/web/h5-uno-rule"
-    }
-
-     val carromRule = when (AppBase.isProdEnv) {
-        true -> "https://web.wenext.chat/web/h5-carrom-rule"
-        else -> "http://web-test.wenext.chat/web/h5-carrom-rule"
-    }
-
-     val dominoRule = when (AppBase.isProdEnv) {
-        true -> "https://web.wenext.chat/web/h5-domino-rule"
-        else -> "http://web-test.wenext.chat/web/h5-domino-rule"
-    }
-
-     val ludoRules = when {
-        isProdEnv -> "https://web.wenext.chat/web/lama-ludo-rule"
-        else -> "http://web-test.wenext.chat/web/lama-ludo-rule"
-    }
-
-     val thirdPartyRecharge =
+    val thirdPartyRecharge =
         when {
             isProdEnv -> "https://web.yoki.chat/third-party-recharge?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
             else -> "http://web-test.yoki.chat/third-party-recharge?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
         }
 
-     val diamondWithdrawal =
+    val diamondWithdrawal =
         when {
             isProdEnv -> "https://web.yoki.chat/diamond-deal?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
             else -> "http://web-test.yoki.chat/diamond-deal?projectname=yoki-payment&hideAppBar=true&vpResizable=true"
         }
 
-     val prefetchResource =
-        when {
-            isProdEnv -> " https://web.yoki.chat/index?projectname=h5-preload"
-            else -> "http://web-test.yoki.chat/index?projectname=h5-preload"
-        }
-
-     val taskCenter =
-        when {
-            AppBase.isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-task-center&hideAppBar=true"
-            else -> "https://web-test.yoki.chat/index/?projectname=yoki-task-center&hideAppBar=true"
-        }
-
-     val chatAchievement =
-        when {
-            AppBase.isProdEnv -> "https://web-new.yoki.chat/index?projectname=yoki-chat-achievement&hideAppBar=true"
-            else -> "https://web-test-new.yoki.chat/index?projectname=yoki-chat-achievement&hideAppBar=true"
-        }
-
-     val signatureAgreement =
-        when {
-            AppBase.isProdEnv -> "https://web.yoki.chat/signature_agreement/0?projectname=yoki-terms"
-            else -> "https://web-test.yoki.chat/signature_agreement/0?projectname=yoki-terms"
-        }
-
-     val childSafetyPolicy =
+    val childSafetyPolicy =
         when {
             AppBase.isProdEnv -> "https://web.yoki.chat/child_policy/0?projectname=yoki-terms"
             else -> "https://web-test.yoki.chat/child_policy/0?projectname=yoki-terms"
         }
 
-    /**
-     * 拉新活动H5页
-     *
-     * 对应的 tab 参数:
-     * "personnel_rewards", // 人数奖励
-     * "recharge_bonus", // 男性充值
-     * "streamer_rewards", // 女性收钻
-     *
-     * 打开奖励记录弹窗:
-     * showRecords=1
-     */
-     val inviteActivity =
-        when {
-            AppBase.isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-invite&hideAppBar=true"
-            else -> "https://web-test.yoki.chat/index?projectname=yoki-invite&hideAppBar=true"
-        }
-
-    // 外部引导下载的网页 -> 用于分享
-     val webGuideDownload =
-        when {
-            AppBase.isProdEnv -> "https://web.yoki.chat/invite?projectname=yoki-invite"
-            else -> "https://web-test.yoki.chat/invite?projectname=yoki-invite"
-        }
-
-     val anchorCenter =
-        when {
-            AppBase.isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-union-center&hideAppBar=true&vpResizable=true"
-            else -> "http://web-test.yoki.chat/index?projectname=yoki-union-center&hideAppBar=true&vpResizable=true"
-        }
-
-    /**
-     * Family Daily Task
-     */
-     val familyDailyTask =
-        when {
-            AppBase.isProdEnv -> "https://web.yoki.chat/daily-task?projectname=yoki-family&hideAppBar=true"
-            else -> "http://web-test.yoki.chat/daily-task?projectname=yoki-family&hideAppBar=true"
-        }
-
-    /**
-     * Family Daily Task Dialog
-     */
-     val familyDailyTaskDialog = when {
-        AppBase.isProdEnv -> "https://web.yoki.chat/daily-task-fragment?projectname=yoki-family&hideAppBar=true"
-        else -> "http://web-test.yoki.chat/daily-task-fragment?projectname=yoki-family&hideAppBar=true"
-    }
-
-    /**
-     * 家族等级说明
-     */
-     val familyLevelDetail =
-        when {
-            AppBase.isProdEnv -> "https://web.yoki.chat/rule?projectname=yoki-family&hideAppBar=true"
-            else -> "http://web-test.yoki.chat/rule?projectname=yoki-family&hideAppBar=true"
-        }
-
-     val dailyMsgLimitDetail =
-        when {
-            AppBase.isProdEnv -> "https://web.yoki.chat/private-chat-task?projectname=yoki-common-page&hideAppBar=true"
-            else -> "http://web-test.yoki.chat/private-chat-task?projectname=yoki-common-page&hideAppBar=true"
-        }
-
-     val rankBoardRule =
-        when {
-            AppBase.isProdEnv -> "https://web.yoki.chat/rank-board-reward?projectname=yoki-rank-board&hideAppBar=true&tab_pos=%s"
-            else -> "http://web-test.yoki.chat/rank-board-reward?projectname=yoki-rank-board&hideAppBar=true&tab_pos=%s"
-        }
-
-     val greedyPersonal = when {
-        isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-greedy-personal&aspect_ratio=1.607"
-        else -> "http://web-test.yoki.chat/index?projectname=yoki-greedy-personal&aspect_ratio=1.607"
-    }
-
-    val gameAggregation  = when {
-        isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-game-aggregation&configType=%s&isShowCloseBtn=%s"
-        else -> "http://web-test.yoki.chat/index?projectname=yoki-game-aggregation&configType=%s&isShowCloseBtn=%s"
-    }
-
-    val gameCenter = when {
-        isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-game-center&hideAppBar=true"
-        else -> "https://web-test.yoki.chat/index?projectname=yoki-game-center&hideAppBar=true"
-    }
-
-    val weekRecharge = when {
-        isProdEnv -> "https://web.yoki.chat/index/?projectname=yoki-recharge-play-free&lotteryActivityId=%s&progressActivityId=%s"
-        else -> "http://web-test.yoki.chat/index/?projectname=yoki-recharge-play-free&lotteryActivityId=%s&progressActivityId=%s"
-    }
-
-     val piggyBank = when {
-        isProdEnv -> "https://web.yoki.chat/index?projectname=yoki-piggy-bank"
-        else -> "http://web-test.yoki.chat/index?projectname=yoki-piggy-bank"
-    }
-
-
 }

+ 674 - 0
app/src/main/java/com/adealink/weparty/webview/BaseWebView.kt

@@ -0,0 +1,674 @@
+package com.adealink.weparty.webview
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.VelocityTracker
+import android.view.View
+import android.view.ViewConfiguration
+import android.view.ViewParent
+import android.webkit.WebSettings
+import android.webkit.WebView
+import android.widget.OverScroller
+import androidx.core.view.NestedScrollingChild3
+import androidx.core.view.NestedScrollingChildHelper
+import androidx.core.view.ViewCompat
+import com.adealink.frame.base.AppBase
+import com.adealink.frame.log.Log
+import com.adealink.weparty.webview.callback.IWebViewCallback
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW
+import com.adealink.weparty.webview.jsbridge.JSBridge
+import com.adealink.weparty.webview.jsbridge.data.NativeMessage
+import com.adealink.weparty.webview.jsbridge.method.JSNativeMethod
+import com.adealink.weparty.webview.jsnativemethod.addCommonMethod
+import com.ushareit.easysdk.web.view.SPWebView
+import kotlin.math.abs
+
+
+/**
+ */
+abstract class BaseWebView : SPWebView, IWebView, NestedScrollingChild3 {
+
+    companion object {
+        private const val INVALID_POINTER = -1
+    }
+
+    private var lUrl: String? = null
+
+    private var jsBridges = hashMapOf<String, JSBridge>()
+    override var webViewCallback: IWebViewCallback? = null
+
+    private var commonWebViewClient: CommonWebViewClient? = null
+    private var commonWebChromeClient: CommonWebChromeClient? = null
+
+    private val scrollOffset = IntArray(2)
+    private val scrollConsumed = IntArray(2)
+    private var lastMotionY = 0F
+    private lateinit var childHelper: NestedScrollingChildHelper
+    private var isBeingDragged = false
+    private var velocityTracker: VelocityTracker? = null
+    private var touchSlop = 0
+    private var activePointerId = INVALID_POINTER
+    private var nestedYOffset = 0F
+    private lateinit var scroller: OverScroller
+    private var minimumVelocity = 0F
+    private var maximumVelocity = 0F
+    private var lastScrollerY = 0
+
+    constructor(context: Context) : super(context) {
+        init()
+    }
+
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
+        init()
+    }
+
+    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
+            super(context, attrs, defStyleAttr) {
+        init()
+    }
+
+    private fun init() {
+        initScrollView()
+        initWebViewSetting()
+        initJSBridge(createDefaultJSBridge())
+        initWebViewClient()
+        initWebChromeClient()
+    }
+
+    @SuppressLint("SetJavaScriptEnabled")
+    open fun initWebViewSetting() {
+        settings.domStorageEnabled = true
+        settings.javaScriptEnabled = true
+        settings.useWideViewPort = true
+        settings.defaultTextEncodingName = "utf-8"
+        settings.cacheMode = WebSettings.LOAD_DEFAULT
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            //5.0以上开启混合模式加载
+            settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
+        }
+        if (!AppBase.isRelease && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            setWebContentsDebuggingEnabled(true)
+        }
+//        settings.setUserAgentString(sb) 增加UA
+        settings.builtInZoomControls = false
+        settings.setSupportZoom(false)
+        settings.saveFormData = false
+        settings.savePassword = false
+        settings.allowFileAccess = true
+        settings.useWideViewPort = true
+        settings.loadWithOverviewMode = true
+        settings.javaScriptCanOpenWindowsAutomatically = true
+        settings.mediaPlaybackRequiresUserGesture = false
+    }
+
+    abstract fun createDefaultJSBridge(): JSBridge
+
+    @SuppressLint("JavascriptInterface")
+    protected fun initJSBridge(bridge: JSBridge) {
+        Log.d(
+            TAG_WEB_VIEW,
+            "initJSBridge, jsBridge:$bridge, interfaceName:${bridge.interfaceName()}"
+        )
+        addJSBridge(bridge)
+    }
+
+    open fun addJSNativeMethods(jsBridge: JSBridge) {}
+
+    open fun addJSNativeMethod(method: JSNativeMethod<*, *>) {
+        for (bridge in jsBridges.values) {
+            bridge.addNativeMethod(method)
+        }
+    }
+
+    @SuppressLint("JavascriptInterface")
+    fun addJSBridge(bridge: JSBridge) {
+        jsBridges[bridge.interfaceName()] = bridge
+        Log.d(
+            TAG_WEB_VIEW,
+            "addJSBridge, interfaceName:${bridge.interfaceName()}, jsBridge:$bridge"
+        )
+        addJavascriptInterface(bridge as Any, bridge.interfaceName())
+        bridge.addCommonMethod(this)
+        addJSNativeMethods(bridge)
+    }
+
+    fun getJSBridge(name: String): JSBridge? {
+        return jsBridges[name]
+    }
+
+    open fun <T> notifyNativeMessage(message: NativeMessage<T>) {
+        // TODO: zhangfei
+        for (bridge in jsBridges.values) {
+            bridge.notifyNativeMessage(message)
+        }
+    }
+
+    override fun shouldReloadUrl(url: String): Boolean {
+        return false
+    }
+
+    open fun initWebChromeClient() {
+        commonWebChromeClient = CommonWebChromeClient(this)
+        webChromeClient = commonWebChromeClient
+    }
+
+    open fun initWebViewClient() {
+        commonWebViewClient = CommonWebViewClient(this)
+        val constWebViewClient = commonWebViewClient ?: return
+        webViewClient = constWebViewClient
+    }
+
+    override fun getCommonWebChromeClient(): CommonWebChromeClient? {
+        return commonWebChromeClient
+    }
+
+    override fun getCommonWebViewClient(): CommonWebViewClient? {
+        return commonWebViewClient
+    }
+
+    override fun loadUrl(url: String) {
+        lUrl = url
+        super.loadUrl(url)
+    }
+
+    override fun getLoadUrl(): String? {
+        return lUrl
+    }
+
+    override fun getWebView(): WebView {
+        return this
+    }
+
+    override fun clearCache(includeDiskFiles: Boolean) {
+        super.clearCache(includeDiskFiles)
+        try {
+            context.deleteDatabase("webview.db")
+            context.deleteDatabase("webviewCache.db")
+        } catch (e: Exception) {
+            Log.e(TAG_WEB_VIEW, "clearCache, e:${e}")
+        }
+    }
+
+    open fun initScrollView() {
+        childHelper = NestedScrollingChildHelper(this)
+        overScrollMode = WebView.OVER_SCROLL_NEVER
+        isNestedScrollingEnabled = true
+        scroller = OverScroller(context)
+        val configuration: ViewConfiguration = ViewConfiguration.get(context)
+        touchSlop = configuration.scaledTouchSlop
+        minimumVelocity = configuration.scaledMinimumFlingVelocity.toFloat()
+        maximumVelocity = configuration.scaledMaximumFlingVelocity.toFloat()
+    }
+
+    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+        val action: Int = ev.action
+        if (action == MotionEvent.ACTION_MOVE && isBeingDragged) {
+            return true
+        }
+
+        when (action and MotionEvent.ACTION_MASK) {
+            MotionEvent.ACTION_MOVE -> {
+                val activePointerId = activePointerId
+                if (activePointerId == INVALID_POINTER) {
+                    return isBeingDragged
+                }
+
+                val pointerIndex: Int = ev.findPointerIndex(activePointerId)
+                if (pointerIndex == -1) {
+                    Log.e(
+                        TAG_WEB_VIEW, "Invalid pointerId:$activePointerId in onInterceptTouchEvent"
+                    )
+                    return isBeingDragged
+                }
+                val y = ev.getY(pointerIndex)
+                val yDiff = abs(y - lastMotionY)
+                if (yDiff > touchSlop && nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL === 0) {
+                    isBeingDragged = true
+                    lastMotionY = y
+                    initVelocityTrackerIfNotExists()
+                    velocityTracker?.addMovement(ev)
+                    nestedYOffset = 0F
+                    val parent: ViewParent? = parent
+                    parent?.requestDisallowInterceptTouchEvent(true)
+                }
+            }
+
+            MotionEvent.ACTION_DOWN -> {
+                lastMotionY = ev.y
+                activePointerId = ev.getPointerId(0)
+                initOrResetVelocityTracker()
+                velocityTracker?.addMovement(ev)
+                scroller.computeScrollOffset()
+                isBeingDragged = !scroller.isFinished
+                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
+            }
+
+            MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
+                isBeingDragged = false
+                activePointerId = INVALID_POINTER
+                recycleVelocityTracker()
+                if (scroller.springBack(scrollX, scrollY, 0, 0, 0, getScrollRange())) {
+                    ViewCompat.postInvalidateOnAnimation(this)
+                }
+                stopNestedScroll()
+            }
+
+            MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
+        }
+        return isBeingDragged
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    override fun onTouchEvent(ev: MotionEvent): Boolean {
+        initVelocityTrackerIfNotExists()
+        val vtev: MotionEvent = MotionEvent.obtain(ev)
+        val actionMasked: Int = ev.actionMasked
+        if (actionMasked == MotionEvent.ACTION_DOWN) {
+            nestedYOffset = 0F
+        }
+        vtev.offsetLocation(0F, nestedYOffset)
+        when (actionMasked) {
+            MotionEvent.ACTION_DOWN -> {
+                if (!scroller.isFinished.also { isBeingDragged = it }) {
+                    parent?.requestDisallowInterceptTouchEvent(true)
+                }
+                if (!scroller.isFinished) {
+                    abortAnimatedScroll()
+                }
+                lastMotionY = ev.y
+                activePointerId = ev.getPointerId(0)
+                startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                val activePointerIndex: Int = ev.findPointerIndex(activePointerId)
+                if (activePointerIndex == -1) {
+                    Log.e(TAG_WEB_VIEW, "Invalid pointerId:$activePointerId in onTouchEvent")
+                    velocityTracker?.addMovement(vtev)
+                    vtev.recycle()
+                    return super.onTouchEvent(ev)
+                }
+                val y = ev.getY(activePointerIndex)
+                var deltaY = lastMotionY - y
+                if (dispatchNestedPreScroll(
+                        0,
+                        deltaY.toInt(),
+                        scrollConsumed,
+                        scrollOffset,
+                        ViewCompat.TYPE_TOUCH
+                    )
+                ) {
+                    deltaY -= scrollConsumed[1]
+                    nestedYOffset += scrollOffset[1]
+                }
+                if (!isBeingDragged && abs(deltaY) > touchSlop) {
+                    this.parent?.requestDisallowInterceptTouchEvent(true)
+                    isBeingDragged = true
+                    if (deltaY > 0) {
+                        deltaY -= touchSlop
+                    } else {
+                        deltaY += touchSlop
+                    }
+                }
+                if (isBeingDragged) {
+                    lastMotionY = y - scrollOffset[1]
+                    val oldY = scrollY
+                    val range = getScrollRange()
+                    // Calling overScrollByCompat will call onOverScrolled, which
+                    // calls onScrollChanged if applicable.
+                    if (overScrollByCompat(
+                            0, deltaY.toInt(), 0, oldY, 0, range, 0,
+                            0, true
+                        ) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)
+                    ) {
+                        velocityTracker?.clear()
+                    }
+                    val scrolledDeltaY = scrollY - oldY
+                    val unconsumedY = deltaY - scrolledDeltaY
+                    scrollConsumed[1] = 0
+                    dispatchNestedScroll(
+                        0, scrolledDeltaY, 0, unconsumedY.toInt(), scrollOffset,
+                        ViewCompat.TYPE_TOUCH, scrollConsumed
+                    )
+                    lastMotionY -= scrollOffset[1]
+                    nestedYOffset += scrollOffset[1]
+                }
+            }
+
+            MotionEvent.ACTION_UP -> {
+                velocityTracker?.computeCurrentVelocity(1000, maximumVelocity)
+                val initialVelocity = velocityTracker?.getYVelocity(activePointerId) ?: 0F
+                if (abs(initialVelocity) > minimumVelocity) {
+                    if (!dispatchNestedPreFling(0f, -initialVelocity)) {
+                        dispatchNestedFling(0f, -initialVelocity, true)
+                        fling(-initialVelocity.toInt())
+                    }
+                } else if (scroller.springBack(scrollX, scrollY, 0, 0, 0, getScrollRange())) {
+                    ViewCompat.postInvalidateOnAnimation(this)
+                }
+                activePointerId = INVALID_POINTER
+                endDrag()
+            }
+
+            MotionEvent.ACTION_CANCEL -> {
+                if (isBeingDragged) {
+                    if (scroller.springBack(scrollX, scrollY, 0, 0, 0, getScrollRange())) {
+                        ViewCompat.postInvalidateOnAnimation(this)
+                    }
+                }
+                activePointerId = INVALID_POINTER
+                endDrag()
+            }
+
+            MotionEvent.ACTION_POINTER_DOWN -> {
+                val index: Int = ev.actionIndex
+                lastMotionY = ev.getY(index)
+                activePointerId = ev.getPointerId(index)
+            }
+
+            MotionEvent.ACTION_POINTER_UP -> {
+                onSecondaryPointerUp(ev)
+                lastMotionY = ev.getY(ev.findPointerIndex(activePointerId))
+            }
+        }
+        velocityTracker?.addMovement(vtev)
+        vtev.recycle()
+        return super.onTouchEvent(ev)
+    }
+
+    open fun abortAnimatedScroll() {
+        scroller.abortAnimation()
+        stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)
+    }
+
+    open fun endDrag() {
+        isBeingDragged = false
+        recycleVelocityTracker()
+        stopNestedScroll()
+    }
+
+    open fun onSecondaryPointerUp(ev: MotionEvent) {
+        val pointerIndex: Int =
+            (ev.action and MotionEvent.ACTION_POINTER_INDEX_MASK shr MotionEvent.ACTION_POINTER_INDEX_SHIFT)
+        val pointerId: Int = ev.getPointerId(pointerIndex)
+        if (pointerId == activePointerId) {
+            val newPointerIndex = if (pointerIndex == 0) 1 else 0
+            lastMotionY = ev.getY(newPointerIndex)
+            activePointerId = ev.getPointerId(newPointerIndex)
+            velocityTracker?.clear()
+        }
+    }
+
+    open fun fling(velocityY: Int) {
+        val height = height
+        scroller.fling(
+            scrollX, scrollY,
+            0, velocityY,
+            0, 0, Int.MIN_VALUE, Int.MAX_VALUE,
+            0, height / 2
+        )
+        runAnimatedScroll(true)
+    }
+
+    open fun runAnimatedScroll(participateInNestedScrolling: Boolean) {
+        if (participateInNestedScrolling) {
+            startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH)
+        } else {
+            stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)
+        }
+        lastScrollerY = scrollY
+        ViewCompat.postInvalidateOnAnimation(this)
+    }
+
+    override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
+        if (disallowIntercept) {
+            recycleVelocityTracker()
+        }
+        super.requestDisallowInterceptTouchEvent(disallowIntercept)
+    }
+
+    open fun initOrResetVelocityTracker() {
+        if (velocityTracker == null) {
+            velocityTracker = VelocityTracker.obtain()
+        } else {
+            velocityTracker?.clear()
+        }
+    }
+
+    open fun initVelocityTrackerIfNotExists() {
+        if (velocityTracker == null) {
+            velocityTracker = VelocityTracker.obtain()
+        }
+    }
+
+    open fun recycleVelocityTracker() {
+        velocityTracker?.recycle()
+        velocityTracker = null
+    }
+
+    override fun overScrollBy(
+        deltaX: Int, deltaY: Int,
+        scrollX: Int, scrollY: Int,
+        scrollRangeX: Int, scrollRangeY: Int,
+        maxOverScrollX: Int, maxOverScrollY: Int,
+        isTouchEvent: Boolean
+    ): Boolean {
+        if (!isBeingDragged) overScrollByCompat(
+            deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY,
+            maxOverScrollX, maxOverScrollY, isTouchEvent
+        )
+        return true
+    }
+
+    open fun getScrollRange(): Int {
+        return computeVerticalScrollRange()
+    }
+
+    override fun isNestedScrollingEnabled(): Boolean {
+        return childHelper.isNestedScrollingEnabled
+    }
+
+    override fun setNestedScrollingEnabled(enabled: Boolean) {
+        childHelper.isNestedScrollingEnabled = enabled
+    }
+
+    override fun startNestedScroll(axes: Int, type: Int): Boolean {
+        return childHelper.startNestedScroll(axes, type)
+    }
+
+    override fun startNestedScroll(axes: Int): Boolean {
+        return startNestedScroll(axes, ViewCompat.TYPE_TOUCH)
+    }
+
+    override fun stopNestedScroll(type: Int) {
+        childHelper.stopNestedScroll(type)
+    }
+
+    override fun stopNestedScroll() {
+        stopNestedScroll(ViewCompat.TYPE_TOUCH)
+    }
+
+    override fun hasNestedScrollingParent(type: Int): Boolean {
+        return childHelper.hasNestedScrollingParent(type)
+    }
+
+    override fun hasNestedScrollingParent(): Boolean {
+        return hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)
+    }
+
+    override fun dispatchNestedScroll(
+        dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,
+        offsetInWindow: IntArray?
+    ): Boolean {
+        return dispatchNestedScroll(
+            dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
+            offsetInWindow, ViewCompat.TYPE_TOUCH
+        )
+    }
+
+    override fun dispatchNestedScroll(
+        dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,
+        offsetInWindow: IntArray?, type: Int
+    ): Boolean {
+        return childHelper.dispatchNestedScroll(
+            dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
+            offsetInWindow, type
+        )
+    }
+
+    override fun dispatchNestedScroll(
+        dxConsumed: Int,
+        dyConsumed: Int,
+        dxUnconsumed: Int,
+        dyUnconsumed: Int,
+        offsetInWindow: IntArray?,
+        type: Int,
+        consumed: IntArray
+    ) {
+        childHelper.dispatchNestedScroll(
+            dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
+            offsetInWindow, type, consumed
+        )
+    }
+
+    override fun dispatchNestedPreScroll(
+        dx: Int,
+        dy: Int,
+        consumed: IntArray?,
+        offsetInWindow: IntArray?
+    ): Boolean {
+        return dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH)
+    }
+
+    override fun dispatchNestedPreScroll(
+        dx: Int,
+        dy: Int,
+        consumed: IntArray?,
+        offsetInWindow: IntArray?,
+        type: Int
+    ): Boolean {
+        return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
+    }
+
+    override fun dispatchNestedFling(
+        velocityX: Float,
+        velocityY: Float,
+        consumed: Boolean
+    ): Boolean {
+        return childHelper.dispatchNestedFling(velocityX, velocityY, false)
+    }
+
+    override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean {
+        return childHelper.dispatchNestedPreFling(velocityX, velocityY)
+    }
+
+    override fun getNestedScrollAxes(): Int {
+        return ViewCompat.SCROLL_AXIS_VERTICAL
+    }
+
+    override fun computeScroll() {
+        if (scroller.isFinished) {
+            return
+        }
+
+        scroller.computeScrollOffset()
+        val y: Int = scroller.currY
+        var unconsumed = y - lastScrollerY
+        lastScrollerY = y
+
+        // Nested Scrolling Pre Pass
+        scrollConsumed[1] = 0
+        dispatchNestedPreScroll(
+            0, unconsumed, scrollConsumed, null,
+            ViewCompat.TYPE_NON_TOUCH
+        )
+        unconsumed -= scrollConsumed[1]
+        if (unconsumed != 0) {
+            // Internal Scroll
+            val oldScrollY = scrollY
+            overScrollByCompat(
+                0, unconsumed, scrollX, oldScrollY, 0, getScrollRange(),
+                0, 0, false
+            )
+            val scrolledByMe = scrollY - oldScrollY
+            unconsumed -= scrolledByMe
+
+            // Nested Scrolling Post Pass
+            scrollConsumed[1] = 0
+            dispatchNestedScroll(
+                0, 0, 0, unconsumed, scrollOffset,
+                ViewCompat.TYPE_NON_TOUCH, scrollConsumed
+            )
+            unconsumed -= scrollConsumed[1]
+        }
+        if (unconsumed != 0) {
+            abortAnimatedScroll()
+        }
+        if (!scroller.isFinished) {
+            ViewCompat.postInvalidateOnAnimation(this)
+        }
+    }
+
+    // copied from NestedScrollView exacly as it looks, leaving overscroll related code, maybe future use
+    open fun overScrollByCompat(
+        deltaX: Int, deltaY: Int,
+        scrollX: Int, scrollY: Int,
+        scrollRangeX: Int, scrollRangeY: Int,
+        maxOverScrollX: Int, maxOverScrollY: Int,
+        isTouchEvent: Boolean
+    ): Boolean {
+        var maxOverScrollX = maxOverScrollX
+        var maxOverScrollY = maxOverScrollY
+        val overScrollMode = overScrollMode
+        val canScrollHorizontal = computeHorizontalScrollRange() > computeHorizontalScrollExtent()
+        val canScrollVertical = computeVerticalScrollRange() > computeVerticalScrollExtent()
+        val overScrollHorizontal = (overScrollMode == View.OVER_SCROLL_ALWAYS
+                || overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal)
+        val overScrollVertical = (overScrollMode == View.OVER_SCROLL_ALWAYS
+                || overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical)
+        var newScrollX = scrollX + deltaX
+        if (!overScrollHorizontal) {
+            maxOverScrollX = 0
+        }
+        var newScrollY = scrollY + deltaY
+        if (!overScrollVertical) {
+            maxOverScrollY = 0
+        }
+
+        // Clamp values if at the limits and record
+        val left = -maxOverScrollX
+        val right = maxOverScrollX + scrollRangeX
+        val top = -maxOverScrollY
+        val bottom = maxOverScrollY + scrollRangeY
+        var clampedX = false
+        if (newScrollX > right) {
+            newScrollX = right
+            clampedX = true
+        } else if (newScrollX < left) {
+            newScrollX = left
+            clampedX = true
+        }
+        var clampedY = false
+        if (newScrollY > bottom) {
+            newScrollY = bottom
+            clampedY = true
+        } else if (newScrollY < top) {
+            newScrollY = top
+            clampedY = true
+        }
+        if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) {
+            scroller.springBack(newScrollX, newScrollY, 0, 0, 0, getScrollRange())
+        }
+        onOverScrolled(newScrollX, newScrollY, clampedX, clampedY)
+        return clampedX || clampedY
+    }
+
+    override fun onDetachedFromWindow() {
+        super.onDetachedFromWindow()
+        commonWebViewClient?.onPageBack(this, originalUrl)
+    }
+
+}

+ 79 - 0
app/src/main/java/com/adealink/weparty/webview/CommonWebChromeClient.kt

@@ -0,0 +1,79 @@
+package com.adealink.weparty.webview
+
+import android.net.Uri
+import android.os.Build
+import android.webkit.ValueCallback
+import android.webkit.WebChromeClient
+import android.webkit.WebView
+import androidx.activity.result.ActivityResultLauncher
+import androidx.fragment.app.FragmentActivity
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.imageselect.SelectImageContract
+import com.adealink.weparty.imageselect.SelectImageRequest
+import com.adealink.weparty.imageselect.model.MEDIA_TYPE_IMAGE
+import com.adealink.weparty.imageselect.model.MEDIA_TYPE_VIDEO
+import com.adealink.weparty.imageselect.model.MediaType
+
+/**
+ */
+class CommonWebChromeClient(val webView: IWebView) : WebChromeClient() {
+
+    private var launcher: ActivityResultLauncher<SelectImageRequest>? = null
+
+    override fun onReceivedTitle(view: WebView?, title: String?) {
+        super.onReceivedTitle(view, title)
+        if (webView.webViewCallback?.isHostActive() == true) {
+            webView.webViewCallback?.setTitle(title)
+        }
+    }
+
+    override fun onProgressChanged(view: WebView?, newProgress: Int) {
+        super.onProgressChanged(view, newProgress)
+        if (webView.webViewCallback?.isHostActive() == true) {
+            webView.webViewCallback?.onProgressChanged(newProgress)
+        }
+    }
+
+    override fun onShowFileChooser(
+        webView: WebView?,
+        filePathCallback: ValueCallback<Array<Uri>>?,
+        fileChooserParams: FileChooserParams?
+    ): Boolean {
+        val activity = AppUtil.currentActivity as? FragmentActivity ?: return false
+        launcher = activity.activityResultRegistry.register(
+            "SelectImage",
+            SelectImageContract()
+        ) {
+            launcher?.unregister()
+            if (it.path.isNullOrEmpty()) {
+                filePathCallback?.onReceiveValue(null)
+            } else {
+                filePathCallback?.onReceiveValue(arrayOf(Uri.parse(it.uri)))
+            }
+        }
+        val mediaTypeList = mutableListOf<String>()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            fileChooserParams?.acceptTypes?.forEach {
+                mediaTypeList.add(it)
+            }
+        }
+        val selectImage = mediaTypeList.contains(MEDIA_TYPE_IMAGE)
+        val selectVideo = mediaTypeList.contains(MEDIA_TYPE_VIDEO)
+        val selectMediaType = when {
+            selectImage && selectVideo -> {
+                MediaType.ALL
+            }
+
+            selectVideo -> {
+                MediaType.VIDEO
+            }
+
+            else -> {
+                MediaType.IMAGE
+            }
+        }
+        launcher?.launch(SelectImageRequest(1, "", null, selectMediaType))
+        return true
+    }
+
+}

+ 214 - 0
app/src/main/java/com/adealink/weparty/webview/CommonWebViewClient.kt

@@ -0,0 +1,214 @@
+package com.adealink.weparty.webview
+
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.net.http.SslError
+import android.os.Build
+import android.os.SystemClock
+import android.webkit.SslErrorHandler
+import android.webkit.WebResourceError
+import android.webkit.WebResourceRequest
+import android.webkit.WebResourceResponse
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.annotation.RequiresApi
+import androidx.fragment.app.FragmentActivity
+import com.adealink.frame.log.Log
+import com.adealink.frame.statistics.CommonEventValue
+import com.adealink.frame.util.AppUtil
+import com.adealink.frame.util.getHost
+import com.adealink.frame.util.getUrlPath
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW_LOAD_FLOW
+import com.adealink.weparty.webview.loader.ResourceRequest
+import com.adealink.weparty.webview.loader.webResourceLoader
+import com.adealink.weparty.webview.stat.WebViewStatEvent
+import com.adealink.weparty.webview.util.SSLErrorHandleUtil
+
+/**
+ */
+open class CommonWebViewClient(val webView: IWebView) : WebViewClient() {
+
+    private var webViewLoadStat: WebViewStatEvent? = null
+    private var pageStartTime: Long = 0
+
+    fun onPageBack(view: WebView, url: String?) {
+        Log.d(TAG_WEB_VIEW_LOAD_FLOW, "page load back, url:$url")
+        webViewLoadStat
+            ?.apply {
+                duration to (SystemClock.elapsedRealtime() - pageStartTime)
+                result to CommonEventValue.Result.CANCEL
+            }
+            ?.send()
+        webViewLoadStat = null
+    }
+
+    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+        super.onPageStarted(view, url, favicon)
+        Log.d(TAG_WEB_VIEW_LOAD_FLOW, "page load start, url:$url")
+        if (webView.webViewCallback?.isHostActive() == true) {
+            pageStartTime = SystemClock.elapsedRealtime()
+            webViewLoadStat = WebViewStatEvent(WebViewStatEvent.Action.LOAD)
+                .apply {
+                    page to url
+                    result to CommonEventValue.Result.SUCCESS
+                    ua to webView.getWebView().settings.userAgentString
+                }
+            webView.webViewCallback?.onPageStarted(url, favicon)
+        }
+    }
+
+    override fun onPageFinished(view: WebView?, url: String?) {
+        super.onPageFinished(view, url)
+        if (webView.webViewCallback?.isHostActive() == true) {
+            Log.d(TAG_WEB_VIEW_LOAD_FLOW, "page load finished, url:$url")
+            webViewLoadStat
+                ?.apply {
+                    duration to (SystemClock.elapsedRealtime() - pageStartTime)
+                }
+                ?.send()
+            webViewLoadStat = null
+            webView.webViewCallback?.onPageFinished(url)
+        }
+    }
+
+    private fun isLoadHost(url: String?): Boolean {
+        if (url.isNullOrEmpty()) {
+            return false
+        }
+
+        val loadUrl = webView.getLoadUrl()
+        if (loadUrl.isNullOrEmpty()) {
+            return false
+        }
+
+        val host = getHost(url)
+        val originHost = getHost(loadUrl)
+        return host == originHost
+    }
+
+    @RequiresApi(Build.VERSION_CODES.M)
+    override fun onReceivedError(
+        view: WebView?,
+        request: WebResourceRequest?,
+        error: WebResourceError?,
+    ) {
+        super.onReceivedError(view, request, error)
+        val failingUrl = request?.url?.toString()
+        val errorCode = error?.errorCode ?: 0
+        val description = error?.description?.toString()
+        if (webView.webViewCallback?.isHostActive() == true && isLoadHost(failingUrl)) {
+            webView.webViewCallback?.onReceivedError(failingUrl, errorCode, description)
+        }
+        reportError(view?.url ?: "", failingUrl ?: "", errorCode, description ?: "")
+        Log.e(
+            TAG_WEB_VIEW,
+            "onReceivedError, webUrl:${view?.url}, failingUrl:$failingUrl, errorCode:$errorCode, description:$description"
+        )
+    }
+
+    override fun onReceivedError(
+        view: WebView?,
+        errorCode: Int,
+        description: String?,
+        failingUrl: String?,
+    ) {
+        super.onReceivedError(view, errorCode, description, failingUrl)
+        if (webView.webViewCallback?.isHostActive() == true && isLoadHost(failingUrl)) {
+            webView.webViewCallback?.onReceivedError(failingUrl, errorCode, description)
+        }
+        reportError(view?.url ?: "", failingUrl ?: "", errorCode, description ?: "")
+        Log.e(
+            TAG_WEB_VIEW,
+            "onReceivedError, webUrl: ${view?.url}, failingUrl:$failingUrl, errorCode:$errorCode, description:$description"
+        )
+    }
+
+    override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
+        if (webView.webViewCallback?.isHostActive() == true) {
+            val activity = AppUtil.currentActivity as? FragmentActivity
+            activity?.supportFragmentManager?.let {
+                SSLErrorHandleUtil.createSSLErrorHandleDialog(view?.url, onPositive = {
+                    handler?.proceed()
+                }, onNegative = {
+                    super.onReceivedSslError(view, handler, error)
+                    webView.webViewCallback?.onReceivedSslError(handler, error)
+                    true
+                })?.show(it)
+            }
+        }
+        reportError(webView.getUrl() ?: "", error?.url ?: "", -100 /*-100表示ssl error*/, error.toString())
+        Log.e(
+            TAG_WEB_VIEW,
+            "onReceivedSslError, url:${webView.getUrl()}, error:$error"
+        )
+    }
+
+    private fun reportError(webUrl: String, requestUrl: String, code: Int, error: String) {
+        Log.d(
+            TAG_WEB_VIEW_LOAD_FLOW,
+            "page load error, webUrl:$webUrl, requestUrl:${requestUrl}, code:$code, error:$error"
+        )
+        webViewLoadStat
+            ?.apply {
+                this.code to code
+                this.error to error
+                uri to getUrlPath(requestUrl)
+                result to CommonEventValue.Result.FAILED
+            }
+            ?.send()
+        WebViewStatEvent(WebViewStatEvent.Action.ERROR)
+            .apply {
+                uri to getUrlPath(requestUrl)
+                this.code to code
+                this.error to error
+                this.host to getHost(webUrl)
+            }
+            .send()
+    }
+
+    override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
+        if (url != null && !url.startsWith("http")) {
+            try {
+                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+                intent.flags = (Intent.FLAG_ACTIVITY_NEW_TASK
+                        or Intent.FLAG_ACTIVITY_SINGLE_TOP)
+                AppUtil.currentActivity?.startActivity(intent)
+            } catch (e: Exception) {
+                e.printStackTrace()
+            }
+            return true
+        }
+
+        if (url != null && webView.shouldReloadUrl(url)) {
+            webView.loadUrl(url)
+            return true
+        }
+
+        return super.shouldOverrideUrlLoading(view, url)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    override fun shouldInterceptRequest(
+        view: WebView,
+        request: WebResourceRequest
+    ): WebResourceResponse? {
+        val isRedirect = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            request.isRedirect
+        } else {
+            false
+        }
+        val response = webResourceLoader.load(
+            ResourceRequest(
+                request.url.toString(),
+                request.method,
+                request.requestHeaders,
+                request.isForMainFrame,
+                isRedirect
+            )
+        )
+        return response ?: super.shouldInterceptRequest(view, request)
+    }
+
+}

+ 34 - 0
app/src/main/java/com/adealink/weparty/webview/IWebView.kt

@@ -0,0 +1,34 @@
+package com.adealink.weparty.webview
+
+import android.webkit.ValueCallback
+import android.webkit.WebView
+import com.adealink.weparty.webview.callback.IWebViewCallback
+
+interface IWebView {
+
+    var webViewCallback: IWebViewCallback?
+
+    fun getUrl(): String?
+
+    fun getOriginalUrl(): String?
+
+    fun getLoadUrl(): String?
+
+    fun loadUrl(url: String)
+
+    fun evaluateJavascript(
+        script: String,
+        resultCallback: ValueCallback<String>?,
+    )
+
+    fun getCommonWebChromeClient(): CommonWebChromeClient?
+
+    fun getCommonWebViewClient(): CommonWebViewClient?
+
+    fun getWebView(): WebView
+
+    /**
+     * 拦截需要重加载url
+     */
+    fun shouldReloadUrl(url: String): Boolean
+}

+ 76 - 0
app/src/main/java/com/adealink/weparty/webview/JSBridgeConfig.kt

@@ -0,0 +1,76 @@
+package com.adealink.weparty.webview
+
+import android.util.Log
+import com.adealink.frame.data.json.froJsonErrorNull
+import com.adealink.frame.data.json.toJsonErrorNull
+import com.adealink.frame.util.getHost
+import com.adealink.frame.util.getPrimaryDomain
+import com.adealink.weparty.webview.jsbridge.config.IJSBridgeConfig
+import com.adealink.weparty.config.GlobalConfigType
+import com.adealink.weparty.config.IGlobalConfigListener
+import com.adealink.weparty.config.globalConfigManager
+import com.adealink.weparty.network.CDN_HOST
+import com.adealink.weparty.network.DEBUG_HOST_CN
+import com.adealink.weparty.network.RELEASE_HOSTS
+import com.adealink.weparty.network.WEB_PRIMARY_DOMAINS
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW_WHITE_HOST
+import java.util.*
+import java.util.concurrent.ConcurrentHashMap
+
+class JSBridgeConfig : IJSBridgeConfig, IGlobalConfigListener {
+
+    private val whiteUrlSet = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
+    private val blackUrlSet = Collections.newSetFromMap(ConcurrentHashMap<String, Boolean>())
+
+    init {
+        whiteUrlSet.addAll(froJsonErrorNull<Set<String>>(JSBridgePrefs.whiteHostSet) ?: setOf())
+        // TODO: 白名单
+//        globalConfigManager.addListener(GlobalConfigType.GLOBAL_JS_HOST_WHITELIST, this)
+//        addWhiteConfig(
+//            globalConfigManager.getConfig(GlobalConfigType.GLOBAL_JS_HOST_WHITELIST)?.getOrNull(0)
+//        )
+        //hardcode 白名单域名
+        addWhiteList(RELEASE_HOSTS)
+        addWhiteList(listOf(CDN_HOST))
+        addWhiteList(listOf(DEBUG_HOST_CN))
+        addWhiteList(WEB_PRIMARY_DOMAINS)
+    }
+
+    override fun onConfigGet(configType: GlobalConfigType, config: List<String>) {
+//        if (configType == GlobalConfigType.GLOBAL_JS_HOST_WHITELIST) {
+//            addWhiteConfig(config.getOrNull(0))
+//        }
+    }
+
+    private fun addWhiteConfig(config: String?) {
+        val jsHostWhiteList = froJsonErrorNull<List<String>>(config)
+        if (jsHostWhiteList != null) {
+            addWhiteList(jsHostWhiteList)
+        }
+    }
+
+    override fun addWhiteList(hosts: List<String>) {
+        Log.i(TAG_WEB_VIEW_WHITE_HOST, "addWhiteList:$hosts")
+        val added = whiteUrlSet.addAll(hosts.filter { it.isNotEmpty() })
+        if (added) {
+            JSBridgePrefs.whiteHostSet = toJsonErrorNull(whiteUrlSet) ?: ""
+        }
+    }
+
+    override fun addBlackList(hosts: List<String>) {
+        blackUrlSet.addAll(hosts.filter { it.isNotEmpty() })
+    }
+
+    override fun isWhite(url: String): Boolean {
+        val host = getHost(url)
+        val primaryDomain = getPrimaryDomain(url)
+        val whiteHost = whiteUrlSet.find { it == primaryDomain || it == host }
+        Log.d(TAG_WEB_VIEW_WHITE_HOST, "isWhite, url:$url, primaryDomain:$primaryDomain, whiteHost:$whiteHost")
+        return whiteHost != null || WebModule.getOfflineH5Urls().contains(url)
+    }
+
+    override fun isBlack(url: String): Boolean {
+        return blackUrlSet.contains(getHost(url))
+    }
+
+}

+ 13 - 0
app/src/main/java/com/adealink/weparty/webview/JSBridgePrefs.kt

@@ -0,0 +1,13 @@
+package com.adealink.weparty.webview
+
+import android.content.Context
+import com.adealink.frame.storage.sp.TypeDelegationPrefs
+import com.adealink.frame.util.AppUtil
+
+object JSBridgePrefs : TypeDelegationPrefs(
+    prefs = {
+        AppUtil.appContext.getSharedPreferences("pref_js_bridge", Context.MODE_PRIVATE)
+    }
+) {
+    var whiteHostSet: String by PrefKey("key_white_host_set", "")
+}

+ 61 - 0
app/src/main/java/com/adealink/weparty/webview/WeNextWebView.kt

@@ -0,0 +1,61 @@
+package com.adealink.weparty.webview
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.ViewGroup
+import com.adealink.frame.log.Log
+import com.adealink.weparty.App
+import com.adealink.weparty.webview.constant.NATIVE_JS_BRIDGE
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW
+import com.adealink.weparty.webview.jsbridge.JSBridge
+import com.adealink.weparty.webview.jsbridge.JSBridgeImpl
+
+class WeNextWebView : BaseWebView {
+
+    constructor(context: Context) : super(context)
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
+            super(context, attrs, defStyleAttr)
+
+    override fun createDefaultJSBridge(): JSBridge {
+        return JSBridgeImpl(this, NATIVE_JS_BRIDGE)
+    }
+
+    override fun loadUrl(urlP: String) {
+        val url = requestUrl(urlP)
+        if (url.isEmpty()) {
+            Log.e(TAG_WEB_VIEW, "loadUrl, url is empty")
+            return
+        }
+
+        val newUrl = App.instance.networkService.replaceUrlHost(url)
+        Log.d(TAG_WEB_VIEW, "loadUrl, url:$urlP, newUrl:$newUrl")
+        super.loadUrl(newUrl)
+    }
+
+    /**
+     * 新旧版本兼容url处理
+     */
+    private fun requestUrl(url: String): String {
+//        val oldVipUrlPrefix = "${UrlConfig.urlPrefix}/vip?"
+//        if (url.startsWith(oldVipUrlPrefix)) {
+//            return url.replace(oldVipUrlPrefix, "${UrlConfig.vip}?")
+//        }
+
+        return url
+    }
+
+    fun onDestroy() {
+        try {
+            loadDataWithBaseURL(null, "", "text/html", "utf-8", null)
+            (parent as? ViewGroup)?.removeView(this)
+            removeAllViewsInLayout()
+            removeAllViews()
+            webChromeClient = null
+            destroy()
+        } catch (e: Exception) {
+            Log.e(TAG_WEB_VIEW, "release webview fail, for ${e.message}")
+        }
+    }
+
+}

+ 56 - 0
app/src/main/java/com/adealink/weparty/webview/WebModule.kt

@@ -0,0 +1,56 @@
+package com.adealink.weparty.webview
+
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.coroutine.dispatcher.Dispatcher
+import com.adealink.frame.data.json.froJsonErrorNull
+import com.adealink.frame.data.json.toJsonErrorNull
+import com.adealink.frame.util.AppUtil
+import com.adealink.frame.util.createDirByDeleteOldDir
+import com.adealink.frame.util.getDirSizeBytes
+import com.adealink.frame.util.runOnUiThread
+import com.adealink.weparty.storage.file.FilePath
+import com.adealink.weparty.webview.datasource.local.WebLocalService
+import kotlinx.coroutines.withContext
+import java.io.File
+
+object WebModule {
+
+    private val offlineH5UrlSet by fastLazy {
+        froJsonErrorNull<MutableSet<String>>(WebLocalService.offlineH5UrlsJson) ?: mutableSetOf()
+    }
+
+    fun clearCache() {
+        runOnUiThread {
+            AppUtil.currentActivity?.let {
+                WeNextWebView(it).clearCache(true)
+            }
+        }
+        Dispatcher.wenextThreadPoolExecutor.execute {
+            createDirByDeleteOldDir(FilePath.webPath)
+        }
+    }
+
+    suspend fun getCacheSize(): Long {
+        return withContext(Dispatcher.WENEXT_THREAD_POOL) {
+            return@withContext FilePath.webPath?.let {
+                getDirSizeBytes(File(it))
+            } ?: 0L
+        }
+    }
+
+    fun inEnteredBSFullScreenWeb(): Boolean {
+        // TODO: zhangfei
+        return false
+//        return BSWebViewActivity.isEnteredBSWebActivity
+    }
+
+    fun addOfflineH5Url(url: String) {
+        offlineH5UrlSet.add(url)
+        WebLocalService.offlineH5UrlsJson = toJsonErrorNull(offlineH5UrlSet) ?: ""
+    }
+
+    fun getOfflineH5Urls(): Set<String> {
+        return offlineH5UrlSet
+    }
+
+}

+ 28 - 0
app/src/main/java/com/adealink/weparty/webview/WebResourceConfig.kt

@@ -0,0 +1,28 @@
+package com.adealink.weparty.webview
+
+import com.adealink.weparty.network.CDN_HOST
+import com.adealink.weparty.storage.file.FilePath
+import com.adealink.weparty.webview.loader.IWebResourceConfig
+import com.adealink.weparty.webview.loader.LoaderType
+
+class WebResourceConfig : IWebResourceConfig {
+
+    override val timeoutMS: Long = 60_000L
+
+    override val cachePath: String?
+        get() = FilePath.webPath
+
+    override val cacheSizeKB: Long = 20 * 1024 //20M
+
+    override val loaderType: LoaderType
+        get() {
+            // TODO: 网络加载方式
+//            val webConfigJson =
+//                globalConfigManager.getConfig(GlobalConfigType.GLOBAL_WEB_CONFIG)?.getOrNull(0)
+//            val webConfig: WebConfig = froJsonErrorNull(webConfigJson) ?: return LoaderType.NORMAL
+////            return LoaderType.map(webConfig.loaderType)
+            return LoaderType.NORMAL
+        }
+    override val reportHosts: Set<String> = setOf(CDN_HOST)
+
+}

+ 113 - 0
app/src/main/java/com/adealink/weparty/webview/WebViewActivity.kt

@@ -0,0 +1,113 @@
+package com.adealink.weparty.webview
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.KeyEvent
+import android.view.View
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.updateLayoutParams
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.router.Router
+import com.adealink.frame.router.annotation.BindExtra
+import com.adealink.frame.router.annotation.RouterUri
+import com.adealink.frame.share.shareManager
+import com.adealink.frame.util.DisplayUtil
+import com.adealink.frame.util.statusBarHeight
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.BaseActivity
+import com.adealink.weparty.databinding.ActivityWebviewBinding
+import com.adealink.weparty.module.webview.Web
+import com.adealink.weparty.webview.callback.IWebViewFragmentCallback
+import com.adealink.weparty.webview.constant.NavigationBarMode
+import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
+
+@RouterUri(path = [Web.FullScreen.PATH], desc = "全屏web页")
+class WebViewActivity : BaseActivity(), IWebViewFragmentCallback {
+
+    @BindExtra(name = Web.Common.EXTRA_URL, desc = "链接")
+    var url: String? = null
+
+    @BindExtra(Web.Common.EXTRA_LOADING_URL, desc = "加载图标链接")
+    var loadingUrl: String? = null
+
+    @BindExtra(Web.Common.EXTRA_DIALOG_TAG, desc = "dialog的tag")
+    var fgTag: String? = ""
+
+
+    private val binding by viewBinding(ActivityWebviewBinding::inflate)
+
+    private val webFragment: WebViewFragment by fastLazy { WebViewFragment() }
+
+    override fun onBeforeCreate() {
+        super.onBeforeCreate()
+        Router.bind(this)
+    }
+
+    override fun initViews() {
+        QMUIStatusBarHelper.setStatusBarLightMode(this)
+        setContentView(binding.root)
+        binding.topBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
+            topMargin = this@WebViewActivity.statusBarHeight()
+        }
+
+
+        webFragment.arguments = Bundle().apply {
+            putString(Web.Common.EXTRA_URL, url)
+            putString(Web.Common.EXTRA_LOADING_URL, loadingUrl)
+            putString(Web.Common.EXTRA_DIALOG_TAG, fgTag)
+        }
+        webFragment.replaceAndShow(supportFragmentManager, R.id.v_webview, url ?: "")
+        webFragment.setWebFragmentCallback(this)
+    }
+
+    override fun closeWebView() {
+        finish()
+    }
+
+    override fun setTitle(title: String?) {
+        binding.topBar.setTitle(title ?: "")
+    }
+
+    override fun setImmersionMode() {
+        binding.topBar.setToTransparentMode()
+        binding.topBar.updateLayoutParams<ConstraintLayout.LayoutParams> {
+            topMargin = this@WebViewActivity.statusBarHeight()
+        }
+        binding.root.fitsSystemWindows = false
+        setNavigationBarMode(NavigationBarMode.DARK)
+    }
+
+    override fun setNavigationBarMode(mode: NavigationBarMode) {
+        when (mode) {
+            NavigationBarMode.DARK -> {
+                QMUIStatusBarHelper.setStatusBarDarkMode(this)
+                setNavigationBarColor(R.color.black)
+            }
+
+            NavigationBarMode.LIGHT -> {
+                QMUIStatusBarHelper.setStatusBarLightMode(this)
+            }
+        }
+    }
+
+    override fun hideNavigationBar() {
+        binding.topBar.visibility = View.GONE
+        binding.root.fitsSystemWindows = false
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        super.onActivityResult(requestCode, resultCode, data)
+        webFragment.onActivityResult(requestCode, resultCode, data)
+        shareManager.onActivityResult(requestCode, resultCode, data)
+//        WalletModule.onActivityResult(requestCode, resultCode, data)
+    }
+
+    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+        if (webFragment.onKeyDown(keyCode, event)) {
+            return true
+        }
+        return super.onKeyDown(keyCode, event)
+    }
+
+}

+ 126 - 0
app/src/main/java/com/adealink/weparty/webview/WebViewDialogFragment.kt

@@ -0,0 +1,126 @@
+package com.adealink.weparty.webview
+
+import android.content.DialogInterface
+import android.graphics.drawable.GradientDrawable
+import android.os.Bundle
+import android.view.Gravity
+import android.view.KeyEvent
+import android.view.WindowManager
+import androidx.fragment.app.FragmentManager
+import com.adealink.frame.aab.util.getCompatColor
+import com.adealink.frame.base.fastLazy
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.router.Router
+import com.adealink.frame.router.annotation.BindExtra
+import com.adealink.frame.util.DisplayUtil
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.commonui.widget.BottomDialogFragment
+import com.adealink.weparty.databinding.DialogWebviewBinding
+import com.adealink.weparty.module.webview.Web
+import com.adealink.weparty.module.webview.Web.Common.Companion.EXTRA_HEIGHT
+import com.adealink.weparty.module.webview.Web.Common.Companion.EXTRA_TOP_RADIUS
+import com.adealink.weparty.webview.callback.IWebViewFragmentCallback
+
+class WebViewDialogFragment : BottomDialogFragment(R.layout.dialog_webview),
+    IWebViewFragmentCallback {
+
+    private val binding by viewBinding(DialogWebviewBinding::bind)
+
+    private var url: String = ""
+
+    @BindExtra(EXTRA_HEIGHT, desc = "高度")
+    var heightPx = 0
+        set(value) {
+            field = value
+            if (isAdded) {
+                resetDialogAttributes()
+            }
+        }
+
+    @BindExtra(Web.Common.EXTRA_TRANSPARENT, desc = "是否背景透明")
+    var transparent: Boolean? = false
+
+    private val webFragment: WebViewFragment by fastLazy { WebViewFragment() }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        Router.bind(this)
+    }
+
+    override fun initViews() {
+        super.initViews()
+        webFragment.arguments = this.arguments
+        webFragment.setWebFragmentCallback(this)
+        webFragment.replaceAndShow(childFragmentManager, R.id.v_webview, url)
+
+        val topRadius = arguments?.getInt(EXTRA_TOP_RADIUS) ?: 16.dp()
+        val bgDrawable = GradientDrawable().apply {
+            shape = GradientDrawable.RECTANGLE
+            setColor(getCompatColor(R.color.white))
+            cornerRadii = floatArrayOf(
+                topRadius.toFloat(), topRadius.toFloat(),
+                topRadius.toFloat(), topRadius.toFloat(),
+                0f, 0f,
+                0f, 0f,
+            )
+        }
+        binding.root.background = bgDrawable
+
+        dialog?.setOnKeyListener { _: DialogInterface?, keyCode: Int, keyEvent: KeyEvent ->
+            if (webFragment.onKeyDown(keyCode, keyEvent)) {
+                return@setOnKeyListener true
+            }
+            false
+        }
+        if (transparent == true) {
+            binding.root.setBackgroundColor(getCompatColor(R.color.transparent))
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        resetDialogAttributes()
+    }
+
+    private fun resetDialogAttributes() {
+        val dialog = dialog ?: return
+        val window = dialog.window ?: return
+        val layoutParams = window.attributes
+        layoutParams.gravity = Gravity.BOTTOM
+        layoutParams.flags = layoutParams.flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND.inv()
+        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT
+        layoutParams.height = getRealHeight()
+        layoutParams.dimAmount = dimAmount
+        window.attributes = layoutParams
+    }
+
+    private fun getRealHeight(): Int {
+        var finalHeight = heightPx
+        val window = dialog?.window
+        if (window != null && heightPx > DisplayUtil.getScreenHeight() - DisplayUtil.getStatusBarHeight(
+                window
+            )
+        ) {
+            finalHeight = DisplayUtil.getScreenHeight() - DisplayUtil.getStatusBarHeight(window)
+        }
+        return if (finalHeight > 0) finalHeight else (DisplayUtil.getScreenHeight() * 0.5).toInt()
+    }
+
+    fun showUrl(fragmentManager: FragmentManager, url: String) {
+        this.url = url
+        if (isAdded) {
+            if (webFragment.isAdded) {
+                webFragment.showUrl(url)
+            }
+        } else {
+            show(fragmentManager)
+        }
+    }
+
+
+    override fun closeWebView() {
+        dismiss()
+    }
+
+}

+ 320 - 0
app/src/main/java/com/adealink/weparty/webview/WebViewFragment.kt

@@ -0,0 +1,320 @@
+package com.adealink.weparty.webview
+
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.Outline
+import android.net.http.SslError
+import android.os.Bundle
+import android.view.KeyEvent
+import android.view.View
+import android.view.ViewOutlineProvider
+import android.webkit.SslErrorHandler
+import androidx.core.net.toUri
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.ext.isViewBindingValid
+import com.adealink.frame.log.Log
+import com.adealink.frame.mvvm.view.viewBinding
+import com.adealink.frame.router.Router
+import com.adealink.frame.router.annotation.BindExtra
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.BaseFragment
+import com.adealink.weparty.commonui.ext.dp
+import com.adealink.weparty.databinding.FragmentWebviewBinding
+import com.adealink.weparty.module.webview.Web
+import com.adealink.weparty.module.webview.Web.Common.Companion.EXTRA_TOP_RADIUS
+import com.adealink.weparty.webview.callback.IWebViewCallback
+import com.adealink.weparty.webview.callback.IWebViewFragmentCallback
+import com.adealink.weparty.webview.component.ErrorComp
+import com.adealink.weparty.webview.constant.NATIVE_JS_BRIDGE
+import com.adealink.weparty.webview.constant.NavigationBarMode
+import com.adealink.weparty.webview.constant.TAG_WEB_FRAGMENT
+import com.adealink.weparty.webview.datasource.local.WebLocalService
+import com.adealink.weparty.webview.jsbridge.JSBridge
+import com.adealink.weparty.webview.jsbridge.data.NativeMessage
+import com.adealink.weparty.webview.jsbridge.data.NativeMessageType
+import com.adealink.weparty.webview.jsnativemethod.addCommonMethod
+import com.adealink.weparty.webview.payermax.SpWebChromeClient
+import com.adealink.weparty.webview.payermax.SpWebViewClient
+import com.ushareit.easysdk.web.util.SPWebHelper
+
+class WebViewFragment : BaseFragment(R.layout.fragment_webview),
+    IWebViewCallback {
+
+    @BindExtra(Web.Common.EXTRA_LOADING_URL, desc = "加载图标链接")
+    var loadingUrl: String? = null
+
+    @BindExtra(Web.Common.EXTRA_DIALOG_TAG, desc = "dialog的tag")
+    var fgTag: String? = ""
+
+    private val binding by viewBinding(FragmentWebviewBinding::bind)
+    private var url: String = ""
+
+    private var topRadius: Int = 0
+
+    private var errorComp: ErrorComp? = null
+
+    private var transparentBgWhenPageFinish = false
+    private var transparentBg = false
+
+    private var webFragmentCallback: IWebViewFragmentCallback? = null
+
+    private val webHelper by lazy { SPWebHelper() }
+    private var isPayerMaxPage = false
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        Router.bind(this)
+    }
+
+    override fun initViews() {
+        super.initViews()
+        val topRadius = arguments?.getInt(EXTRA_TOP_RADIUS) ?: 16.dp()
+        binding.webView.outlineProvider = object : ViewOutlineProvider() {
+            override fun getOutline(view: View, outline: Outline) {
+                val left = 0
+                val top = 0
+                val right = view.width
+                val bottom = view.height
+
+                //top corners
+                outline.setRoundRect(
+                    left,
+                    top,
+                    right,
+                    bottom + topRadius,
+                    topRadius.toFloat()
+                )
+            }
+        }
+        binding.webView.clipToOutline = true
+        binding.webView.webViewCallback = this
+
+        if (transparentBg) {
+            binding.root.background = null
+            binding.webView.setBackgroundColor(0)
+            binding.webView.background?.alpha = 0
+        }
+
+        initJsBridge()
+        initPayerMaxPageIfNeed()
+    }
+
+    private fun initJsBridge() {
+        binding.webView.getJSBridge(NATIVE_JS_BRIDGE)?.let { bridge ->
+            bridge.addCommonMethod(binding.webView)
+        }
+    }
+
+    private fun initPayerMaxPageIfNeed() {
+        val act = activity ?: return
+        var host: String? = null
+        if (url.isEmpty().not()) {
+            host = url.toUri().host
+        }
+        if (host?.contains("payermax") == true) {
+            isPayerMaxPage = true
+        }
+        val commonWebChromeClient = binding.webView.getCommonWebChromeClient()
+        val commonWebViewClient = binding.webView.getCommonWebViewClient()
+        val spWebChromeClient = SpWebChromeClient(webHelper, commonWebChromeClient)
+        val spWebViewClient = SpWebViewClient(act, binding.webView, commonWebViewClient)
+        webHelper.initWebView(
+            AppUtil.application,
+            act,
+            binding.webView,
+            spWebChromeClient,
+            spWebViewClient
+        )
+    }
+
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<out String>,
+        grantResults: IntArray
+    ) {
+        if (isPayerMaxPage) {
+            webHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
+        }
+    }
+
+    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        webHelper.onActivityResult(requestCode, resultCode, data)
+    }
+
+    override fun initComponents() {
+        super.initComponents()
+        errorComp =
+            ErrorComp(
+                this,
+                binding.webView,
+                binding.errorView,
+                reload = { loadUrl(url) }
+            ).apply { attach() }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        notifyResumeMessage()
+    }
+
+    private fun notifyResumeMessage() {
+        notifyNativeMessage(NativeMessage<Any>(NativeMessageType.MSG_ON_RESUME.msgType))
+    }
+
+    fun showUrl(url: String) {
+        this.url = url
+        if (isAdded) {
+            loadUrl(url)
+        }
+    }
+
+    fun replaceAndShow(fragmentManager: FragmentManager, contentId: Int, url: String) {
+        Log.d(TAG_WEB_FRAGMENT, "replaceAndShow, url:$url")
+        this.url = url
+        if (isAdded) {
+            loadUrl(url)
+            return
+        }
+        fragmentManager.beginTransaction()
+            .replace(contentId, this, "WebFragment")
+            .commit()
+        showUrl(url)
+    }
+
+    fun getWebView(): BaseWebView? {
+        if (!isAdded) {
+            return null
+        }
+        return binding.webView
+    }
+
+    fun setTopRadius(topRadius: Int) {
+        this.topRadius = topRadius
+    }
+
+    fun setTransparentBgWhenPageFinish() {
+        this.transparentBgWhenPageFinish = true
+    }
+
+    fun setTransparentBg() {
+        this.transparentBg = true
+    }
+
+    fun setWebFragmentCallback(callback: IWebViewFragmentCallback?) {
+        this.webFragmentCallback = callback
+    }
+
+    override fun loadData() {
+        super.loadData()
+        loadUrl(url)
+    }
+
+    private fun loadUrl(url: String) {
+        errorComp?.hideError()
+        binding.webView.loadUrl(url)
+    }
+
+    override val lifeCycleOwner: LifecycleOwner
+        get() = viewLifecycleOwner
+
+    override fun closeWebView() {
+        webFragmentCallback?.closeWebView()
+    }
+
+    override fun setTitle(title: String?) {
+        webFragmentCallback?.setTitle(title)
+    }
+
+    override fun onPageStarted(url: String?, favicon: Bitmap?) {
+        binding.progressIndicator.visibility = View.VISIBLE
+    }
+
+    override fun onPageFinished(url: String?) {
+        binding.progressIndicator.visibility = View.GONE
+        WebLocalService.initWebVersion()
+        if (transparentBgWhenPageFinish) {
+            binding.root.background = null
+            binding.webView.setBackgroundColor(0)
+            binding.webView.background?.alpha = 0
+        }
+    }
+
+    override fun onProgressChanged(progress: Int) {
+        binding.progressIndicator.setProgressCompat(progress, true)
+    }
+
+    override fun onReceivedError(url: String?, errorCode: Int, description: String?) {
+        errorComp?.showError("$errorCode:$description")
+    }
+
+    override fun onReceivedSslError(handler: SslErrorHandler?, error: SslError?) {
+        errorComp?.showError()
+    }
+
+    override fun isHostActive(): Boolean {
+        val hostActivity = activity ?: return false
+        return !hostActivity.isDestroyed && isAdded
+    }
+
+    override fun setImmersionMode() {
+        webFragmentCallback?.setImmersionMode()
+    }
+
+    override fun setNavigationBarMode(mode: NavigationBarMode) {
+        webFragmentCallback?.setNavigationBarMode(mode)
+    }
+
+    override fun hideNavigationBar() {
+        webFragmentCallback?.hideNavigationBar()
+    }
+
+    override fun <T> notifyNativeMessage(message: NativeMessage<T>) {
+        if (!isViewBindingValid()) {
+            return
+        }
+        binding.webView.notifyNativeMessage(message)
+    }
+
+    override fun getJsBridge(name: String): JSBridge? {
+        if (!isViewBindingValid()) {
+            return null
+        }
+        return binding.webView.getJSBridge(name)
+    }
+
+    override fun url(): String? {
+        return url
+    }
+
+    override fun onPageLoadSuccess() {
+
+    }
+
+    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+        if (!isViewBindingValid()) {
+            return false
+        }
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            if (onBackPressed()) {
+                return true
+            }
+        }
+        return false
+    }
+
+    private fun onBackPressed(): Boolean {
+        if (binding.webView.canGoBack()) {
+            binding.webView.goBack()
+            return true
+        }
+
+        return false
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        binding.webView.onDestroy()
+    }
+}

+ 51 - 0
app/src/main/java/com/adealink/weparty/webview/callback/IWebViewCallback.kt

@@ -0,0 +1,51 @@
+package com.adealink.weparty.webview.callback
+
+import android.app.Activity
+import android.graphics.Bitmap
+import android.net.http.SslError
+import android.webkit.SslErrorHandler
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.weparty.webview.constant.NavigationBarMode
+import com.adealink.weparty.webview.jsbridge.JSBridge
+import com.adealink.weparty.webview.jsbridge.data.NativeMessage
+
+interface IWebViewCallback {
+
+    val lifeCycleOwner: LifecycleOwner
+
+    fun closeWebView()
+
+    fun getActivity(): Activity?
+
+    fun setTitle(title: String?)
+
+    fun onPageStarted(url: String?, favicon: Bitmap?)
+
+    fun onPageFinished(url: String?)
+
+    fun onProgressChanged(progress: Int)
+
+    fun onReceivedError(url: String?, errorCode: Int, description: String?)
+
+    fun onReceivedSslError(handler: SslErrorHandler?, error: SslError?)
+
+    /**
+     * 宿主是否激活状态
+     */
+    fun isHostActive(): Boolean
+
+    fun setImmersionMode()
+
+    fun setNavigationBarMode(mode: NavigationBarMode)
+
+    fun hideNavigationBar()
+
+    fun <T> notifyNativeMessage(message: NativeMessage<T>)
+
+    fun getJsBridge(name: String): JSBridge?
+
+    fun url(): String?
+
+    // 需要桥接调用
+    fun onPageLoadSuccess()
+}

+ 15 - 0
app/src/main/java/com/adealink/weparty/webview/callback/IWebViewFragmentCallback.kt

@@ -0,0 +1,15 @@
+package com.adealink.weparty.webview.callback
+
+import com.adealink.weparty.webview.constant.NavigationBarMode
+
+interface IWebViewFragmentCallback {
+    fun closeWebView() {}
+
+    fun setTitle(title: String?) {}
+
+    fun setImmersionMode() {}
+
+    fun setNavigationBarMode(mode: NavigationBarMode) {}
+
+    fun hideNavigationBar() {}
+}

+ 48 - 0
app/src/main/java/com/adealink/weparty/webview/component/ErrorComp.kt

@@ -0,0 +1,48 @@
+package com.adealink.weparty.webview.component
+
+import android.view.View
+import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.mvvm.view.ViewComponent
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.show
+import com.adealink.weparty.commonui.widget.CommonEmptyErrorView
+
+/**
+ */
+class ErrorComp(
+    lifecycleOwner: LifecycleOwner,
+    val webView: com.adealink.weparty.webview.BaseWebView,
+    private val errorView: CommonEmptyErrorView,
+    val reload: () -> Unit,
+) : ViewComponent(lifecycleOwner) {
+
+    fun showError(msg: String? = null) {
+        webView.visibility = View.GONE
+        errorView.visibility = View.VISIBLE
+        errorView.show(
+            R.drawable.common_net_error_ic,
+            R.string.web_view_page_load_error_tip,
+            null,
+            R.string.common_retry,
+            {
+                reload()
+            })
+        errorMsg(msg)
+    }
+
+    fun hideError() {
+        webView.visibility = View.VISIBLE
+        errorView.visibility = View.GONE
+    }
+
+    private fun errorMsg(msg: String?) {
+        if(msg == null) {
+            errorView.binding.tvEmptyErrorSubTitle.gone()
+        }else {
+            errorView.binding.tvEmptyErrorSubTitle.show()
+            errorView.binding.tvEmptyErrorSubTitle.text = msg
+        }
+    }
+
+}

+ 4 - 0
app/src/main/java/com/adealink/weparty/webview/constant/Constant.kt

@@ -0,0 +1,4 @@
+package com.adealink.weparty.webview.constant
+
+const val NATIVE_JS_BRIDGE = "we_bridge"
+const val COCOS_GAME_JS_BRIDGE = "cocos_bridge"

+ 12 - 0
app/src/main/java/com/adealink/weparty/webview/constant/Data.kt

@@ -0,0 +1,12 @@
+package com.adealink.weparty.webview.constant
+
+enum class NavigationBarMode(val mode: Int) {
+    DARK(0),
+    LIGHT(1);
+
+    companion object {
+        fun map(mode: Int): NavigationBarMode {
+            return values().firstOrNull { it.mode == mode } ?: DARK
+        }
+    }
+}

+ 2 - 0
app/src/main/java/com/adealink/weparty/webview/constant/Error.kt

@@ -0,0 +1,2 @@
+package com.adealink.weparty.webview.constant
+

+ 15 - 0
app/src/main/java/com/adealink/weparty/webview/constant/Tags.kt

@@ -0,0 +1,15 @@
+package com.adealink.weparty.webview.constant
+
+const val TAG_WEB_VIEW = "tag_web_view"
+const val TAG_WEB_VIEW_JS_BRIDGE = "${TAG_WEB_VIEW}_js_bridge"
+const val TAG_WEB_VIEW_RES_LOAD = "${TAG_WEB_VIEW}_res_load"
+const val TAG_WEB_VIEW_LOAD_FLOW = "${TAG_WEB_VIEW}_load_flow"
+const val TAG_WEB_VIEW_WHITE_HOST = "${TAG_WEB_VIEW}_white_host"
+const val TAG_WEB_BS = "tag_web_bs"
+
+const val TAG_COCOS_GAME_WEB = "tag_cocos_web"
+const val TAG_COCOS_GAME_WEB_FLOW = "${TAG_COCOS_GAME_WEB}_flow"
+const val TAG_COCOS_GAME_WEB_NETWORK = "${TAG_COCOS_GAME_WEB}_network"
+const val TAG_COCOS_GAME_WEB_CALL = "${TAG_COCOS_GAME_WEB}_call"
+
+const val TAG_WEB_FRAGMENT = "tag_web_fragment"

+ 33 - 0
app/src/main/java/com/adealink/weparty/webview/datasource/local/WebLocalService.kt

@@ -0,0 +1,33 @@
+package com.adealink.weparty.webview.datasource.local
+
+import android.content.Context
+import com.adealink.frame.log.Log
+import com.adealink.frame.storage.sp.TypeDelegationPrefs
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.module.profile.ProfileModule
+import com.adealink.weparty.storage.PREF_WEB
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW
+
+
+object WebLocalService : TypeDelegationPrefs(
+    prefs = {
+        AppUtil.appContext.getSharedPreferences(PREF_WEB, Context.MODE_PRIVATE)
+    },
+    userId = {
+        ProfileModule.getMyUid()
+    }
+) {
+
+    var webVersion: String by PrefKey("key_web_version", "")
+
+    var offlineH5UrlsJson: String by PrefKey("key_offline_urls_json", "")
+
+    fun initWebVersion() {
+        Log.i(TAG_WEB_VIEW, "initWebVersion, webVersion:${webVersion}")
+        if (webVersion.isEmpty()) {
+            webVersion = "1.0.0"
+        }
+    }
+
+
+}

+ 15 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/JSBridge.kt

@@ -0,0 +1,15 @@
+package com.adealink.weparty.webview.jsbridge
+
+import com.adealink.weparty.webview.jsbridge.data.NativeMessage
+import com.adealink.weparty.webview.jsbridge.method.JSNativeMethod
+
+interface JSBridge {
+
+    fun interfaceName(): String
+
+    fun <T, R> addNativeMethod(method: JSNativeMethod<T, R>)
+
+    fun removeNativeMethod(methodName: String)
+
+    fun <T> notifyNativeMessage(message: NativeMessage<T>)
+}

+ 202 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/JSBridgeImpl.kt

@@ -0,0 +1,202 @@
+package com.adealink.weparty.webview.jsbridge
+
+import android.os.Build
+import android.text.TextUtils
+import android.webkit.JavascriptInterface
+import androidx.annotation.UiThread
+import com.adealink.frame.data.json.froJsonErrorNull
+import com.adealink.frame.data.json.toJsonErrorNull
+import com.adealink.frame.log.Log
+import com.adealink.frame.util.runOnUiThread
+import com.adealink.weparty.webview.IWebView
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW_JS_BRIDGE
+import com.adealink.weparty.webview.jsbridge.callback.JSBridgeCallback
+import com.adealink.weparty.webview.jsbridge.data.JSRequest
+import com.adealink.weparty.webview.jsbridge.data.JSResponse
+import com.adealink.weparty.webview.jsbridge.data.NativeMessage
+import com.adealink.weparty.webview.jsbridge.manager.JSBridgeManager
+import com.adealink.weparty.webview.jsbridge.method.JSNativeMethod
+
+
+class JSBridgeImpl(val webView: IWebView, private val interfaceName: String) : JSBridge {
+
+    private val methodMap = hashMapOf<String, JSNativeMethod<Any, Any>>() //key: method name
+
+    init {
+        addNativeMethod(BridgeSupportJSNativeMethod())
+    }
+
+    override fun interfaceName(): String {
+        return interfaceName
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    @UiThread
+    override fun <T, R> addNativeMethod(method: JSNativeMethod<T, R>) {
+        methodMap[method.methodName] = method as JSNativeMethod<Any, Any>
+    }
+
+    @UiThread
+    override fun removeNativeMethod(methodName: String) {
+        methodMap.remove(methodName)
+    }
+
+    override fun <T>notifyNativeMessage(message: NativeMessage<T>) {
+        val messageStr = toJsonErrorNull(message)
+        if (messageStr.isNullOrEmpty()) {
+            Log.e(TAG_WEB_VIEW_JS_BRIDGE,
+                "notifyNativeMessage, message is empty")
+            return
+        }
+        val script = "javascript:window.notifyMessage('$messageStr')"
+        runOnUiThread {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                webView.evaluateJavascript(script, null)
+            } else {
+                webView.loadUrl(script)
+            }
+        }
+    }
+
+    @JavascriptInterface
+    fun postMessageToNative(json: String?) {
+        runOnUiThread { handleJSMessage(json) }
+    }
+
+    @UiThread
+    private fun handleJSMessage(json: String?) {
+        Log.d(TAG_WEB_VIEW_JS_BRIDGE, "handleJSMessage, json:${json}")
+        val jsRequest = JSRequest.parseJson(json)
+        val callbackBridge = CallbackBridge(jsRequest.callbackId, jsRequest.methodName)
+        if (isDenied()) {
+            callbackBridge.reject(JSResponse.JSError.CLIENT_DENIED)
+            return
+        }
+
+        when (jsRequest.type) {
+            JSRequest.TYPE_INVOKE_METHOD -> doInvokeMethod(jsRequest, callbackBridge)
+            else -> callbackBridge.reject(JSResponse.JSError.UNKNOWN_REQUEST_TYPE)
+        }
+    }
+
+    /**
+     * 是否拒绝执行
+     */
+    private fun isDenied(): Boolean {
+        val url = webView.getUrl() ?: ""
+        val originalUrl = webView.getOriginalUrl() ?: ""
+        Log.d(TAG_WEB_VIEW_JS_BRIDGE, "isDenied, url:${url}, originalUrl:${originalUrl}")
+        return (!JSBridgeManager.instance.isWhite(url)
+                && !JSBridgeManager.instance.isWhite(originalUrl)
+                || JSBridgeManager.instance.isBlack(url)
+                || JSBridgeManager.instance.isBlack(originalUrl))
+    }
+
+    private fun doInvokeMethod(jsRequest: JSRequest, callbackBridge: CallbackBridge) {
+        val methodName = jsRequest.methodName
+        if (methodName.isEmpty()) {
+            callbackBridge.reject(JSResponse.JSError.REQUEST_METHOD_NAME_NULL)
+            return
+        }
+
+        val nativeMethod = methodMap[methodName]
+        if (nativeMethod == null) {
+            callbackBridge.reject(JSResponse.JSError.CLIENT_NO_REGISTER_HANDLE_METHOD)
+            return
+        }
+
+        val clazz = JSNativeMethod.classOfT(nativeMethod)
+        if (clazz == null) {
+            callbackBridge.reject(JSResponse.JSError.CLIENT_HANDLE_REQUEST_DATA_TYPE_ERROR)
+            return
+        }
+
+        val dataStr = jsRequest.data
+        if (clazz == Any::class.java) {
+            nativeMethod.handleMethodCall(Any(), callbackBridge)
+            return
+        }
+
+        val data =
+            if (clazz == Nothing::class.java || clazz == Void::class.java || dataStr.isNullOrEmpty()) {
+                null
+            } else if (clazz == String::class.java) {
+                dataStr
+            } else {
+                froJsonErrorNull(dataStr, clazz)
+            }
+
+        if (data == null) {
+            callbackBridge.reject(JSResponse.JSError.CLIENT_HANDLE_REQUEST_DATA_PARSE_ERROR)
+            return
+        }
+
+        nativeMethod.handleMethodCall(data, callbackBridge)
+    }
+
+    inner class CallbackBridge(override val callbackId: String, val methodName: String?) :
+        JSBridgeCallback<Any> {
+
+        private fun sendResponseToJS(response: JSResponse<Any>) {
+            if (TextUtils.isEmpty(callbackId)) {
+                Log.e(TAG_WEB_VIEW_JS_BRIDGE,
+                    "sendResponseToJS, methodName:${methodName}, response:${response}, callbackId is empty")
+                return
+            }
+
+            val responseStr = toJsonErrorNull(response)
+            if (responseStr.isNullOrEmpty()) {
+                Log.e(TAG_WEB_VIEW_JS_BRIDGE,
+                    "sendResponseToJS, methodName:${methodName}, response:${response}, to json error")
+                return
+            }
+
+            runOnUiThread { callJavascript("javascript:window.${callbackId}($responseStr)") }
+        }
+
+        private fun callJavascript(script: String) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+                webView.evaluateJavascript(script, null)
+            } else {
+                webView.loadUrl(script)
+            }
+        }
+
+        override fun resolve(data: Any) {
+            Log.d(TAG_WEB_VIEW_JS_BRIDGE,
+                "resolve, methodName:${methodName}, callbackId:${callbackId}, data:${data}")
+            sendResponseToJS(JSResponse(callbackId = this@CallbackBridge.callbackId, data = data))
+        }
+
+        override fun reject(error: JSResponse.JSError) {
+            Log.e(TAG_WEB_VIEW_JS_BRIDGE,
+                "reject, methodName:${methodName}, callbackId:${callbackId}, error:${error.message}")
+            sendResponseToJS(JSResponse(error.code, error.message, this@CallbackBridge.callbackId))
+        }
+
+        override fun reject(errorCode: Int, message: String?) {
+            Log.e(
+                TAG_WEB_VIEW_JS_BRIDGE,
+                "reject, methodName:${methodName}, callbackId:${callbackId}, error:${message ?: ""}"
+            )
+            sendResponseToJS(JSResponse(errorCode, message ?: "", this@CallbackBridge.callbackId))
+        }
+
+    }
+
+    inner class BridgeSupportJSNativeMethod: JSNativeMethod<Map<String,List<String>>, Map<String,Boolean>> {
+        override val methodName: String = "isBridgeSupport"
+
+        override fun handleMethodCall(data: Map<String,List<String>>, callback: JSBridgeCallback<Map<String,Boolean>>?) {
+            val jsNativeMethods = data["data"]
+
+            if(jsNativeMethods == null) {
+                callback?.reject(JSResponse.JSError.CLIENT_HANDLE_REQUEST_DATA_TYPE_ERROR)
+                return
+            }
+            val result =  jsNativeMethods.associateWith { key -> methodMap.containsKey(key) }
+            callback?.resolve(result)
+        }
+    }
+
+}

+ 15 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/callback/JSBridgeCallback.kt

@@ -0,0 +1,15 @@
+package com.adealink.weparty.webview.jsbridge.callback
+
+import com.adealink.weparty.webview.jsbridge.data.JSResponse
+
+
+interface JSBridgeCallback<T> {
+
+    val callbackId: String
+
+    fun resolve(data: T)
+
+    fun reject(error: JSResponse.JSError)
+
+    fun reject(errorCode: Int, message: String? = null) {}
+}

+ 13 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/config/IJSBridgeConfig.kt

@@ -0,0 +1,13 @@
+package com.adealink.weparty.webview.jsbridge.config
+
+interface IJSBridgeConfig {
+
+    fun addWhiteList(hosts: List<String>)
+
+    fun addBlackList(hosts: List<String>)
+
+    fun isWhite(url: String): Boolean
+
+    fun isBlack(url: String): Boolean
+
+}

+ 47 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/data/JSRequest.kt

@@ -0,0 +1,47 @@
+package com.adealink.weparty.webview.jsbridge.data
+
+import androidx.annotation.IntDef
+import org.json.JSONObject
+
+data class JSRequest(
+    @RequestType val type: Int = TYPE_UNKNOWN,
+    val methodName: String = "",
+    val callbackId: String = "",
+    var data: String? = null,
+) {
+
+    companion object {
+
+        private const val KET_CALLBACK_ID = "callback_id"
+        private const val KET_METHOD = "method"
+        private const val KET_TYPE = "type"
+        private const val KET_DATA = "data"
+
+        const val TYPE_UNKNOWN = 0
+        const val TYPE_INVOKE_METHOD = 1
+        const val TYPE_ADD_LISTENER = 2
+        const val TYPE_REMOVE_LISTENER = 3
+
+        @IntDef(TYPE_UNKNOWN, TYPE_INVOKE_METHOD, TYPE_ADD_LISTENER, TYPE_REMOVE_LISTENER)
+        internal annotation class RequestType
+
+        fun parseJson(json: String?): JSRequest {
+            if (json.isNullOrEmpty()) {
+                return JSRequest(TYPE_UNKNOWN, "", "", null)
+            }
+
+            return try {
+                val jsonObject = JSONObject(json)
+                val requestType = jsonObject.optInt(KET_TYPE, TYPE_UNKNOWN)
+                val methodName = jsonObject.optString(KET_METHOD, "")
+                val callbackId = jsonObject.optString(KET_CALLBACK_ID, "")
+                val dataJSONObject = jsonObject.optJSONObject(KET_DATA)
+                JSRequest(requestType, methodName, callbackId, dataJSONObject?.toString())
+            } catch (e: Exception) {
+                JSRequest(TYPE_UNKNOWN, "", "", null)
+            }
+        }
+    }
+
+
+}

+ 28 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/data/JSResponse.kt

@@ -0,0 +1,28 @@
+package com.adealink.weparty.webview.jsbridge.data
+
+import com.google.gson.annotations.SerializedName
+
+data class JSResponse<T>(
+    @SerializedName("code")
+    var code: Int = JSError.SUCCESS.code,
+    @SerializedName("message")
+    var message: String = JSError.SUCCESS.message,
+    @SerializedName("callback_id")
+    var callbackId: String = "",
+    @SerializedName("data")
+    var data: T? = null,
+) {
+
+    enum class JSError(val code: Int, val message: String) {
+        SUCCESS(0, "success"),
+        REQUEST_METHOD_NAME_NULL(1, "request method name is null"),
+        CLIENT_NO_REGISTER_HANDLE_METHOD(2, "client no register handle method"),
+        CLIENT_DENIED(3, "client denied"),
+        CLIENT_HANDLE_REQUEST_DATA_TYPE_ERROR(4, "client handle request data type error"),
+        CLIENT_HANDLE_REQUEST_DATA_PARSE_ERROR(5, "client handle request data parse error"),
+        UNKNOWN_REQUEST_TYPE(6, "unknown request type"),
+        CLIENT_UN_LOGIN(7, "client un login"),
+        CLIENT_ERROR(8, "client error")
+    }
+
+}

+ 19 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/data/NativeMessage.kt

@@ -0,0 +1,19 @@
+package com.adealink.weparty.webview.jsbridge.data
+
+import com.google.gson.annotations.SerializedName
+
+enum class NativeMessageType(val msgType: String) {
+    MSG_COUNTRY_SELECT("msg_country_select"),
+    MSG_REFRESH_PRODUCT("msg_refresh_product"),
+    MSG_REFRESH_MY_VIP("msg_refresh_my_vip"),
+    MSG_ON_RESUME("msg_on_resume"),
+    MSG_ROOM_SUPPORT_ADD_USER("msg_room_support_add_user"),
+    MSG_ROOM_SUPPORT_RANK_SELECT_DATE("msg_room_support_rank_select_date"),
+    RECEIVE_REWARD_RESULT("receive_reward_result"),
+}
+
+data class NativeMessage<T>(
+    @SerializedName("type")
+    var type: String,
+    @SerializedName("data")
+    var data: T? = null)

+ 13 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/manager/IJSBridgeManager.kt

@@ -0,0 +1,13 @@
+package com.adealink.weparty.webview.jsbridge.manager
+
+interface IJSBridgeManager {
+
+    fun addWhiteList(hosts: List<String>)
+
+    fun addBlackList(hosts: List<String>)
+
+    fun isWhite(url: String): Boolean
+
+    fun isBlack(url: String): Boolean
+
+}

+ 40 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/manager/JSBridgeManager.kt

@@ -0,0 +1,40 @@
+package com.adealink.weparty.webview.jsbridge.manager
+
+import com.adealink.weparty.webview.jsbridge.config.IJSBridgeConfig
+
+fun initJSBridgeManager(initiator: (() -> JSBridgeManager)) {
+    JSBridgeManager.initiator = initiator
+}
+
+@Synchronized
+fun createJSBridgeManager(config: IJSBridgeConfig): JSBridgeManager {
+    return JSBridgeManager(config)
+}
+
+class JSBridgeManager(val config: IJSBridgeConfig) : IJSBridgeManager {
+
+    companion object {
+
+        var initiator: (() -> JSBridgeManager)? = null
+
+        val instance: IJSBridgeManager by lazy { initiator!!() }
+
+    }
+
+    override fun addWhiteList(hosts: List<String>) {
+        config.addWhiteList(hosts)
+    }
+
+    override fun addBlackList(hosts: List<String>) {
+        config.addBlackList(hosts)
+    }
+
+    override fun isWhite(url: String): Boolean {
+        return config.isWhite(url)
+    }
+
+    override fun isBlack(url: String): Boolean {
+        return config.isBlack(url)
+    }
+
+}

+ 25 - 0
app/src/main/java/com/adealink/weparty/webview/jsbridge/method/JSNativeMethod.kt

@@ -0,0 +1,25 @@
+package com.adealink.weparty.webview.jsbridge.method
+
+import com.adealink.frame.util.getRawType
+import com.adealink.weparty.webview.jsbridge.callback.JSBridgeCallback
+import java.lang.reflect.ParameterizedType
+
+/**
+ * JSNativeMethod不要使用匿名内部类形式
+ */
+interface JSNativeMethod<ReqData, ResData> {
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+        fun <T : Any, R : Any> classOfT(data: JSNativeMethod<T, R>): Class<T>? {
+            val type = (data.javaClass.genericInterfaces[0] as? ParameterizedType)
+                ?.actualTypeArguments?.get(0)
+            return if (type == null) null else (getRawType(type) as? Class<T>)
+        }
+    }
+
+    val methodName: String
+
+    fun handleMethodCall(data: ReqData, callback: JSBridgeCallback<ResData>?)
+
+}

+ 7 - 0
app/src/main/java/com/adealink/weparty/webview/jsnativemethod/JsNativeMethodExt.kt

@@ -0,0 +1,7 @@
+package com.adealink.weparty.webview.jsnativemethod
+
+import com.adealink.weparty.webview.IWebView
+import com.adealink.weparty.webview.jsbridge.JSBridge
+
+fun JSBridge.addCommonMethod(webView: IWebView) {
+}

+ 72 - 0
app/src/main/java/com/adealink/weparty/webview/loader/IResourceLoader.kt

@@ -0,0 +1,72 @@
+package com.adealink.weparty.webview.loader
+
+import android.webkit.WebResourceResponse
+import com.adealink.weparty.webview.stat.WebViewStatEvent
+import java.util.*
+
+interface IResourceLoader {
+
+    companion object {
+        private val REPORT_HEADERS = setOf(
+            "pragme",//HTTP 1.0,只有 no-cache 这一个值
+            "cache-control",
+            "expires",//HTTP 1.0,缓存过期的时间点(服务端时间)
+            "last-modified",//资源最后更新的时间
+            "if-modified-since",//请求回传的缓存资源最后更新时间
+            "date",//请求时间
+            "etag",//资源计算得出一个唯一标志符
+            "if-none-match",//请求头回传的缓存ETag
+            "x-swift-cachetime",//CDN节点上的允许缓存时间,即该文件可以在CDN节点上缓存多久,是指文件在CDN节点缓存的总时间。计算还有多久需要回源刷新= ’X-Swift-CacheTime’ – ‘Age’
+            "x-swift-savetime",//CDN节点上的缓存RS(swift)的时间,即该文件是在什么时间缓存到CDN节点上
+            "x-cache", //HIT:表示已缓存, MISS:节点上无该文件的缓存,回源请求
+            "via",//请求经过的cdn节点
+            "age"//为CDN返回的头部字段,表示该文件在CDN节点上缓存的时间,单位为秒。只有文件存在于节点上Age字段才会出现,当文件被刷新后或者文件被清除的首次访问,在此前文件并未缓存,无Age头部字段,需要注意当Age为0时,表示节点已有文件的缓存,但由于缓存已过期,本次无法直接使用该缓存,需回源校验。
+        )
+    }
+
+    fun load(request: ResourceRequest): WebResourceResponse?
+
+    fun report(
+        loaderType: LoaderType,
+        url: String,
+        method: String,
+        cacheType: WebCacheType,
+        redirect: Boolean,
+        code: Int,
+        protocol: String,
+        reqHeaders: Map<String, String>,
+        resHeaders: Map<String, String>,
+        duration: Long //ms
+    ) {
+        WebViewStatEvent(WebViewStatEvent.Action.LOAD)
+            .apply {
+                uri to url
+                type to loaderType.type
+                this.method to method
+                this.code to code
+                this.cache to when (cacheType) {
+                    WebCacheType.NO_LOCAL_CACHE -> WebViewStatEvent.Cache.NO_CACHE
+                    WebCacheType.DIRECT_LOCAL_CACHE -> WebViewStatEvent.Cache.DIRECT_CACHE
+                    WebCacheType.REDIRECT_LOCAL_CACHE -> WebViewStatEvent.Cache.REDIRECT_CACHE
+                }
+                this.redirect to if (redirect) WebViewStatEvent.Redirect.TRUE else WebViewStatEvent.Redirect.FALSE
+                this.duration to duration
+                this.protocol to protocol
+                addEventMap(filterHeaders(reqHeaders))
+                addEventMap(filterHeaders(resHeaders))
+            }
+            .send()
+    }
+
+    fun filterHeaders(map: Map<String, String>): Map<String, String> {
+        val filterHeaders = hashMapOf<String, String>()
+        map.entries.forEach {
+            val lowerKey = it.key.toLowerCase(Locale.ROOT)
+            if (REPORT_HEADERS.contains(lowerKey)) {
+                filterHeaders[lowerKey.replace("-", "_")] = it.value
+            }
+        }
+        return filterHeaders
+    }
+
+}

+ 29 - 0
app/src/main/java/com/adealink/weparty/webview/loader/IWebResourceConfig.kt

@@ -0,0 +1,29 @@
+package com.adealink.weparty.webview.loader
+
+interface IWebResourceConfig {
+
+    val timeoutMS: Long
+
+    val cachePath: String?
+
+    val cacheSizeKB: Long
+
+    val loaderType: LoaderType
+
+    val reportHosts: Set<String>
+
+}
+
+class DefaultWebResourceConfig : IWebResourceConfig {
+
+    override val timeoutMS: Long = 60_000L
+
+    override val cachePath: String? = null
+
+    override val cacheSizeKB: Long = 20 * 1024 //20M
+
+    override val loaderType: LoaderType = LoaderType.OKHTTP
+
+    override val reportHosts: Set<String> = setOf()
+
+}

+ 8 - 0
app/src/main/java/com/adealink/weparty/webview/loader/LoaderData.kt

@@ -0,0 +1,8 @@
+package com.adealink.weparty.webview.loader
+
+
+enum class WebCacheType(val cache: Int) {
+    NO_LOCAL_CACHE(0), //不使用本地缓存,从网络加载
+    DIRECT_LOCAL_CACHE(1), //直接使用本地缓存
+    REDIRECT_LOCAL_CACHE(2), //重定向本地缓存
+}

+ 136 - 0
app/src/main/java/com/adealink/weparty/webview/loader/OkHttpResourceLoader.kt

@@ -0,0 +1,136 @@
+package com.adealink.weparty.webview.loader
+
+import android.os.Build
+import android.os.SystemClock
+import android.webkit.WebResourceResponse
+import com.adealink.frame.log.Log
+import com.adealink.frame.util.getHost
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW_RES_LOAD
+import com.adealink.weparty.webview.jsbridge.manager.JSBridgeManager
+import com.adealink.weparty.webview.stat.WebViewStatEvent
+import com.adealink.weparty.webview.util.getCharsetFromHeaders
+import com.adealink.weparty.webview.util.getMimeTypeFromHeader
+import com.adealink.weparty.webview.util.getMimeTypeFromUrl
+import okhttp3.Cache
+import okhttp3.Headers
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import java.io.File
+import java.net.HttpURLConnection.HTTP_NOT_MODIFIED
+import java.util.concurrent.TimeUnit
+
+class OkHttpResourceLoader(val config: IWebResourceConfig) : IResourceLoader {
+
+    private val okHttpClient by lazy {
+        val builder = OkHttpClient.Builder()
+            .connectTimeout(20_000L, TimeUnit.MILLISECONDS)
+            .readTimeout(config.timeoutMS, TimeUnit.MILLISECONDS)
+            .writeTimeout(0, TimeUnit.MILLISECONDS)
+        val cachePath = config.cachePath
+        if (cachePath != null) {
+            builder.cache(Cache(File(cachePath), config.cacheSizeKB * 1024))
+        }
+        builder.build()
+    }
+
+    override fun load(request: ResourceRequest): WebResourceResponse? {
+        var response: WebResourceResponse? = null
+        val method = request.method
+        if (method == "POST") {
+            return response
+        }
+
+        val url = request.url
+        if (!JSBridgeManager.instance.isWhite(url)) {
+            return response
+        }
+
+        try {
+            val startTime = SystemClock.elapsedRealtime()
+            val builder = Headers.Builder()
+            request.headers.forEach { (key, value) ->
+                builder.add(key, value)
+            }
+            val httpRequest = Request.Builder()
+                .url(url)
+                .method(method, null)
+                .headers(builder.build())
+                .build()
+            val res = okHttpClient.newCall(httpRequest).execute()
+            val body = res.body
+            var mimeType = body?.contentType()?.run {
+                "${type}/${subtype}"
+            }
+            if (mimeType == null) {
+                mimeType = getMimeTypeFromHeader(res.headers)
+            }
+            if (mimeType == null) {
+                mimeType = getMimeTypeFromUrl(url)
+            }
+            var responseHeaders = res.headers.toMap()
+            response = WebResourceResponse(
+                mimeType,
+                getCharsetFromHeaders(res.headers),
+                res.body?.byteStream()
+            ).apply {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                    this.responseHeaders = responseHeaders
+                }
+            }
+            var requestHeaders = res.request.headers
+            var protocol = res.protocol
+            var resCode = res.code
+            var cacheType = WebCacheType.NO_LOCAL_CACHE
+            val cacheResponse = res.cacheResponse
+            val networkResponse = res.networkResponse
+            if (cacheResponse != null && networkResponse == null) {
+                requestHeaders = cacheResponse.request.headers
+                protocol = cacheResponse.protocol
+                resCode = cacheResponse.code
+                cacheType = WebCacheType.DIRECT_LOCAL_CACHE
+                responseHeaders = cacheResponse.headers.toMap()
+            }
+            if (networkResponse != null) {
+                requestHeaders = networkResponse.request.headers
+                protocol = networkResponse.protocol
+                resCode = networkResponse.code
+                responseHeaders = networkResponse.headers.toMap()
+                if (networkResponse.code == HTTP_NOT_MODIFIED) {
+                    cacheType = WebCacheType.REDIRECT_LOCAL_CACHE
+                }
+            }
+            val duration = SystemClock.elapsedRealtime() - startTime
+            Log.d(
+                TAG_WEB_VIEW_RES_LOAD,
+                "okhttp load, url:$url, method:${method}, redirect:${request.isRedirect}, forMainFrame:${request.isForMainFrame}, requestHeaders:${requestHeaders.toMap()}, protocol:${res.protocol}, responseCode:${resCode}, cacheType:${cacheType.cache}, responseHeaders:${responseHeaders}, duration:$duration"
+            )
+            if (config.reportHosts.contains(getHost(url))) {
+                report(
+                    LoaderType.OKHTTP,
+                    url,
+                    method,
+                    cacheType,
+                    request.isRedirect,
+                    resCode,
+                    protocol.toString(),
+                    requestHeaders.toMap(),
+                    responseHeaders,
+                    duration
+                )
+            }
+        } catch (e: Throwable) {
+            Log.e(TAG_WEB_VIEW_RES_LOAD, "okhttp load, url:$url, e:$e")
+            WebViewStatEvent(WebViewStatEvent.Action.LOAD)
+                .apply {
+                    uri to url
+                    type to LoaderType.OKHTTP.type
+                    this.method to method
+                    this.error to e.localizedMessage
+                    addEventMap(filterHeaders(request.headers))
+                }
+                .send()
+        }
+        return response
+    }
+
+}

+ 9 - 0
app/src/main/java/com/adealink/weparty/webview/loader/ResourceRequest.kt

@@ -0,0 +1,9 @@
+package com.adealink.weparty.webview.loader
+
+data class ResourceRequest(
+    val url: String,
+    val method: String,
+    val headers: Map<String, String>,
+    val isForMainFrame: Boolean,
+    val isRedirect: Boolean
+)

+ 46 - 0
app/src/main/java/com/adealink/weparty/webview/loader/WebResourceLoader.kt

@@ -0,0 +1,46 @@
+package com.adealink.weparty.webview.loader
+
+import android.webkit.WebResourceResponse
+import com.adealink.frame.log.Log
+import com.adealink.weparty.webview.constant.TAG_WEB_VIEW_RES_LOAD
+
+fun initWebResourceLoader(config: IWebResourceConfig) {
+    WebResourceLoader.config = config
+}
+
+val webResourceLoader: IResourceLoader by lazy { WebResourceLoader() }
+
+enum class LoaderType(val type: Int) {
+    NORMAL(0),
+    OKHTTP(1);
+
+    companion object {
+        fun map(type: Int): LoaderType {
+            return values().firstOrNull { it.type == type } ?: NORMAL
+        }
+    }
+}
+
+internal class WebResourceLoader : IResourceLoader {
+
+    companion object {
+        internal var config: IWebResourceConfig = DefaultWebResourceConfig()
+    }
+
+    private val okHttpResourceLoader by lazy { OkHttpResourceLoader(config) }
+
+    override fun load(request: ResourceRequest): WebResourceResponse? {
+
+        return when (config.loaderType) {
+            LoaderType.NORMAL -> {
+                Log.d(
+                    TAG_WEB_VIEW_RES_LOAD,
+                    "normal load, url:${request.url}, method:${request.method}, redirect:${request.isRedirect}, forMainFrame:${request.isForMainFrame}, requestHeaders:${request.headers}"
+                )
+                null
+            }
+            LoaderType.OKHTTP -> okHttpResourceLoader.load(request)
+        }
+    }
+
+}

+ 15 - 0
app/src/main/java/com/adealink/weparty/webview/payermax/PayerMaxHelper.kt

@@ -0,0 +1,15 @@
+package com.adealink.weparty.webview.payermax
+
+import android.app.Application
+import com.ushareit.easysdk.entry.SPEasySDK
+
+/**
+ * PayerMax前端优化工具
+ */
+object PayerMaxHelper {
+    private const val MERCHANT_ID = "SP20288235"
+
+    fun init(application: Application) {
+        SPEasySDK.init(application, MERCHANT_ID)
+    }
+}

+ 34 - 0
app/src/main/java/com/adealink/weparty/webview/payermax/SpWebChromeClient.kt

@@ -0,0 +1,34 @@
+package com.adealink.weparty.webview.payermax
+
+import android.net.Uri
+import android.webkit.ValueCallback
+import android.webkit.WebView
+import com.adealink.weparty.webview.CommonWebChromeClient
+import com.ushareit.easysdk.web.util.SPWebHelper
+import com.ushareit.easysdk.web.view.SPBaseChromeClient
+
+class SpWebChromeClient(
+    webHelper: SPWebHelper,
+    private val webChromeClient: CommonWebChromeClient?
+) : SPBaseChromeClient(webHelper) {
+
+    override fun onReceivedTitle(view: WebView?, title: String?) {
+        super.onReceivedTitle(view, title)
+        webChromeClient?.onReceivedTitle(view, title)
+    }
+
+    override fun onProgressChanged(view: WebView?, newProgress: Int) {
+        super.onProgressChanged(view, newProgress)
+        webChromeClient?.onProgressChanged(view, newProgress)
+    }
+
+    override fun onShowFileChooser(
+        webView: WebView?,
+        filePathCallback: ValueCallback<Array<Uri>>?,
+        fileChooserParams: FileChooserParams?
+    ): Boolean {
+        return webChromeClient?.onShowFileChooser(webView, filePathCallback, fileChooserParams)
+            ?: super.onShowFileChooser(webView, filePathCallback, fileChooserParams)
+    }
+
+}

+ 105 - 0
app/src/main/java/com/adealink/weparty/webview/payermax/SpWebViewClient.kt

@@ -0,0 +1,105 @@
+package com.adealink.weparty.webview.payermax
+
+import android.app.Activity
+import android.graphics.Bitmap
+import android.net.http.SslError
+import android.os.Build
+import android.webkit.SslErrorHandler
+import android.webkit.WebResourceError
+import android.webkit.WebResourceRequest
+import android.webkit.WebResourceResponse
+import android.webkit.WebView
+import androidx.annotation.RequiresApi
+import androidx.fragment.app.FragmentActivity
+import com.adealink.frame.util.AppUtil
+import com.adealink.weparty.webview.CommonWebViewClient
+import com.adealink.weparty.webview.IWebView
+import com.adealink.weparty.webview.loader.ResourceRequest
+import com.adealink.weparty.webview.loader.webResourceLoader
+import com.adealink.weparty.webview.util.SSLErrorHandleUtil
+import com.ushareit.easysdk.web.view.SPBaseWebViewClient
+
+class SpWebViewClient(
+    activity: Activity,
+    private val iWebView: IWebView,
+    private val webViewClient: CommonWebViewClient?
+) : SPBaseWebViewClient(activity, iWebView.getWebView()) {
+    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
+        super.onPageStarted(view, url, favicon)
+        webViewClient?.onPageStarted(view, url, favicon)
+    }
+
+    override fun onPageFinished(view: WebView?, url: String?) {
+        super.onPageFinished(view, url)
+        webViewClient?.onPageFinished(view, url)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.M)
+    override fun onReceivedError(
+        view: WebView?,
+        request: WebResourceRequest?,
+        error: WebResourceError?,
+    ) {
+        super.onReceivedError(view, request, error)
+        webViewClient?.onReceivedError(view, request, error)
+    }
+
+    override fun onReceivedError(
+        view: WebView?,
+        errorCode: Int,
+        description: String?,
+        failingUrl: String?,
+    ) {
+        super.onReceivedError(view, errorCode, description, failingUrl)
+        webViewClient?.onReceivedError(view, errorCode, description, failingUrl)
+    }
+
+    override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
+        val activity = AppUtil.currentActivity as? FragmentActivity
+        activity?.supportFragmentManager?.let {
+            SSLErrorHandleUtil.createSSLErrorHandleDialog(view?.url, onPositive = {
+                handler?.proceed()
+            }, onNegative = {
+                super.onReceivedSslError(view, handler, error)
+                webViewClient?.onReceivedSslError(view, handler, error)
+                false
+            })?.show(it)
+        }
+    }
+
+    override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
+        return webViewClient?.shouldOverrideUrlLoading(view, url) ?: super.shouldOverrideUrlLoading(
+            view,
+            url
+        )
+    }
+
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    override fun shouldInterceptRequest(
+        view: WebView,
+        request: WebResourceRequest
+    ): WebResourceResponse? {
+        val response = super.shouldInterceptRequest(view, request)
+        if (response != null) {
+            return response
+        }
+
+        val isRedirect = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            request.isRedirect
+        } else {
+            false
+        }
+        return webResourceLoader.load(
+            ResourceRequest(
+                request.url.toString(),
+                request.method,
+                request.requestHeaders,
+                request.isForMainFrame,
+                isRedirect
+            )
+        )
+    }
+
+}
+
+

+ 55 - 0
app/src/main/java/com/adealink/weparty/webview/stat/WebViewStatEvent.kt

@@ -0,0 +1,55 @@
+package com.adealink.weparty.webview.stat
+
+import com.adealink.frame.statistics.BaseStatEvent
+import com.adealink.frame.statistics.CommonEventKey
+import com.adealink.frame.statistics.IEventValue
+import com.adealink.frame.statistics.StatClientError
+import org.json.JSONObject
+
+class WebViewStatEvent(override val action: IEventValue) : BaseStatEvent("web_view") {
+
+    enum class Action(override val value: String, override val desc: String) : IEventValue {
+        ERROR("error", "发生错误"),
+        CLEAR_CACHE("clear_cache", "清理缓存"),
+        LOAD("load", "加载"),
+    }
+
+    enum class Redirect(override val value: String, override val desc: String) : IEventValue {
+        FALSE("0", "不是重定向"),
+        TRUE("1", "是重定向"),
+    }
+
+    enum class Cache(override val value: String, override val desc: String) : IEventValue {
+        NO_CACHE("0", "未用缓存"),
+        DIRECT_CACHE("1", "直接使用本地缓存"),
+        REDIRECT_CACHE("2", "重定向本地缓存"),
+    }
+
+    val page = Param(CommonEventKey.PAGE)
+    val uri = Param(CommonEventKey.URI)
+    val code = Param(CommonEventKey.CODE)
+    val error = Param(CommonEventKey.ERROR)
+    val result = Param(CommonEventKey.RESULT)
+    val lastVersion = Param(CommonEventKey.LAST_VERSION)
+    val newVersion = Param(CommonEventKey.NEW_VERSION)
+    val type = Param(CommonEventKey.TYPE)
+    val method = Param(CommonEventKey.METHOD)
+    val cache = Param(CommonEventKey.CACHE)
+    val redirect = Param(CommonEventKey.REDIRECT)
+    val duration = Param(CommonEventKey.DURATION)
+    var protocol = Param(CommonEventKey.PROTOCOL)
+    val host = Param(CommonEventKey.HOST)
+    var ua = Param(CommonEventKey.UA)
+
+    override fun clientErrorInfo(): StatClientError? {
+        if (action != Action.ERROR) {
+            return null
+        }
+        val uri = uri.value?.toString() ?: return null
+        val host = host.value?.toString() ?: ""
+        val jsonObj = JSONObject()
+        jsonObj.put("code", code.value)
+        jsonObj.put("error", error.value)
+        return StatClientError(host + uri, jsonObj.toString())
+    }
+}

+ 40 - 0
app/src/main/java/com/adealink/weparty/webview/util/SSLErrorHandleUtil.kt

@@ -0,0 +1,40 @@
+package com.adealink.weparty.webview.util
+
+import com.adealink.frame.aab.util.getCompatString
+import com.adealink.weparty.R
+import com.adealink.weparty.commonui.widget.CommonDialog
+
+object SSLErrorHandleUtil {
+
+    /**
+     * 上一次取消处理的Url
+     */
+    private var lastNegativeUrl: String? = null
+
+    fun createSSLErrorHandleDialog(url: String? = null, onPositive: () -> Unit, onNegative: () -> Boolean): CommonDialog? {
+        url ?: return null
+        //如果上一次没有允许访问的Url与这一次相同,则保持取消处理
+        if (lastNegativeUrl == url) {
+            if (onNegative.invoke()) {
+                //可控是否清除
+                lastNegativeUrl = null
+            }
+            return null
+        }
+        return CommonDialog.Builder()
+            .title(getCompatString(R.string.common_error))
+            .message(getCompatString(R.string.common_ssl_error_tip))
+            .canceledOnTouchOutside(false)
+            .negativeText(getCompatString(com.adealink.weparty.R.string.commonui_cancel))
+            .positiveText(getCompatString(com.adealink.weparty.R.string.commonui_confirm))
+            .onPositive {
+                onPositive.invoke()
+            }.onNegative {
+                lastNegativeUrl = url
+                if (onNegative.invoke()) {
+                    //可控是否清除
+                    lastNegativeUrl = null
+                }
+            }.build()
+    }
+}

+ 61 - 0
app/src/main/java/com/adealink/weparty/webview/util/WebUtil.kt

@@ -0,0 +1,61 @@
+package com.adealink.weparty.webview.util
+
+import android.net.Uri
+import android.webkit.MimeTypeMap
+import com.adealink.frame.ext.runSafely
+import okhttp3.Headers
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import java.nio.charset.Charset
+
+
+fun getCharsetFromHeaders(
+    headers: Headers,
+    default: String = Charset.defaultCharset().name()
+): String = runSafely(default) {
+    val contentType = headers["Content-Type"] ?: return@runSafely default
+    val mediaType = contentType.toMediaTypeOrNull() ?: return@runSafely default
+    return@runSafely mediaType.charset()?.toString() ?: default
+}
+
+@Suppress("RemoveExplicitTypeArguments")
+fun getMimeTypeFromHeader(headers: Headers?) = runSafely<String?>(null) {
+    val contentType = headers?.get("Content-Type") ?: return@runSafely null
+    val mediaType = contentType.toMediaTypeOrNull() ?: return@runSafely null
+    return@runSafely "${mediaType.type}/${mediaType.subtype}"
+}
+
+fun getMimeTypeFromUrl(_url: String, default: String = "text/html"): String {
+    // 强行适配前端2倍图命名,png文件带@
+    val url = _url.replace("@", "_")
+    var mimeType: String? = null
+    runSafely {
+        val fileExtension = MimeTypeMap.getFileExtensionFromUrl(url)
+        mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension)
+    }
+    if (mimeType == null) {
+        mimeType = guessMimeFromUrl(url, default)
+    }
+    return mimeType!!
+}
+
+private fun guessMimeFromUrl(url: String, default: String): String = runSafely(default) {
+    var mime = default
+    val currentUri = Uri.parse(url)
+    val path = currentUri.path
+    path?.let {
+        if (it.endsWith(".css")) {
+            mime = "text/css"
+        } else if (it.endsWith(".js")) {
+            mime = "application/javascript"
+        } else if (it.endsWith(".jpg") || it.endsWith(".gif") ||
+            it.endsWith(".png") || it.endsWith(".jpeg") ||
+            it.endsWith(".webp") || it.endsWith(".bmp") ||
+            it.endsWith(".ico")
+        ) {
+            mime = "image/*"
+        } else if (it.endsWith(".json")) {
+            mime = "application/json"
+        }
+    }
+    return mime
+}

+ 25 - 0
app/src/main/res/layout/activity_webview.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.adealink.weparty.commonui.widget.CommonTopBar
+        android:id="@+id/top_bar"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/common_top_bar_height"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/v_webview"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/top_bar" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 15 - 0
app/src/main/res/layout/dialog_webview.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/v_webview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 48 - 0
app/src/main/res/layout/fragment_webview.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/cl_main"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.adealink.weparty.commonui.widget.nestedscrolling.NestedScrollableHost
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <com.adealink.weparty.webview.WeNextWebView
+            android:id="@+id/web_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+    </com.adealink.weparty.commonui.widget.nestedscrolling.NestedScrollableHost>
+
+    <com.google.android.material.progressindicator.LinearProgressIndicator
+        android:id="@+id/progress_indicator"
+        android:layout_width="0dp"
+        android:layout_height="20dp"
+        android:layout_marginHorizontal="40dp"
+        android:visibility="gone"
+        app:indicatorColor="@color/color_app_main"
+        app:indicatorDirectionLinear="startToEnd"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:trackColor="@color/color_AAAAAA"
+        app:trackCornerRadius="8dp" />
+
+    <com.adealink.weparty.commonui.widget.CommonEmptyErrorView
+        android:id="@+id/error_view"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 6 - 9
app/src/main/res/values/colors.xml

@@ -5,15 +5,12 @@
     <color name="color_C2C2C5">#C2C2C5</color>
 
     <!--app品牌色-->
-    <color name="color_app_main">@color/color_FFFF50AE</color>
-    <color name="color_app_main_pressed">#FFE651A1</color>
-    <color name="color_app_main_disable">#99FF50AE</color>
-
-    <color name="color_app_main_1a">#1A00E0D7</color>
-    <color name="color_app_main_26">#2600E0D7</color>
-    <color name="color_app_main_4d">#4D00E0D7</color>
-    <color name="color_app_main_99">#9900E0D7</color>
-    <color name="color_app_main_btn_text">#FF00D0BE</color>
+    <color name="color_app_main">#2F95FF</color>
+    <color name="color_app_main_1a">#1A50FFD8</color>
+    <color name="color_app_main_26">#2650FFD8</color>
+    <color name="color_app_main_4d">#4D50FFD8</color>
+    <color name="color_app_main_99">#9950FFD8</color>
+    <color name="color_app_main_btn_text">@color/color_FF1D2129</color>
 
     <!--常用色值-->
     <color name="color_border">#FFE1E3E6</color>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -265,4 +265,5 @@
     <string name="common_choose_date">Choose Date</string>
     <string name="common_just_now">刚刚</string>
     <string name="common_help_center">Help Center</string>
+    <string name="web_view_page_load_error_tip">The page failed to load, please try again</string>
 </resources>

+ 26 - 21
module/account/src/main/java/com/adealink/weparty/account/login/LoginDialog.kt

@@ -44,6 +44,8 @@ import com.adealink.weparty.commonui.widget.ColorClickSpan
 import com.adealink.weparty.debug.Debug
 import com.adealink.weparty.debug.isTestEnv
 import com.adealink.weparty.module.account.Account
+import com.adealink.weparty.module.webview.Web
+import com.adealink.weparty.url.UrlConfig
 import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
@@ -196,13 +198,14 @@ class LoginDialog : BaseDialogFragment(R.layout.dialog_login) {
                             object : ColorClickSpan(APP_R.color.color_FF55CCFF) {
                                 override fun onClick(widget: View) {
                                     super.onClick(widget)
-                                    // TODO: 用户隐私协议
-//                                    Router.build(this@LoginActivity, Web.FullScreen.PATH)
-//                                        .putExtra(
-//                                            Web.Common.EXTRA_URL,
-//                                            urlConfigService.getH5Url(H5Page.SERVICE_TERMS)
-//                                        )
-//                                        .start()
+                                    activity?.let { act ->
+                                        Router.build(act, Web.FullScreen.PATH)
+                                            .putExtra(
+                                                Web.Common.EXTRA_URL,
+                                                UrlConfig.serviceTerms
+                                            )
+                                            .start()
+                                    }
                                 }
                             },
                             0,
@@ -220,13 +223,14 @@ class LoginDialog : BaseDialogFragment(R.layout.dialog_login) {
                             object : ColorClickSpan(APP_R.color.color_FF55CCFF) {
                                 override fun onClick(widget: View) {
                                     super.onClick(widget)
-                                    // TODO: 用户隐私协议
-//                                    Router.build(this@LoginActivity, Web.FullScreen.PATH)
-//                                        .putExtra(
-//                                            Web.Common.EXTRA_URL,
-//                                            urlConfigService.getH5Url(H5Page.PRIVACY_POLICY)
-//                                        )
-//                                        .start()
+                                    activity?.let { act ->
+                                        Router.build(act, Web.FullScreen.PATH)
+                                            .putExtra(
+                                                Web.Common.EXTRA_URL,
+                                                UrlConfig.privacyPolicy
+                                            )
+                                            .start()
+                                    }
                                 }
                             },
                             0,
@@ -244,13 +248,14 @@ class LoginDialog : BaseDialogFragment(R.layout.dialog_login) {
                             object : ColorClickSpan(APP_R.color.color_FF55CCFF) {
                                 override fun onClick(widget: View) {
                                     super.onClick(widget)
-                                    // TODO: 用户安全协议
-//                                    Router.build(this@LoginActivity, Web.FullScreen.PATH)
-//                                        .putExtra(
-//                                            Web.Common.EXTRA_URL,
-//                                            urlConfigService.getH5Url(H5Page.CHILD_SAFETY_POLICY)
-//                                        )
-//                                        .start()
+                                    activity?.let { act ->
+                                        Router.build(act, Web.FullScreen.PATH)
+                                            .putExtra(
+                                                Web.Common.EXTRA_URL,
+                                                UrlConfig.childSafetyPolicy
+                                            )
+                                            .start()
+                                    }
                                 }
                             },
                             0,

+ 0 - 3
module/image/src/main/java/com/adealink/weparty/image/data/Tags.kt

@@ -1,8 +1,5 @@
 package com.adealink.weparty.image.data
 
-/**
- * Created by sunxiaodong on 2022/7/19.
- */
 const val TAG_VIDEO = "tag_video"
 const val TAG_VIDEO_DOWNLOAD = "${TAG_VIDEO}_download"
 const val TAG_IMAGE_PREVIEW = "tag_image_preview"

+ 0 - 3
module/image/src/main/java/com/adealink/weparty/image/preview/VideoPreviewActivity.kt

@@ -16,9 +16,6 @@ import com.adealink.weparty.module.image.data.VideoItem
 import com.qmuiteam.qmui.widget.util.QMUIStatusBarHelper
 import com.adealink.weparty.R as APP_R
 
-/**
- * Created by sunxiaodong on 2022/7/15.
- */
 @RouterUri(path = [Video.Preview.PATH], desc = "视频预览页")
 class VideoPreviewActivity : BaseActivity() {
 

+ 0 - 3
module/image/src/main/java/com/adealink/weparty/image/preview/VideoPreviewFragment.kt

@@ -30,9 +30,6 @@ import com.adealink.weparty.module.image.data.VideoItem
 import com.adealink.weparty.permission.PermissionUtils
 import com.adealink.weparty.R as APP_R
 
-/**
- * Created by sunxiaodong on 2022/7/15.
- */
 class VideoPreviewFragment : BaseFragment(R.layout.fragment_video_preview) {
 
     companion object {

+ 0 - 3
module/image/src/main/java/com/adealink/weparty/image/viewmodel/VideoViewModel.kt

@@ -18,9 +18,6 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlin.coroutines.resume
 
-/**
- * Created by sunxiaodong on 2022/7/19.
- */
 class VideoViewModel : BaseViewModel() {
 
     fun downloadVideo(url: String): LiveData<Rlt<WeFile>> {

+ 22 - 4
module/setting/src/main/java/com/adealink/weparty/setting/about/AboutActivity.kt

@@ -13,8 +13,11 @@ import com.adealink.frame.util.onClick
 import com.adealink.frame.util.statusBarHeight
 import com.adealink.weparty.commonui.BaseActivity
 import com.adealink.weparty.module.setting.Setting
+import com.adealink.weparty.module.webview.Web
 import com.adealink.weparty.setting.R
 import com.adealink.weparty.setting.databinding.ActivityAboutBinding
+import com.adealink.weparty.update.updateManager
+import com.adealink.weparty.url.UrlConfig
 import com.adealink.weparty.R as APP_R
 
 @RouterUri(
@@ -57,19 +60,34 @@ class AboutActivity : BaseActivity() {
     }
 
     private fun goPrivacyPolicy() {
-
+        Router.build(this, Web.FullScreen.PATH)
+            .putExtra(
+                Web.Common.EXTRA_URL,
+                UrlConfig.privacyPolicy
+            )
+            .start()
     }
 
     private fun goTermsService() {
-
+        Router.build(this, Web.FullScreen.PATH)
+            .putExtra(
+                Web.Common.EXTRA_URL,
+                UrlConfig.serviceTerms
+            )
+            .start()
     }
 
     private fun goCommunityCovenant() {
-
+        Router.build(this, Web.FullScreen.PATH)
+            .putExtra(
+                Web.Common.EXTRA_URL,
+                UrlConfig.childSafetyPolicy
+            )
+            .start()
     }
 
     private fun checkVersionUpdate() {
-
+        updateManager.checkUpdate(this)
     }
 
 }

+ 0 - 1
native/wenext_jni/src/main/cpp/wenext_jni.c

@@ -1,5 +1,4 @@
 //
-// Created by sunxiaodong on 2022/11/22.
 //
 
 #include <string.h>