catalyst-core 0.0.3-canary.9 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/bin/catalyst.js +28 -15
- package/changelog.md +139 -30
- package/dist/native/androidProject/app/build.gradle.kts +125 -15
- package/dist/native/androidProject/app/proguard-rules.pro +85 -14
- package/dist/native/androidProject/app/src/debug/res/xml/network_security_config.xml +8 -0
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +10 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +552 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +109 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +605 -124
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +405 -47
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +341 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +954 -71
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +111 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/URLWhitelistManager.kt +231 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/Utils.kt +49 -55
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +81 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/design/DesignTokens.kt +307 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/EmulatorDetector.kt +203 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +187 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertHandler.kt +170 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityAlertUI.kt +366 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityBottomSheet.kt +138 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckManager.kt +511 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/SecurityCheckScheduler.kt +68 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +47 -11
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DeviceInfoUtils.kt +38 -13
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/DownloadUtils.kt +36 -14
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FileUtils.kt +3 -2
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +57 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/IntentUtils.kt +31 -6
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +82 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NetworkUtils.kt +105 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationConstants.kt +28 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/NotificationUtils.kt +639 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/SafeAreaUtils.kt +62 -0
- package/dist/native/androidProject/app/src/main/res/drawable/splashscreen_fallback.png +0 -0
- package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +1 -0
- package/dist/native/androidProject/app/src/main/res/layout/activity_splash.xml +20 -0
- package/dist/native/androidProject/app/src/main/res/values/themes.xml +23 -0
- package/dist/native/androidProject/app/src/main/res/values-night/themes.xml +11 -0
- package/dist/native/androidProject/app/src/main/res/xml/network_security_config.xml +12 -0
- package/dist/native/androidProject/app/src/noFcm/java/io/yourname/androidproject/utils/NotificationManager.kt +166 -0
- package/dist/native/androidProject/app/src/noFcm/java/io/yourname/androidproject/utils/PushNotificationUtils.kt +94 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeMessageValidatorTest.kt +267 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/BridgeUtilsTest.kt +286 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CameraUtilsTest.kt +296 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +594 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +466 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/DeviceInfoUtilsTest.kt +187 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/FileUtilsTest.kt +304 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NativeBridgeTest.kt +424 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/NotificationUtilsTest.kt +469 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/SecurityBridgeTest.kt +199 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/URLWhitelistManagerTest.kt +462 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/WebCacheManagerTest.kt +391 -0
- package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/NotificationManager.kt +224 -0
- package/dist/native/androidProject/app/src/withFcm/java/io/yourname/androidproject/utils/PushNotificationUtils.kt +399 -0
- package/dist/native/androidProject/build.gradle.kts +6 -0
- package/dist/native/androidProject/gradle/libs.versions.toml +8 -2
- package/dist/native/androidSetup.js +4 -3
- package/dist/native/assets/catalyst.png +0 -0
- package/dist/native/bridge/WebBridge.js +16 -3
- package/dist/native/bridge/constants/NativeInterfaces.js +28 -10
- package/dist/native/bridge/errors.js +1 -1
- package/dist/native/bridge/hooks.js +76 -16
- package/dist/native/bridge/safeArea.js +5 -0
- package/dist/native/bridge/standardizedHooks.js +23 -5
- package/dist/native/bridge/useBaseHook.js +7 -9
- package/dist/native/bridge/utils/CameraUtils.js +58 -0
- package/dist/native/bridge/utils/FileObjectConverter.js +40 -0
- package/dist/native/bridge/utils/NativeBridge.js +57 -8
- package/dist/native/buildAppAndroid.js +115 -43
- package/dist/native/buildAppIos.js +291 -26
- package/dist/native/iosnativeWebView/Configurations/Debug.xcconfig +21 -0
- package/dist/native/iosnativeWebView/Configurations/Release.xcconfig +24 -0
- package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +50 -0
- package/dist/native/iosnativeWebView/Package.resolved +32 -0
- package/dist/native/iosnativeWebView/Package.swift +37 -0
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/LocalNotificationHandler.swift +293 -0
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/NotificationManager.swift +299 -0
- package/dist/native/iosnativeWebView/Sources/CatalystNotifications/PushNotificationHandler.swift +199 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +125 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +70 -0
- package/dist/native/iosnativeWebView/Sources/Core/Notifications/NotificationModels.swift +156 -0
- package/dist/native/iosnativeWebView/Sources/Core/Protocols/NotificationHandlerProtocol.swift +35 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +26 -0
- package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/CacheManager.swift +69 -36
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CameraTransportUtils.swift +363 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/DeviceInfoUtils.swift +67 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/FilePickerHandler.swift +1053 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/FrameworkServerUtils.swift +901 -0
- package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/ImageHandler.swift +206 -20
- package/dist/native/iosnativeWebView/Sources/Core/Utils/NetworkMonitor.swift +112 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/NotificationHandlerProvider.swift +20 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/NullNotificationHandler.swift +85 -0
- package/dist/native/iosnativeWebView/{iosnativeWebView → Sources/Core/Utils}/ResourceURLProtocol.swift +60 -25
- package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +132 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ScreenSecureManager.swift +131 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/URLWhitelistManager.swift +224 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/WebKitConfig.swift +43 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +991 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeDelegateHandler.swift +166 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeFileHandler.swift +340 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeJavaScriptInterface.swift +149 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +611 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +334 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +95 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +86 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +181 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +182 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +395 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/AppDelegate.swift +84 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/AccentColor.colorset/Contents.json +8 -8
- package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/AppIcon.appiconset/Contents.json +10 -10
- package/dist/native/iosnativeWebView/iosnativeWebView/Assets.xcassets/Contents.json +4 -4
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +65 -10
- package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +21 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +13 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/Preview Content/Preview Assets.xcassets/Contents.json +4 -4
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +12 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebViewApp.swift +80 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +118 -100
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +69 -0
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/xcshareddata/xcschemes/iosnativeWebView.xcscheme +9 -4
- package/dist/native/iosnativeWebView/iosnativeWebView.xctestplan +47 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BootTimingUtilityTests.swift +236 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeCommandHandlerSecurityTests.swift +212 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +418 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/CacheManagerTests.swift +390 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ConfigMappingTests.swift +299 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FilePickerHandlerTests.swift +383 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/FrameworkServerUtilsTests.swift +391 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/NativeBridgeTests.swift +581 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/NotificationHandlerTests.swift +430 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/ScreenSecureManagerTests.swift +121 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/URLWhitelistManagerTests.swift +367 -0
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/WebViewTests.swift +440 -0
- package/dist/native/renameAndroidProject.js +80 -70
- package/dist/native/setupEmulatorIos.js +22 -5
- package/dist/native/setupServer.js +11 -10
- package/dist/native/terminalProgress.js +10 -10
- package/dist/native/utils.js +4 -5
- package/dist/otel.js +4 -0
- package/dist/scripts/build.js +2 -2
- package/dist/scripts/devBuild.js +2 -2
- package/dist/scripts/loadEnvironmentVariables.js +3 -1
- package/dist/scripts/scriptUtils.js +2 -2
- package/dist/sentry.js +4 -1
- package/dist/server/renderer/document/Body.js +4 -4
- package/dist/server/renderer/handler.js +7 -5
- package/dist/webpack/development.client.babel.js +1 -1
- package/mcp_v2/conversion-tasks.json +326 -0
- package/mcp_v2/knowledge-base.json +1068 -0
- package/mcp_v2/lib/helpers.js +165 -0
- package/mcp_v2/mcp.js +563 -0
- package/mcp_v2/package.json +13 -0
- package/mcp_v2/schema.sql +88 -0
- package/mcp_v2/setup.js +278 -0
- package/mcp_v2/tools/build.js +686 -0
- package/mcp_v2/tools/config.js +453 -0
- package/mcp_v2/tools/conversion.js +799 -0
- package/mcp_v2/tools/debug.js +113 -0
- package/mcp_v2/tools/knowledge.js +219 -0
- package/mcp_v2/tools/sync.js +23 -0
- package/mcp_v2/tools/tasks.js +945 -0
- package/package.json +11 -15
- package/dist/native/androidProject/.idea/.name +0 -1
- package/dist/native/androidProject/.idea/AndroidProjectSystem.xml +0 -6
- package/dist/native/androidProject/.idea/compiler.xml +0 -6
- package/dist/native/androidProject/.idea/deploymentTargetSelector.xml +0 -10
- package/dist/native/androidProject/.idea/gradle.xml +0 -18
- package/dist/native/androidProject/.idea/migrations.xml +0 -10
- package/dist/native/androidProject/.idea/misc.xml +0 -10
- package/dist/native/androidProject/.idea/runConfigurations.xml +0 -17
- package/dist/native/androidProject/.idea/vcs.xml +0 -6
- package/dist/native/iosnativeWebView/iosnativeWebView/NativeBridge.swift +0 -217
- package/dist/native/iosnativeWebView/iosnativeWebView/WebView.swift +0 -96
- package/dist/native/iosnativeWebView/iosnativeWebView/WebViewModel.swift +0 -47
- package/dist/native/iosnativeWebView/iosnativeWebView/WebViewNavigationDelegate.swift +0 -140
- package/license +0 -10
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -9,12 +9,18 @@ import android.os.Bundle
|
|
|
9
9
|
import android.util.Log
|
|
10
10
|
import android.view.View
|
|
11
11
|
import android.webkit.*
|
|
12
|
+
import androidx.webkit.ServiceWorkerClientCompat
|
|
13
|
+
import androidx.webkit.ServiceWorkerControllerCompat
|
|
14
|
+
import androidx.webkit.WebResourceRequestCompat
|
|
12
15
|
import android.widget.ProgressBar
|
|
13
16
|
import androidx.webkit.WebViewAssetLoader
|
|
14
17
|
import io.yourname.androidproject.WebCacheManager
|
|
15
|
-
import io.yourname.androidproject.
|
|
16
|
-
import io.yourname.androidproject.
|
|
18
|
+
import io.yourname.androidproject.URLWhitelistManager
|
|
19
|
+
import io.yourname.androidproject.matchesCachePattern
|
|
20
|
+
import io.yourname.androidproject.utils.CameraUtils
|
|
21
|
+
import io.yourname.androidproject.utils.NetworkUtils
|
|
17
22
|
import kotlinx.coroutines.*
|
|
23
|
+
import java.io.FileNotFoundException
|
|
18
24
|
import java.util.Properties
|
|
19
25
|
|
|
20
26
|
class CustomWebView(
|
|
@@ -29,6 +35,7 @@ class CustomWebView(
|
|
|
29
35
|
override val coroutineContext = Dispatchers.Main + job
|
|
30
36
|
|
|
31
37
|
private var cacheManager: WebCacheManager
|
|
38
|
+
private val metricsMonitor = MetricsMonitor.getInstance(context)
|
|
32
39
|
private var buildType: String = "debug" // Default to debug
|
|
33
40
|
private var cachePatterns: List<String> = emptyList()
|
|
34
41
|
private var isHardwareAccelerationEnabled = false
|
|
@@ -39,35 +46,62 @@ class CustomWebView(
|
|
|
39
46
|
private lateinit var assetLoader: WebViewAssetLoader
|
|
40
47
|
private var allowedUrls: List<String> = emptyList()
|
|
41
48
|
private var accessControlEnabled: Boolean = false
|
|
49
|
+
private val offlineAssetPath = "offline/offline.html"
|
|
50
|
+
private val offlineAssetUrl = "file:///android_asset/$offlineAssetPath"
|
|
51
|
+
private var offlinePageVisible = false
|
|
52
|
+
private var lastTargetUrl: String? = null
|
|
53
|
+
private var defaultRequestHeaders: Map<String, String> = emptyMap()
|
|
42
54
|
|
|
43
55
|
// Counters for asset loading statistics
|
|
44
56
|
private var assetLoadAttempts = 0
|
|
45
57
|
private var assetLoadFailures = 0
|
|
46
58
|
|
|
59
|
+
// Track ongoing cache fetches to prevent duplicates
|
|
60
|
+
private val ongoingCacheFetches = mutableSetOf<String>()
|
|
61
|
+
|
|
47
62
|
init {
|
|
48
|
-
cacheManager = WebCacheManager(context)
|
|
49
63
|
setupFromProperties()
|
|
64
|
+
cacheManager = WebCacheManager(context, properties)
|
|
50
65
|
setupWebView()
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
private fun setupFromProperties() {
|
|
54
69
|
buildType = properties.getProperty("buildType", "debug")
|
|
55
70
|
apiBaseUrl = properties.getProperty("apiBaseUrl", "")
|
|
56
|
-
|
|
71
|
+
// Try to get cache patterns from multiple possible property keys
|
|
72
|
+
val cachePatternProperty = properties.getProperty("cachePattern", "")
|
|
73
|
+
.takeIf { it.isNotEmpty() }
|
|
74
|
+
?: properties.getProperty("android.cachePattern", "")
|
|
75
|
+
|
|
76
|
+
cachePatterns = cachePatternProperty
|
|
57
77
|
.split(",")
|
|
58
78
|
.map { it.trim() }
|
|
59
79
|
.filter { it.isNotEmpty() }
|
|
60
80
|
|
|
81
|
+
if (BuildConfig.DEBUG) {
|
|
82
|
+
Log.d(TAG, "📦 Cache patterns loaded: $cachePatterns (count: ${cachePatterns.size})")
|
|
83
|
+
Log.d(TAG, "📦 Raw cachePattern property: '${properties.getProperty("cachePattern", "")}'")
|
|
84
|
+
Log.d(TAG, "📦 Raw android.cachePattern property: '${properties.getProperty("android.cachePattern", "")}'")
|
|
85
|
+
Log.d(TAG, "📦 Selected cache pattern property: '$cachePatternProperty'")
|
|
86
|
+
}
|
|
87
|
+
|
|
61
88
|
// Parse buildOptimisation property
|
|
62
89
|
buildOptimisation = properties.getProperty("buildOptimisation", "false").toBoolean()
|
|
63
90
|
|
|
64
|
-
// Load
|
|
65
|
-
accessControlEnabled = properties.getProperty("accessControl.enabled", "false").toBoolean()
|
|
91
|
+
// Load allowed URLs from properties
|
|
66
92
|
allowedUrls = properties.getProperty("accessControl.allowedUrls", "")
|
|
67
93
|
.split(",")
|
|
68
94
|
.map { it.trim() }
|
|
69
95
|
.filter { it.isNotEmpty() }
|
|
70
96
|
|
|
97
|
+
// Access control toggle
|
|
98
|
+
accessControlEnabled = properties
|
|
99
|
+
.getProperty("accessControl.enabled", "true")
|
|
100
|
+
.equals("true", ignoreCase = true)
|
|
101
|
+
|
|
102
|
+
// Initialize URLWhitelistManager with access control configuration
|
|
103
|
+
URLWhitelistManager.initialize(accessControlEnabled, allowedUrls)
|
|
104
|
+
|
|
71
105
|
// Set initial flags based on buildOptimisation
|
|
72
106
|
if (buildOptimisation) {
|
|
73
107
|
isInitialApiCalled = false
|
|
@@ -96,10 +130,52 @@ class CustomWebView(
|
|
|
96
130
|
.build()
|
|
97
131
|
}
|
|
98
132
|
|
|
133
|
+
fun setDefaultRequestHeaders(headers: Map<String, String>) {
|
|
134
|
+
defaultRequestHeaders = headers.toMap()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private fun loadUrlInternal(url: String) {
|
|
138
|
+
val isNetworkUrl = url.startsWith("http://") || url.startsWith("https://")
|
|
139
|
+
|
|
140
|
+
if (isNetworkUrl && defaultRequestHeaders.isNotEmpty()) {
|
|
141
|
+
if (BuildConfig.DEBUG) {
|
|
142
|
+
Log.d(TAG, "🌐 Loading with headers: $url, headers=$defaultRequestHeaders")
|
|
143
|
+
}
|
|
144
|
+
webView.loadUrl(url, defaultRequestHeaders)
|
|
145
|
+
} else {
|
|
146
|
+
webView.loadUrl(url)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
99
150
|
fun loadUrl(url: String) {
|
|
100
|
-
|
|
151
|
+
lastTargetUrl = url
|
|
152
|
+
offlinePageVisible = false
|
|
153
|
+
loadUrlInternal(url)
|
|
101
154
|
}
|
|
102
155
|
|
|
156
|
+
fun updateLastTargetUrl(url: String) {
|
|
157
|
+
lastTargetUrl = url
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fun showOfflinePage() {
|
|
161
|
+
if (offlinePageVisible) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Verify offline asset exists to avoid crash
|
|
166
|
+
try {
|
|
167
|
+
context.assets.open(offlineAssetPath).close()
|
|
168
|
+
offlinePageVisible = true
|
|
169
|
+
loadUrlInternal(offlineAssetUrl)
|
|
170
|
+
if (BuildConfig.DEBUG) {
|
|
171
|
+
Log.d(TAG, "📴 Showing offline page from assets: $offlineAssetPath")
|
|
172
|
+
}
|
|
173
|
+
} catch (e: Exception) {
|
|
174
|
+
Log.e(TAG, "❌ offline.html not found in assets, cannot show offline page", e)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
103
179
|
fun saveState(outState: Bundle) {
|
|
104
180
|
webView.saveState(outState)
|
|
105
181
|
}
|
|
@@ -113,6 +189,10 @@ class CustomWebView(
|
|
|
113
189
|
webView.clearHistory()
|
|
114
190
|
}
|
|
115
191
|
|
|
192
|
+
fun clearAllCache() {
|
|
193
|
+
cacheManager.clearAll()
|
|
194
|
+
}
|
|
195
|
+
|
|
116
196
|
fun canGoBack(): Boolean = webView.canGoBack()
|
|
117
197
|
|
|
118
198
|
fun goBack() {
|
|
@@ -131,20 +211,35 @@ class CustomWebView(
|
|
|
131
211
|
return webView
|
|
132
212
|
}
|
|
133
213
|
|
|
134
|
-
@SuppressLint("JavascriptInterface")
|
|
135
214
|
fun addJavascriptInterface(obj: Any, name: String) {
|
|
136
|
-
//
|
|
137
|
-
if (android.os.Build.VERSION.SDK_INT
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
215
|
+
// Minimum API level check - JavascriptInterface annotation is safe from API 17+
|
|
216
|
+
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
|
217
|
+
Log.e(TAG, "❌ JavaScript interfaces are not secure on API level < 17. Refusing to add interface: $name")
|
|
218
|
+
return
|
|
219
|
+
}
|
|
142
220
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
221
|
+
// Security: Verify that the object has at least one method annotated with @JavascriptInterface
|
|
222
|
+
// Check both declared methods and inherited methods, excluding Object class methods
|
|
223
|
+
val allMethods = obj.javaClass.methods.filter {
|
|
224
|
+
it.declaringClass != Object::class.java
|
|
225
|
+
}
|
|
226
|
+
val hasAnnotatedMethods = allMethods.any {
|
|
227
|
+
it.isAnnotationPresent(android.webkit.JavascriptInterface::class.java)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!hasAnnotatedMethods) {
|
|
231
|
+
Log.e(TAG, "❌ Security: No methods in ${obj.javaClass.simpleName} are annotated with @JavascriptInterface. Refusing to add interface.")
|
|
232
|
+
return
|
|
146
233
|
}
|
|
147
234
|
|
|
235
|
+
// Additional security check: only allow whitelisted interface names
|
|
236
|
+
val allowedInterfaces = setOf("NativeBridge", "AndroidBridge")
|
|
237
|
+
if (name !in allowedInterfaces) {
|
|
238
|
+
Log.e(TAG, "❌ Security: Interface name '$name' is not in whitelist. Refusing to add interface.")
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@Suppress("JavascriptInterface")
|
|
148
243
|
webView.addJavascriptInterface(obj, name)
|
|
149
244
|
if (BuildConfig.DEBUG) {
|
|
150
245
|
Log.d(TAG, "🔗 Added JavaScript interface: $name")
|
|
@@ -198,21 +293,108 @@ class CustomWebView(
|
|
|
198
293
|
}
|
|
199
294
|
|
|
200
295
|
private fun shouldCacheUrl(url: String): Boolean {
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return this.replace(".", "\\.")
|
|
205
|
-
.replace("*", ".*")
|
|
206
|
-
.let { "^$it$" }
|
|
296
|
+
if (BuildConfig.DEBUG) {
|
|
297
|
+
Log.d(TAG, "🔍 CACHE_CHECK: Checking if URL should be cached: $url")
|
|
298
|
+
Log.d(TAG, "🔍 CACHE_CHECK: Cache patterns: $cachePatterns")
|
|
207
299
|
}
|
|
208
300
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
301
|
+
val shouldCache = matchesCachePattern(url, cachePatterns)
|
|
302
|
+
|
|
303
|
+
if (BuildConfig.DEBUG) {
|
|
304
|
+
if (shouldCache) {
|
|
305
|
+
Log.d(TAG, "✅ CACHE_CHECK: URL WILL be cached: $url")
|
|
306
|
+
} else {
|
|
307
|
+
Log.d(TAG, "❌ CACHE_CHECK: URL will NOT be cached (no pattern match): $url")
|
|
308
|
+
}
|
|
212
309
|
}
|
|
310
|
+
|
|
311
|
+
return shouldCache
|
|
213
312
|
}
|
|
214
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Handle special URL schemes (tel:, mailto:, sms:)
|
|
316
|
+
*/
|
|
317
|
+
private fun handleSpecialScheme(url: String): Boolean {
|
|
318
|
+
val uri = Uri.parse(url)
|
|
319
|
+
val scheme = uri.scheme?.lowercase() ?: return false
|
|
320
|
+
|
|
321
|
+
// Handle offline retry scheme
|
|
322
|
+
if (scheme == "catalyst" && (uri.host?.lowercase() == "retry" || uri.schemeSpecificPart?.lowercase() == "retry")) {
|
|
323
|
+
val status = NetworkUtils.getCurrentStatus(context)
|
|
324
|
+
if (status.isOnline) {
|
|
325
|
+
val target = lastTargetUrl
|
|
326
|
+
if (target != null) {
|
|
327
|
+
if (BuildConfig.DEBUG) {
|
|
328
|
+
Log.d(TAG, "🔄 Retry requested, online. Reloading: $target")
|
|
329
|
+
}
|
|
330
|
+
offlinePageVisible = false
|
|
331
|
+
lastTargetUrl = target
|
|
332
|
+
loadUrlInternal(target)
|
|
333
|
+
} else if (BuildConfig.DEBUG) {
|
|
334
|
+
Log.w(TAG, "🔄 Retry requested but no target URL is known")
|
|
335
|
+
}
|
|
336
|
+
} else if (BuildConfig.DEBUG) {
|
|
337
|
+
Log.d(TAG, "🔄 Retry requested but still offline; staying on offline page")
|
|
338
|
+
}
|
|
339
|
+
return true
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Only handle tel, mailto, sms
|
|
343
|
+
if (scheme !in listOf("tel", "mailto", "sms")) {
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
215
346
|
|
|
347
|
+
try {
|
|
348
|
+
val intent = when (scheme) {
|
|
349
|
+
"tel" -> Intent(Intent.ACTION_DIAL, uri)
|
|
350
|
+
"mailto" -> Intent(Intent.ACTION_SENDTO, uri)
|
|
351
|
+
"sms" -> Intent(Intent.ACTION_VIEW, uri)
|
|
352
|
+
else -> return false
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// For mailto, check if any apps can handle it before showing chooser
|
|
356
|
+
if (scheme == "mailto") {
|
|
357
|
+
val packageManager = context.packageManager
|
|
358
|
+
val activities = packageManager.queryIntentActivities(intent, 0)
|
|
359
|
+
|
|
360
|
+
if (activities.isNotEmpty()) {
|
|
361
|
+
// Apps available - show chooser
|
|
362
|
+
val chooser = Intent.createChooser(intent, "Send email")
|
|
363
|
+
chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
364
|
+
context.startActivity(chooser)
|
|
365
|
+
} else {
|
|
366
|
+
// No email apps - try Gmail app, then browser
|
|
367
|
+
try {
|
|
368
|
+
val gmailIntent = Intent(Intent.ACTION_SENDTO, uri).apply {
|
|
369
|
+
setPackage("com.google.android.gm")
|
|
370
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
371
|
+
}
|
|
372
|
+
context.startActivity(gmailIntent)
|
|
373
|
+
if (BuildConfig.DEBUG) {
|
|
374
|
+
Log.d(TAG, "Opened Gmail app")
|
|
375
|
+
}
|
|
376
|
+
} catch (e: android.content.ActivityNotFoundException) {
|
|
377
|
+
// Gmail app not available, open in browser
|
|
378
|
+
openInInAppBrowser("https://mail.google.com")
|
|
379
|
+
if (BuildConfig.DEBUG) {
|
|
380
|
+
Log.d(TAG, "Opened Gmail in browser")
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
context.startActivity(intent)
|
|
386
|
+
}
|
|
387
|
+
return true
|
|
388
|
+
} catch (e: android.content.ActivityNotFoundException) {
|
|
389
|
+
if (BuildConfig.DEBUG) {
|
|
390
|
+
Log.d(TAG, "No app to handle $scheme")
|
|
391
|
+
}
|
|
392
|
+
return true
|
|
393
|
+
} catch (e: Exception) {
|
|
394
|
+
Log.e(TAG, "Error handling $scheme: ${e.message}")
|
|
395
|
+
return true
|
|
396
|
+
}
|
|
397
|
+
}
|
|
216
398
|
|
|
217
399
|
private fun openInInAppBrowser(url: String) {
|
|
218
400
|
try {
|
|
@@ -253,10 +435,11 @@ class CustomWebView(
|
|
|
253
435
|
// Remove leading slash if present
|
|
254
436
|
var cleanPath = if (path.startsWith("/")) path.substring(1) else path
|
|
255
437
|
|
|
438
|
+
// TODO: Add HTML file workflow - index.html not available yet
|
|
256
439
|
// If path is empty or just "/", return index.html
|
|
257
|
-
if (cleanPath.isEmpty()) {
|
|
258
|
-
|
|
259
|
-
}
|
|
440
|
+
// if (cleanPath.isEmpty()) {
|
|
441
|
+
// return "build/public/index.html"
|
|
442
|
+
// }
|
|
260
443
|
|
|
261
444
|
// Special case for favicon.ico which might be in root
|
|
262
445
|
if (cleanPath == "favicon.ico") {
|
|
@@ -289,6 +472,120 @@ class CustomWebView(
|
|
|
289
472
|
}
|
|
290
473
|
}
|
|
291
474
|
|
|
475
|
+
private fun setupServiceWorker() {
|
|
476
|
+
try {
|
|
477
|
+
val serviceWorkerController = ServiceWorkerControllerCompat.getInstance()
|
|
478
|
+
serviceWorkerController.setServiceWorkerClient(object : ServiceWorkerClientCompat() {
|
|
479
|
+
override fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
|
|
480
|
+
val url = request.url.toString()
|
|
481
|
+
|
|
482
|
+
if (BuildConfig.DEBUG) {
|
|
483
|
+
Log.d(TAG, "🔧 SERVICE_WORKER: Intercepting request: $url")
|
|
484
|
+
Log.d(TAG, "🔧 SERVICE_WORKER: Method: ${request.method}")
|
|
485
|
+
Log.d(TAG, "🔧 SERVICE_WORKER: Headers: ${request.requestHeaders}")
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Check if URL is allowed
|
|
489
|
+
if (!URLWhitelistManager.isUrlAllowed(url)) {
|
|
490
|
+
if (BuildConfig.DEBUG) {
|
|
491
|
+
Log.w(TAG, "🙫 SERVICE_WORKER: Request blocked by access control: $url")
|
|
492
|
+
}
|
|
493
|
+
return WebResourceResponse("text/plain", "utf-8", null)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Let API calls go through normally (skip caching)
|
|
497
|
+
if (isApiCall(url)) {
|
|
498
|
+
if (BuildConfig.DEBUG) {
|
|
499
|
+
Log.d(TAG, "🌐 SERVICE_WORKER: API call detected, skipping cache: $url")
|
|
500
|
+
}
|
|
501
|
+
return null
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Check if this should be cached
|
|
505
|
+
if (request.method == "GET" && shouldCacheUrl(url)) {
|
|
506
|
+
if (BuildConfig.DEBUG) {
|
|
507
|
+
Log.d(TAG, "🎯 SERVICE_WORKER: Using cache system for: $url")
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return try {
|
|
511
|
+
// Use synchronous cache check to avoid blocking UI thread
|
|
512
|
+
val headers = request.requestHeaders.toMutableMap().apply {
|
|
513
|
+
if (!containsKey("Cache-Control")) {
|
|
514
|
+
put("Cache-Control", "max-age=86400")
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Get cached response synchronously (non-blocking for ServiceWorker)
|
|
519
|
+
val response = cacheManager.getCachedResponseSync(url, headers)
|
|
520
|
+
if (response != null) {
|
|
521
|
+
if (BuildConfig.DEBUG) {
|
|
522
|
+
Log.d(TAG, "✅ SERVICE_WORKER: Served from cache: $url")
|
|
523
|
+
}
|
|
524
|
+
response
|
|
525
|
+
} else {
|
|
526
|
+
if (BuildConfig.DEBUG) {
|
|
527
|
+
Log.d(TAG, "❌ SERVICE_WORKER: Cache miss: $url")
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Trigger async cache population for next request (with deduplication)
|
|
531
|
+
val shouldFetch = synchronized(ongoingCacheFetches) {
|
|
532
|
+
ongoingCacheFetches.add(url)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (shouldFetch) {
|
|
536
|
+
if (BuildConfig.DEBUG) {
|
|
537
|
+
Log.d(TAG, "🔄 SERVICE_WORKER: Triggering async cache fetch for: $url")
|
|
538
|
+
}
|
|
539
|
+
launch(Dispatchers.IO) {
|
|
540
|
+
try {
|
|
541
|
+
cacheManager.getCachedResponse(url, headers)
|
|
542
|
+
if (BuildConfig.DEBUG) {
|
|
543
|
+
Log.d(TAG, "✅ SERVICE_WORKER: Async cache fetch completed: $url")
|
|
544
|
+
}
|
|
545
|
+
} catch (e: Exception) {
|
|
546
|
+
if (BuildConfig.DEBUG) {
|
|
547
|
+
Log.e(TAG, "❌ SERVICE_WORKER: Async cache fetch failed: $url - ${e.message}")
|
|
548
|
+
}
|
|
549
|
+
} finally {
|
|
550
|
+
synchronized(ongoingCacheFetches) {
|
|
551
|
+
ongoingCacheFetches.remove(url)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
if (BuildConfig.DEBUG) {
|
|
557
|
+
Log.d(TAG, "⏭️ SERVICE_WORKER: Cache fetch already in progress for: $url")
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Let this request fall back to network
|
|
562
|
+
null
|
|
563
|
+
}
|
|
564
|
+
} catch (e: Exception) {
|
|
565
|
+
if (BuildConfig.DEBUG) {
|
|
566
|
+
Log.e(TAG, "❌ SERVICE_WORKER: Error processing cache request: ${e.message}")
|
|
567
|
+
}
|
|
568
|
+
null
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
if (BuildConfig.DEBUG) {
|
|
572
|
+
Log.d(TAG, "⏭️ SERVICE_WORKER: Skipping cache for: $url")
|
|
573
|
+
Log.d(TAG, "⏭️ SERVICE_WORKER: Method: ${request.method}, shouldCache: ${shouldCacheUrl(url)}")
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return null // Let default handling proceed
|
|
578
|
+
}
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
if (BuildConfig.DEBUG) {
|
|
582
|
+
Log.d(TAG, "✅ ServiceWorker intercept configured successfully")
|
|
583
|
+
}
|
|
584
|
+
} catch (e: Exception) {
|
|
585
|
+
Log.e(TAG, "❌ Failed to setup ServiceWorker: ${e.message}")
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
292
589
|
@SuppressLint("SetJavaScriptEnabled")
|
|
293
590
|
private fun setupWebView() {
|
|
294
591
|
if (BuildConfig.DEBUG) {
|
|
@@ -304,45 +601,54 @@ class CustomWebView(
|
|
|
304
601
|
WebSettings.MIXED_CONTENT_NEVER_ALLOW
|
|
305
602
|
}
|
|
306
603
|
|
|
307
|
-
setRenderPriority(WebSettings.RenderPriority.HIGH)
|
|
308
604
|
cacheMode = WebSettings.LOAD_DEFAULT
|
|
309
605
|
databaseEnabled = true
|
|
310
606
|
domStorageEnabled = true
|
|
311
607
|
allowFileAccess = true
|
|
312
608
|
allowContentAccess = true
|
|
313
|
-
setEnableSmoothTransition(true)
|
|
314
609
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
610
|
+
// TODO: Enable these when build optimization feature is implemented
|
|
611
|
+
// These are deprecated but may be needed for local file access in development
|
|
612
|
+
// allowFileAccessFromFileURLs = BuildConfig.DEBUG
|
|
613
|
+
// allowUniversalAccessFromFileURLs = BuildConfig.DEBUG
|
|
318
614
|
}
|
|
319
615
|
|
|
616
|
+
// Setup ServiceWorker to intercept requests that bypass normal WebView intercept
|
|
617
|
+
setupServiceWorker()
|
|
618
|
+
|
|
320
619
|
webView.webViewClient = object : WebViewClient() {
|
|
321
620
|
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
|
322
621
|
request?.url?.let { url ->
|
|
323
622
|
val urlString = url.toString()
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
Log.d(TAG, "🌍 External domain detected, opening in in-app browser: $urlString")
|
|
330
|
-
}
|
|
331
|
-
openInInAppBrowser(urlString)
|
|
332
|
-
return true
|
|
623
|
+
|
|
624
|
+
// Handle special URL schemes (tel:, mailto:, sms:) HERE to prevent page loading
|
|
625
|
+
if (handleSpecialScheme(urlString)) {
|
|
626
|
+
if (BuildConfig.DEBUG) {
|
|
627
|
+
Log.d(TAG, "📞 Special scheme handled in shouldOverrideUrlLoading: $urlString")
|
|
333
628
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
629
|
+
return true // Prevent WebView from trying to load the URL
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Check if URL is an external domain
|
|
633
|
+
if (url.scheme in listOf("http", "https") && URLWhitelistManager.isExternalDomain(urlString)) {
|
|
634
|
+
if (BuildConfig.DEBUG) {
|
|
635
|
+
Log.d(TAG, "🌍 External domain detected, opening in in-app browser: $urlString")
|
|
636
|
+
}
|
|
637
|
+
openInInAppBrowser(urlString)
|
|
638
|
+
return true
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Check if URL is allowed for internal navigation
|
|
642
|
+
if (!URLWhitelistManager.isUrlAllowed(urlString)) {
|
|
643
|
+
if (BuildConfig.DEBUG) {
|
|
644
|
+
Log.w(TAG, "🚫 URL blocked by access control: $urlString")
|
|
341
645
|
}
|
|
646
|
+
return true
|
|
342
647
|
}
|
|
343
|
-
|
|
648
|
+
|
|
344
649
|
// Let WebView handle loading non-API HTTP/HTTPS URLs
|
|
345
650
|
if (url.scheme in listOf("http", "https")) {
|
|
651
|
+
lastTargetUrl = urlString
|
|
346
652
|
return false
|
|
347
653
|
}
|
|
348
654
|
}
|
|
@@ -354,118 +660,218 @@ class CustomWebView(
|
|
|
354
660
|
request: WebResourceRequest
|
|
355
661
|
): WebResourceResponse? {
|
|
356
662
|
val url = request.url.toString()
|
|
663
|
+
|
|
664
|
+
// Track all network requests
|
|
665
|
+
metricsMonitor.recordNetworkRequest(url)
|
|
666
|
+
|
|
357
667
|
if (BuildConfig.DEBUG) {
|
|
358
|
-
Log.d(TAG, "🔄
|
|
668
|
+
Log.d(TAG, "🔄 INTERCEPT: URL: $url")
|
|
669
|
+
Log.d(TAG, "🔄 INTERCEPT: Method: ${request.method}")
|
|
670
|
+
Log.d(TAG, "🔄 INTERCEPT: Thread: ${Thread.currentThread().name}")
|
|
671
|
+
Log.d(TAG, "🔄 INTERCEPT: File extension: ${url.substringAfterLast('.', "none")}")
|
|
672
|
+
Log.d(TAG, "🔄 INTERCEPT: isInitialPageLoaded: $isInitialPageLoaded")
|
|
359
673
|
}
|
|
360
674
|
|
|
361
|
-
if (
|
|
675
|
+
if (!URLWhitelistManager.isUrlAllowed(url)) {
|
|
362
676
|
if (BuildConfig.DEBUG) {
|
|
363
|
-
Log.w(TAG, "🚫 Network request blocked by access control: $url")
|
|
677
|
+
Log.w(TAG, "🚫 STEP_1_FAIL: Network request blocked by access control: $url")
|
|
364
678
|
}
|
|
365
679
|
// Return an empty response to block the request
|
|
366
680
|
return WebResourceResponse("text/plain", "utf-8", null)
|
|
681
|
+
} else if (BuildConfig.DEBUG) {
|
|
682
|
+
if (URLWhitelistManager.isAccessControlEnabled()) {
|
|
683
|
+
Log.d(TAG, "✅ STEP_1_PASS: URL allowed by access control: $url")
|
|
684
|
+
} else {
|
|
685
|
+
Log.d(TAG, "⚙️ STEP_1_SKIP: Access control disabled, allowing URL: $url")
|
|
686
|
+
}
|
|
367
687
|
}
|
|
368
688
|
|
|
689
|
+
// TODO: Add HTML file workflow - index.html not available yet
|
|
369
690
|
// Handle the initial route request - intercept first request regardless of host
|
|
370
|
-
if (!isInitialApiCalled && request.method == "GET") {
|
|
371
|
-
|
|
691
|
+
// if (!isInitialApiCalled && request.method == "GET") {
|
|
692
|
+
// isInitialApiCalled = true
|
|
693
|
+
// if (BuildConfig.DEBUG) {
|
|
694
|
+
// Log.d(TAG, "📄 STEP_2_INITIAL: Serving initial index.html from assets for route: $url")
|
|
695
|
+
// Log.d(TAG, "📄 STEP_2_INITIAL: This request will NOT go through cache system")
|
|
696
|
+
// }
|
|
697
|
+
//
|
|
698
|
+
// try {
|
|
699
|
+
// val indexHtml = context.assets.open("build/public/index.html")
|
|
700
|
+
// return WebResourceResponse(
|
|
701
|
+
// "text/html",
|
|
702
|
+
// "utf-8",
|
|
703
|
+
// indexHtml
|
|
704
|
+
// )
|
|
705
|
+
// } catch (e: Exception) {
|
|
706
|
+
// Log.e(TAG, "❌ Error loading index.html from assets", e)
|
|
707
|
+
// }
|
|
708
|
+
// }
|
|
709
|
+
|
|
710
|
+
// Let API calls go through normally (before any other checks)
|
|
711
|
+
if (isApiCall(url)) {
|
|
372
712
|
if (BuildConfig.DEBUG) {
|
|
373
|
-
Log.d(TAG, "
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
val indexHtml = context.assets.open("build/public/index.html")
|
|
378
|
-
return WebResourceResponse(
|
|
379
|
-
"text/html",
|
|
380
|
-
"utf-8",
|
|
381
|
-
indexHtml
|
|
382
|
-
)
|
|
383
|
-
} catch (e: Exception) {
|
|
384
|
-
Log.e(TAG, "❌ Error loading index.html from assets", e)
|
|
713
|
+
Log.d(TAG, "🌐 STEP_3_API: API call detected, letting it go through network: $url")
|
|
714
|
+
Log.d(TAG, "🌐 STEP_3_API: This request will NOT go through cache or asset system")
|
|
385
715
|
}
|
|
716
|
+
return null
|
|
386
717
|
}
|
|
387
718
|
|
|
388
719
|
if(!isInitialPageLoaded) {
|
|
389
|
-
|
|
390
|
-
|
|
720
|
+
if (BuildConfig.DEBUG) {
|
|
721
|
+
Log.d(TAG, "🔍 STEP_4_CHECK: Initial page not loaded yet, checking for static resources")
|
|
722
|
+
Log.d(TAG, "🔍 STEP_4_CHECK: isStaticResourceRequest($url) = ${isStaticResourceRequest(url)}")
|
|
723
|
+
Log.d(TAG, "🔍 STEP_4_CHECK: shouldCacheUrl($url) = ${shouldCacheUrl(url)}")
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// IMPORTANT: Skip asset loading if the URL matches a cache pattern
|
|
727
|
+
// This allows cached resources to be served from the cache system instead
|
|
728
|
+
if (request.method == "GET" && isStaticResourceRequest(url) && !shouldCacheUrl(url)) {
|
|
391
729
|
val assetPath = extractAssetPath(url)
|
|
392
730
|
assetLoadAttempts++
|
|
393
731
|
|
|
394
732
|
try {
|
|
395
733
|
val mimeType = getMimeType(assetPath)
|
|
396
734
|
if (BuildConfig.DEBUG) {
|
|
397
|
-
Log.d(TAG, "📦 Attempting to serve from assets: $assetPath")
|
|
735
|
+
Log.d(TAG, "📦 Attempting to serve from assets: $assetPath (not in cache patterns)")
|
|
398
736
|
}
|
|
399
737
|
val inputStream = context.assets.open(assetPath)
|
|
400
738
|
if (BuildConfig.DEBUG) {
|
|
401
739
|
Log.d(TAG, "✅ Successfully loaded from assets: $assetPath")
|
|
402
740
|
}
|
|
403
741
|
return WebResourceResponse(mimeType, "utf-8", inputStream)
|
|
742
|
+
} catch (e: FileNotFoundException) {
|
|
743
|
+
assetLoadFailures++
|
|
744
|
+
if (BuildConfig.DEBUG) {
|
|
745
|
+
Log.w(TAG, "📁 Asset not found: $assetPath - falling through to cache check")
|
|
746
|
+
}
|
|
747
|
+
// Asset not found - fall through to cache check below
|
|
748
|
+
} catch (e: SecurityException) {
|
|
749
|
+
assetLoadFailures++
|
|
750
|
+
Log.e(TAG, "🚫 Security error loading asset: $assetPath", e)
|
|
751
|
+
return WebResourceResponse("text/plain", "utf-8", null)
|
|
752
|
+
} catch (e: OutOfMemoryError) {
|
|
753
|
+
assetLoadFailures++
|
|
754
|
+
Log.e(TAG, "📢 Out of memory loading asset: $assetPath", e)
|
|
755
|
+
// Try to free some memory
|
|
756
|
+
System.gc()
|
|
757
|
+
return WebResourceResponse("text/plain", "utf-8", null)
|
|
404
758
|
} catch (e: Exception) {
|
|
405
759
|
assetLoadFailures++
|
|
406
760
|
if (BuildConfig.DEBUG) {
|
|
407
|
-
Log.e(TAG, "❌
|
|
408
|
-
Log.d(TAG, "⚠️ Falling
|
|
761
|
+
Log.e(TAG, "❌ Unexpected error loading asset: $assetPath", e)
|
|
762
|
+
Log.d(TAG, "⚠️ Falling through to cache check for: $url")
|
|
409
763
|
}
|
|
410
|
-
//
|
|
411
|
-
return null
|
|
764
|
+
// Unexpected error - fall through to cache check below
|
|
412
765
|
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Let API calls go through normally
|
|
416
|
-
if (isApiCall(url)) {
|
|
766
|
+
} else if (shouldCacheUrl(url)) {
|
|
417
767
|
if (BuildConfig.DEBUG) {
|
|
418
|
-
Log.d(TAG, "
|
|
768
|
+
Log.d(TAG, "⏭️ STEP_4_SKIP: Skipping asset loading for cached URL: $url")
|
|
419
769
|
}
|
|
420
|
-
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
if (BuildConfig.DEBUG) {
|
|
773
|
+
Log.d(TAG, "✅ STEP_3_PASS: Initial page loaded, skipping asset loading logic")
|
|
421
774
|
}
|
|
422
775
|
}
|
|
423
776
|
|
|
424
777
|
// For non-API HTTP requests that match cache patterns, use cache system
|
|
778
|
+
if (BuildConfig.DEBUG) {
|
|
779
|
+
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: Evaluating cache conditions")
|
|
780
|
+
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: Request method: ${request.method} (need GET)")
|
|
781
|
+
Log.d(TAG, "🔍 STEP_5_CACHE_CHECK: shouldCacheUrl($url) = ${shouldCacheUrl(url)}")
|
|
782
|
+
}
|
|
783
|
+
|
|
425
784
|
if (request.method == "GET" && shouldCacheUrl(url)) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
785
|
+
// Track that this request was evaluated for caching
|
|
786
|
+
metricsMonitor.recordCacheEvaluation(url)
|
|
787
|
+
if (BuildConfig.DEBUG) {
|
|
788
|
+
Log.d(TAG, "🎯 STEP_5_SUCCESS: Using cache system for URL: $url")
|
|
789
|
+
Log.d(TAG, "🎯 STEP_5_SUCCESS: Request method: ${request.method}")
|
|
790
|
+
Log.d(TAG, "🎯 STEP_5_SUCCESS: isInitialPageLoaded: $isInitialPageLoaded")
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
val startTime = System.currentTimeMillis()
|
|
794
|
+
disableHardwareAcceleration()
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
val headers = request.requestHeaders.toMutableMap().apply {
|
|
798
|
+
if (!containsKey("Cache-Control")) {
|
|
799
|
+
put("Cache-Control", "max-age=86400")
|
|
800
|
+
}
|
|
801
|
+
if (!containsKey("Pragma")) {
|
|
802
|
+
put("Pragma", "cache")
|
|
440
803
|
}
|
|
804
|
+
}
|
|
441
805
|
|
|
442
|
-
|
|
806
|
+
// Use synchronous cache check to avoid blocking
|
|
807
|
+
val response = cacheManager.getCachedResponseSync(url, headers)
|
|
443
808
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
response
|
|
450
|
-
} else {
|
|
451
|
-
if (BuildConfig.DEBUG) {
|
|
452
|
-
Log.d(TAG, "❌ Cache miss for: $url")
|
|
453
|
-
}
|
|
454
|
-
null
|
|
809
|
+
if (response != null) {
|
|
810
|
+
if (BuildConfig.DEBUG) {
|
|
811
|
+
val duration = System.currentTimeMillis() - startTime
|
|
812
|
+
Log.d(TAG, "✅ WEBVIEW_CLIENT: Served from cache in ${duration}ms: $url")
|
|
455
813
|
}
|
|
456
|
-
|
|
457
|
-
|
|
814
|
+
metricsMonitor.recordCacheHit(url)
|
|
815
|
+
return response
|
|
816
|
+
} else {
|
|
458
817
|
if (BuildConfig.DEBUG) {
|
|
459
|
-
|
|
818
|
+
Log.d(TAG, "❌ WEBVIEW_CLIENT: Cache miss, triggering async fetch: $url")
|
|
460
819
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
820
|
+
metricsMonitor.recordCacheMiss(url)
|
|
821
|
+
|
|
822
|
+
// Trigger async cache population for next time
|
|
823
|
+
val shouldFetch = synchronized(ongoingCacheFetches) {
|
|
824
|
+
ongoingCacheFetches.add(url)
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (shouldFetch) {
|
|
828
|
+
launch(Dispatchers.IO) {
|
|
829
|
+
try {
|
|
830
|
+
cacheManager.getCachedResponse(url, headers)
|
|
831
|
+
if (BuildConfig.DEBUG) {
|
|
832
|
+
Log.d(TAG, "✅ WEBVIEW_CLIENT: Async cache fetch completed: $url")
|
|
833
|
+
}
|
|
834
|
+
} catch (e: Exception) {
|
|
835
|
+
if (BuildConfig.DEBUG) {
|
|
836
|
+
Log.e(TAG, "❌ WEBVIEW_CLIENT: Async cache fetch failed: $url - ${e.message}")
|
|
837
|
+
}
|
|
838
|
+
} finally {
|
|
839
|
+
synchronized(ongoingCacheFetches) {
|
|
840
|
+
ongoingCacheFetches.remove(url)
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Return null to let network handle this request
|
|
847
|
+
return null
|
|
464
848
|
}
|
|
849
|
+
} catch (e: OutOfMemoryError) {
|
|
850
|
+
Log.e(TAG, "📢 Out of memory processing cache request: $url", e)
|
|
851
|
+
metricsMonitor.recordCacheMiss(url)
|
|
852
|
+
System.gc()
|
|
853
|
+
return null
|
|
854
|
+
} catch (e: SecurityException) {
|
|
855
|
+
Log.e(TAG, "🚫 Security error in cache processing: $url", e)
|
|
856
|
+
metricsMonitor.recordCacheMiss(url)
|
|
857
|
+
return null
|
|
858
|
+
} catch (e: Exception) {
|
|
859
|
+
Log.e(TAG, "❌ Unexpected error processing cache request: $url: ${e.message}")
|
|
860
|
+
if (BuildConfig.DEBUG) {
|
|
861
|
+
e.printStackTrace()
|
|
862
|
+
}
|
|
863
|
+
metricsMonitor.recordCacheMiss(url)
|
|
864
|
+
return null
|
|
865
|
+
} finally {
|
|
866
|
+
enableHardwareAcceleration()
|
|
465
867
|
}
|
|
466
868
|
} else {
|
|
467
869
|
if (BuildConfig.DEBUG) {
|
|
468
|
-
Log.d(TAG, "
|
|
870
|
+
Log.d(TAG, "❌ STEP_5_FAIL: URL doesn't match cache criteria, skipping cache")
|
|
871
|
+
Log.d(TAG, "❌ STEP_5_FAIL: URL: $url")
|
|
872
|
+
Log.d(TAG, "❌ STEP_5_FAIL: Request method: ${request.method} (need GET)")
|
|
873
|
+
Log.d(TAG, "❌ STEP_5_FAIL: shouldCacheUrl result: ${shouldCacheUrl(url)}")
|
|
874
|
+
Log.d(TAG, "❌ STEP_5_FAIL: This request will go through normal network loading")
|
|
469
875
|
}
|
|
470
876
|
}
|
|
471
877
|
|
|
@@ -474,9 +880,13 @@ class CustomWebView(
|
|
|
474
880
|
|
|
475
881
|
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
|
476
882
|
super.onPageStarted(view, url, favicon)
|
|
883
|
+
if (url != null && url != offlineAssetUrl) {
|
|
884
|
+
offlinePageVisible = false
|
|
885
|
+
}
|
|
477
886
|
progressBar.visibility = View.VISIBLE
|
|
478
887
|
val startTime = System.currentTimeMillis()
|
|
479
888
|
view?.tag = startTime // Store start time for performance tracking
|
|
889
|
+
url?.let { metricsMonitor.trackPageLoadStart(it) }
|
|
480
890
|
if (BuildConfig.DEBUG) {
|
|
481
891
|
Log.d(TAG, "⏳ Page load started for: $url - Hardware Acceleration: $isHardwareAccelerationEnabled")
|
|
482
892
|
}
|
|
@@ -487,11 +897,26 @@ class CustomWebView(
|
|
|
487
897
|
if(!isInitialPageLoaded){
|
|
488
898
|
isInitialPageLoaded = true
|
|
489
899
|
}
|
|
900
|
+
url?.let { metricsMonitor.trackPageLoadEnd(it) }
|
|
490
901
|
if (BuildConfig.DEBUG) {
|
|
491
902
|
val startTime = view?.tag as? Long ?: return
|
|
492
903
|
val loadTime = System.currentTimeMillis() - startTime
|
|
493
904
|
Log.d(TAG, "✅ Page load finished for: $url - Load time: ${loadTime}ms - Hardware Acceleration: $isHardwareAccelerationEnabled")
|
|
494
905
|
Log.d(TAG, "📊 Asset loading stats: Attempted: $assetLoadAttempts, Failed: $assetLoadFailures (${String.format("%.1f", assetLoadFailures * 100.0 / assetLoadAttempts.coerceAtLeast(1))}%)")
|
|
906
|
+
Log.d(TAG, "📊 ${metricsMonitor.getCacheStats()}")
|
|
907
|
+
|
|
908
|
+
// Log files served from cache
|
|
909
|
+
val cachedFiles = metricsMonitor.getFilesServedFromCache()
|
|
910
|
+
if (cachedFiles.isNotEmpty()) {
|
|
911
|
+
Log.d(TAG, "📁 FILES_FROM_CACHE: ${cachedFiles.size} files served from cache on this page:")
|
|
912
|
+
cachedFiles.takeLast(5).forEach { cachedUrl ->
|
|
913
|
+
Log.d(TAG, "📁 ✅ ${cachedUrl.substringAfterLast("/")}")
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Log brief performance summary
|
|
918
|
+
Log.d(TAG, "📊 PERFORMANCE_SUMMARY: Page finished, showing current metrics")
|
|
919
|
+
metricsMonitor.logAllMetrics()
|
|
495
920
|
}
|
|
496
921
|
super.onPageFinished(view, url)
|
|
497
922
|
}
|
|
@@ -499,18 +924,45 @@ class CustomWebView(
|
|
|
499
924
|
override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
|
|
500
925
|
if (error != null) {
|
|
501
926
|
Log.e(TAG, "❌ Error loading ${request?.url}: ${error.errorCode} ${error.description}")
|
|
502
|
-
|
|
503
|
-
// Only show a fallback page for main frame errors in production
|
|
504
|
-
if (request?.isForMainFrame == true && !BuildConfig.DEBUG) {
|
|
505
|
-
try {
|
|
506
|
-
view?.loadUrl("file:///android_asset/build/public/error.html")
|
|
507
|
-
} catch (e: Exception) {
|
|
508
|
-
Log.e(TAG, "Failed to load error page: ${e.message}")
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
927
|
}
|
|
512
928
|
super.onReceivedError(view, request, error)
|
|
513
929
|
}
|
|
930
|
+
|
|
931
|
+
override fun onReceivedSslError(view: WebView?, handler: android.webkit.SslErrorHandler?, error: android.net.http.SslError?) {
|
|
932
|
+
if (error != null) {
|
|
933
|
+
val errorMsg = when (error.primaryError) {
|
|
934
|
+
android.net.http.SslError.SSL_EXPIRED -> "Certificate expired"
|
|
935
|
+
android.net.http.SslError.SSL_IDMISMATCH -> "Certificate hostname mismatch"
|
|
936
|
+
android.net.http.SslError.SSL_NOTYETVALID -> "Certificate not yet valid"
|
|
937
|
+
android.net.http.SslError.SSL_UNTRUSTED -> "Certificate authority not trusted"
|
|
938
|
+
android.net.http.SslError.SSL_DATE_INVALID -> "Certificate date invalid"
|
|
939
|
+
android.net.http.SslError.SSL_INVALID -> "Certificate invalid"
|
|
940
|
+
else -> "Unknown SSL error"
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
Log.e(TAG, "🔒 SSL Error for ${error.url}: $errorMsg")
|
|
944
|
+
|
|
945
|
+
// NEVER proceed on SSL errors in production
|
|
946
|
+
// Only allow in debug mode for development/testing with local certificates
|
|
947
|
+
if (BuildConfig.DEBUG) {
|
|
948
|
+
Log.w(TAG, "⚠️ DEBUG MODE: Ignoring SSL error - DO NOT DO THIS IN PRODUCTION!")
|
|
949
|
+
handler?.proceed()
|
|
950
|
+
} else {
|
|
951
|
+
// Cancel the request in production
|
|
952
|
+
handler?.cancel()
|
|
953
|
+
|
|
954
|
+
// TODO: Add HTML file workflow - error.html not available yet
|
|
955
|
+
// Show error page for main frame SSL errors
|
|
956
|
+
// try {
|
|
957
|
+
// view?.loadUrl("file:///android_asset/build/public/error.html")
|
|
958
|
+
// } catch (e: Exception) {
|
|
959
|
+
// Log.e(TAG, "Failed to load SSL error page: ${e.message}")
|
|
960
|
+
// }
|
|
961
|
+
}
|
|
962
|
+
} else {
|
|
963
|
+
handler?.cancel()
|
|
964
|
+
}
|
|
965
|
+
}
|
|
514
966
|
}
|
|
515
967
|
|
|
516
968
|
webView.webChromeClient = object : WebChromeClient() {
|
|
@@ -527,9 +979,38 @@ class CustomWebView(
|
|
|
527
979
|
}
|
|
528
980
|
return true
|
|
529
981
|
}
|
|
982
|
+
|
|
983
|
+
override fun onPermissionRequest(request: PermissionRequest) {
|
|
984
|
+
if (BuildConfig.DEBUG) {
|
|
985
|
+
Log.d(TAG, "Permission request from web content: ${request.resources?.joinToString()}")
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Check if camera permission is being requested
|
|
989
|
+
val cameraRequested = request.resources?.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE) == true
|
|
990
|
+
|
|
991
|
+
if (cameraRequested) {
|
|
992
|
+
// Check if native app has camera permission
|
|
993
|
+
if (CameraUtils.hasCameraPermission(context as android.app.Activity)) {
|
|
994
|
+
if (BuildConfig.DEBUG) {
|
|
995
|
+
Log.d(TAG, "Granting camera permission to WebView - native permission available")
|
|
996
|
+
}
|
|
997
|
+
request.grant(request.resources)
|
|
998
|
+
} else {
|
|
999
|
+
if (BuildConfig.DEBUG) {
|
|
1000
|
+
Log.d(TAG, "Denying camera permission to WebView - native permission not granted")
|
|
1001
|
+
}
|
|
1002
|
+
request.deny()
|
|
1003
|
+
}
|
|
1004
|
+
} else {
|
|
1005
|
+
if (BuildConfig.DEBUG) {
|
|
1006
|
+
Log.d(TAG, "Denying non-camera permission request: ${request.resources?.joinToString()}")
|
|
1007
|
+
}
|
|
1008
|
+
request.deny()
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
530
1011
|
}
|
|
531
1012
|
|
|
532
1013
|
// Only enable WebView debugging in debug builds
|
|
533
1014
|
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
|
534
1015
|
}
|
|
535
|
-
}
|
|
1016
|
+
}
|