feat: 更新 WebView 数据目录,添加持久存储清理逻辑;优化 URL 生成和 H5 路由刷新时的品牌保持
This commit is contained in:
@@ -13,6 +13,7 @@ import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.webkit.ServiceWorkerController
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebStorage
|
||||
import android.webkit.WebView
|
||||
import androidx.core.content.FileProvider
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
@@ -30,10 +31,14 @@ class MainActivity : FlutterActivity() {
|
||||
private val WEBVIEW_CACHE_CHANNEL = "io.openim.flutter.im_webview_app/webview_cache"
|
||||
private val TAG = "MainActivity"
|
||||
private val MAX_BRANDING_ICON_SIZE = 192
|
||||
private val WEBVIEW_DATA_DIRECTORY_SUFFIX = "h5_shell_fresh_profile_v2"
|
||||
private val WEBVIEW_DATA_DIRECTORY_SUFFIX = "h5_shell_fresh_profile_v3"
|
||||
private val WEBVIEW_STORAGE_RESET_PREFS = "h5_shell_webview_storage"
|
||||
private val WEBVIEW_STORAGE_RESET_KEY = "reset_version"
|
||||
private val WEBVIEW_STORAGE_RESET_VERSION = 3
|
||||
|
||||
override fun onCreate(savedInstanceState: android.os.Bundle?) {
|
||||
configureWebViewDataDirectory()
|
||||
clearWebViewPersistentStorageOnce()
|
||||
// 华为/荣耀/OPPO 等国产设备:在任意网络请求之前同步安装 Conscrypt,修复 SSL 握手失败(无 GMS 时系统 SSL 实现不完整)
|
||||
try {
|
||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||
@@ -150,6 +155,46 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearWebViewPersistentStorageOnce() {
|
||||
val prefs = getSharedPreferences(WEBVIEW_STORAGE_RESET_PREFS, MODE_PRIVATE)
|
||||
if (prefs.getInt(WEBVIEW_STORAGE_RESET_KEY, 0) >= WEBVIEW_STORAGE_RESET_VERSION) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
WebStorage.getInstance().deleteAllData()
|
||||
Log.d(TAG, "WebView persistent storage cleared")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "WebView persistent storage clear failed: ${e.message}")
|
||||
}
|
||||
|
||||
clearKnownWebViewCacheDirectories()
|
||||
prefs.edit().putInt(WEBVIEW_STORAGE_RESET_KEY, WEBVIEW_STORAGE_RESET_VERSION).apply()
|
||||
}
|
||||
|
||||
private fun clearKnownWebViewCacheDirectories() {
|
||||
val cacheDirectoryNames = listOf(
|
||||
"WebView",
|
||||
"webview",
|
||||
"org.chromium.android_webview",
|
||||
"com.android.webview"
|
||||
)
|
||||
|
||||
for (name in cacheDirectoryNames) {
|
||||
val directory = File(cacheDir, name)
|
||||
if (!directory.exists()) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
if (!directory.deleteRecursively()) {
|
||||
Log.w(TAG, "WebView cache directory delete returned false: ${directory.absolutePath}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "WebView cache directory delete failed: ${directory.absolutePath}, ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readLongArgument(value: Any?): Long? {
|
||||
return when (value) {
|
||||
is Int -> value.toLong()
|
||||
|
||||
@@ -21,33 +21,14 @@ class AppConfig {
|
||||
final host = environmentHosts.isNotEmpty
|
||||
? environmentHosts.first
|
||||
: 'h5-im.imharry.work';
|
||||
return _withShellBranding(
|
||||
return withFreshShellParams(
|
||||
_normalizeHomeUrl(host),
|
||||
appName: appName,
|
||||
appLogo: appLogo,
|
||||
);
|
||||
}
|
||||
|
||||
static String withFreshShellCacheBust(String url) {
|
||||
final uri = Uri.parse(url);
|
||||
final queryParameters = Map<String, String>.from(uri.queryParameters)
|
||||
..['shell_cache_bust'] = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
return uri.replace(queryParameters: queryParameters).toString();
|
||||
}
|
||||
|
||||
static String _normalizeHomeUrl(String host) {
|
||||
final value = host.trim();
|
||||
final normalized =
|
||||
value.startsWith('http://') || value.startsWith('https://')
|
||||
? value
|
||||
: 'https://$value';
|
||||
final uri = Uri.parse(normalized);
|
||||
final path = uri.path.isEmpty ? '/' : uri.path;
|
||||
return uri.replace(path: path).toString();
|
||||
}
|
||||
|
||||
static String _withShellBranding(
|
||||
static String withFreshShellParams(
|
||||
String url, {
|
||||
String? appName,
|
||||
String? appLogo,
|
||||
@@ -77,4 +58,23 @@ class AppConfig {
|
||||
)
|
||||
.toString();
|
||||
}
|
||||
|
||||
static String withFreshShellCacheBust(String url) {
|
||||
final uri = Uri.parse(url);
|
||||
final queryParameters = Map<String, String>.from(uri.queryParameters)
|
||||
..['shell_cache_bust'] = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
return uri.replace(queryParameters: queryParameters).toString();
|
||||
}
|
||||
|
||||
static String _normalizeHomeUrl(String host) {
|
||||
final value = host.trim();
|
||||
final normalized =
|
||||
value.startsWith('http://') || value.startsWith('https://')
|
||||
? value
|
||||
: 'https://$value';
|
||||
final uri = Uri.parse(normalized);
|
||||
final path = uri.path.isEmpty ? '/' : uri.path;
|
||||
return uri.replace(path: path).toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,45 @@ const _purgeWebRuntimeCacheScript = r'''
|
||||
});
|
||||
})();
|
||||
''';
|
||||
const _inspectH5SnapshotScript = r'''
|
||||
(() => {
|
||||
const toAbsoluteUrl = (value) => {
|
||||
try {
|
||||
return new URL(value, window.location.href).href;
|
||||
} catch (_) {
|
||||
return value || '';
|
||||
}
|
||||
};
|
||||
|
||||
const shrinkAssetUrl = (value) => {
|
||||
const absolute = toAbsoluteUrl(value);
|
||||
const match = absolute.match(/\/assets\/[^?#]+/);
|
||||
return match ? match[0] : absolute;
|
||||
};
|
||||
|
||||
const scripts = Array.from(document.scripts)
|
||||
.map((script) => script.src)
|
||||
.filter(Boolean)
|
||||
.map(shrinkAssetUrl);
|
||||
const links = Array.from(document.querySelectorAll('link[href]'))
|
||||
.map((link) => link.href)
|
||||
.filter(Boolean)
|
||||
.map(shrinkAssetUrl);
|
||||
const bodyText = (document.body?.innerText || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.slice(0, 300);
|
||||
|
||||
return JSON.stringify({
|
||||
type: 'h5Snapshot',
|
||||
href: window.location.href,
|
||||
title: document.title,
|
||||
scripts,
|
||||
links,
|
||||
bodyText,
|
||||
userAgent: navigator.userAgent,
|
||||
});
|
||||
})();
|
||||
''';
|
||||
const _shellMediaPermissionBridgeScript = r'''
|
||||
(() => {
|
||||
if (window.__openimShellMediaBridgeInstalled) {
|
||||
@@ -395,7 +434,10 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
|
||||
final platformController = controller.platform;
|
||||
if (platformController is AndroidWebViewController) {
|
||||
AndroidWebViewController.enableDebugging(false);
|
||||
assert(() {
|
||||
AndroidWebViewController.enableDebugging(true);
|
||||
return true;
|
||||
}());
|
||||
unawaited(platformController.setMediaPlaybackRequiresUserGesture(false));
|
||||
unawaited(platformController.setGeolocationEnabled(true));
|
||||
unawaited(
|
||||
@@ -432,6 +474,7 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
unawaited(openAppSettings());
|
||||
}
|
||||
if (type == 'runtimeCachePurged' && payload != null) {
|
||||
debugPrint('[H5Shell] runtime cache purged: ${jsonEncode(payload)}');
|
||||
unawaited(_reloadAfterRuntimeCachePurge(payload['url'] as String?));
|
||||
}
|
||||
if (type == 'requestMediaPermissions' && payload != null) {
|
||||
@@ -512,7 +555,11 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
try {
|
||||
return AppConfig.withFreshShellCacheBust(value);
|
||||
return AppConfig.withFreshShellParams(
|
||||
value,
|
||||
appName: widget.shellBranding.appName,
|
||||
appLogo: widget.shellBranding.appLogo,
|
||||
);
|
||||
} catch (_) {
|
||||
return _freshHomeUrl();
|
||||
}
|
||||
@@ -527,6 +574,7 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
|
||||
unawaited(_syncShellBranding());
|
||||
unawaited(_injectShellMediaPermissionBridge());
|
||||
unawaited(_logLoadedH5Snapshot());
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_progress = 100;
|
||||
@@ -635,6 +683,35 @@ class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
return _runJavaScriptSafely(_stopWebMediaScript);
|
||||
}
|
||||
|
||||
Future<void> _logLoadedH5Snapshot() async {
|
||||
try {
|
||||
final result = await _controller.runJavaScriptReturningResult(
|
||||
_inspectH5SnapshotScript,
|
||||
);
|
||||
debugPrint(
|
||||
'[H5Shell] loaded H5 snapshot: ${_decodeJavaScriptStringResult(result)}',
|
||||
);
|
||||
} catch (error) {
|
||||
debugPrint('[H5Shell] loaded H5 snapshot failed: $error');
|
||||
}
|
||||
}
|
||||
|
||||
String _decodeJavaScriptStringResult(Object? result) {
|
||||
if (result == null) {
|
||||
return '';
|
||||
}
|
||||
if (result is String) {
|
||||
try {
|
||||
final decoded = jsonDecode(result);
|
||||
if (decoded is String) {
|
||||
return decoded;
|
||||
}
|
||||
} catch (_) {}
|
||||
return result;
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
Future<void> _loadHome() async {
|
||||
await _loadUrl(_freshHomeUrl());
|
||||
}
|
||||
|
||||
@@ -28,4 +28,23 @@ void main() {
|
||||
expect(uri.toString(), isNot(contains('%25')));
|
||||
expect(Uri.splitQueryString(uri.fragment)['shell_app_logo'], logo);
|
||||
});
|
||||
|
||||
test('keeps shell branding when refreshing an H5 route URL', () {
|
||||
const logo = 'data:image/png;base64,routeLogo+/=';
|
||||
|
||||
final uri = Uri.parse(
|
||||
AppConfig.withFreshShellParams(
|
||||
'https://h5-im.imharry.work/login?from=runtime',
|
||||
appName: 'Shell Route',
|
||||
appLogo: logo,
|
||||
),
|
||||
);
|
||||
|
||||
expect(uri.path, '/login');
|
||||
expect(uri.queryParameters['from'], 'runtime');
|
||||
expect(uri.queryParameters['flutter_shell'], '1');
|
||||
expect(uri.queryParameters['shell_app_name'], 'Shell Route');
|
||||
expect(uri.queryParameters['shell_cache_bust'], isNotEmpty);
|
||||
expect(Uri.splitQueryString(uri.fragment)['shell_app_logo'], logo);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user