chore: remove temporary H5 cache diagnostics

This commit is contained in:
Booker
2026-05-26 22:22:07 +07:00
parent 82b06f80b5
commit bd3ccf7f2d
4 changed files with 3 additions and 269 deletions

View File

@@ -11,34 +11,22 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Base64 import android.util.Base64
import android.util.Log 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 androidx.core.content.FileProvider
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import org.conscrypt.Conscrypt import org.conscrypt.Conscrypt
import java.security.Security import java.security.Security
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.webviewflutter.WebViewFlutterAndroidExternalApi
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
class MainActivity : FlutterActivity() { class MainActivity : FlutterActivity() {
private val CHANNEL = "io.openim.flutter.openim/apk_info" private val CHANNEL = "io.openim.flutter.openim/apk_info"
private val SHELL_BRANDING_CHANNEL = "io.openim.flutter.im_webview_app/shell_branding" private val SHELL_BRANDING_CHANNEL = "io.openim.flutter.im_webview_app/shell_branding"
private val WEBVIEW_CACHE_CHANNEL = "io.openim.flutter.im_webview_app/webview_cache"
private val TAG = "MainActivity" private val TAG = "MainActivity"
private val MAX_BRANDING_ICON_SIZE = 192 private val MAX_BRANDING_ICON_SIZE = 192
private val WEBVIEW_DATA_DIRECTORY_SUFFIX = "h5_shell_fresh_profile_v5"
private val WEBVIEW_STORAGE_RESET_PREFS = "h5_shell_webview_storage"
private val WEBVIEW_STORAGE_RESET_KEY = "reset_version"
private val WEBVIEW_STORAGE_RESET_VERSION = 5
override fun onCreate(savedInstanceState: android.os.Bundle?) { override fun onCreate(savedInstanceState: android.os.Bundle?) {
configureWebViewDataDirectory()
clearWebViewPersistentStorageOnce()
// 华为/荣耀/OPPO 等国产设备:在任意网络请求之前同步安装 Conscrypt修复 SSL 握手失败(无 GMS 时系统 SSL 实现不完整) // 华为/荣耀/OPPO 等国产设备:在任意网络请求之前同步安装 Conscrypt修复 SSL 握手失败(无 GMS 时系统 SSL 实现不完整)
try { try {
Security.insertProviderAt(Conscrypt.newProvider(), 1) Security.insertProviderAt(Conscrypt.newProvider(), 1)
@@ -119,109 +107,6 @@ class MainActivity : FlutterActivity() {
} }
} }
} }
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, WEBVIEW_CACHE_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"configureNoCache" -> {
val webViewId = readLongArgument(call.argument<Any>("webViewId"))
if (webViewId == null) {
result.error("INVALID_ARGUMENT", "webViewId 不能为空", null)
return@setMethodCallHandler
}
try {
result.success(configureWebViewNoCache(flutterEngine, webViewId))
} catch (e: Exception) {
result.error("ERROR", "无法配置 WebView 缓存策略: ${e.message}", null)
}
}
else -> {
result.notImplemented()
}
}
}
}
private fun configureWebViewDataDirectory() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return
}
try {
WebView.setDataDirectorySuffix(WEBVIEW_DATA_DIRECTORY_SUFFIX)
} catch (e: IllegalStateException) {
Log.w(TAG, "WebView data directory already initialized: ${e.message}")
} catch (e: Exception) {
Log.w(TAG, "WebView data directory configure failed: ${e.message}")
}
}
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()
is Long -> value
is Number -> value.toLong()
else -> null
}
}
private fun configureWebViewNoCache(flutterEngine: FlutterEngine, webViewId: Long): Boolean {
val webView = WebViewFlutterAndroidExternalApi.getWebView(flutterEngine, webViewId) ?: return false
webView.settings.cacheMode = WebSettings.LOAD_NO_CACHE
webView.clearCache(true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
ServiceWorkerController
.getInstance()
.serviceWorkerWebSettings
.cacheMode = WebSettings.LOAD_NO_CACHE
} catch (e: Exception) {
Log.w(TAG, "ServiceWorker cache mode configure failed: ${e.message}")
}
}
return true
} }
private fun getCurrentAppName(): String { private fun getCurrentAppName(): String {

View File

@@ -8,7 +8,7 @@ class AppConfig {
static const String appLogo = ''; static const String appLogo = '';
static const String canonicalWebHost = 'h5-im.imharry.work'; static const String canonicalWebHost = 'h5-im.imharry.work';
static const Set<String> legacyWebHosts = { static const Set<String> legacyWebHosts = {
'h5-im.imharry.work', 'h5-test.imharry.work',
}; };
static final Map<Environment, List<String>> _environmentHosts = { static final Map<Environment, List<String>> _environmentHosts = {
@@ -25,10 +25,7 @@ class AppConfig {
final host = final host =
environmentHosts.isNotEmpty ? environmentHosts.first : canonicalWebHost; environmentHosts.isNotEmpty ? environmentHosts.first : canonicalWebHost;
final uri = Uri.parse(_normalizeHomeUrl(host)); final uri = Uri.parse(_normalizeHomeUrl(host));
final queryParameters = Map<String, String>.from(uri.queryParameters) return uri.toString();
..['_h5_t'] = DateTime.now().millisecondsSinceEpoch.toString();
return uri.replace(queryParameters: queryParameters).toString();
} }
static String withFreshShellParams(String url) { static String withFreshShellParams(String url) {
@@ -52,14 +49,10 @@ class AppConfig {
return uri.toString(); return uri.toString();
} }
final queryParameters = Map<String, String>.from(uri.queryParameters)
..['_h5_t'] = DateTime.now().millisecondsSinceEpoch.toString();
return uri return uri
.replace( .replace(
scheme: 'https', scheme: 'https',
host: canonicalWebHost, host: canonicalWebHost,
queryParameters: queryParameters,
) )
.toString(); .toString();
} }

View File

@@ -11,48 +11,8 @@ import 'config/app_config.dart';
const _shellBackground = Color(0xFFF8FBFF); const _shellBackground = Color(0xFFF8FBFF);
const _shellAccent = Color(0xFF0089FF); const _shellAccent = Color(0xFF0089FF);
const _shellSubText = Color(0xFF8E9AB0); const _shellSubText = Color(0xFF8E9AB0);
const _showH5DebugOverlay = bool.fromEnvironment(
'H5_SHELL_DEBUG',
defaultValue: true,
);
const _shellBrandingChannel = const _shellBrandingChannel =
MethodChannel('io.openim.flutter.im_webview_app/shell_branding'); MethodChannel('io.openim.flutter.im_webview_app/shell_branding');
const _h5MountedCheckScript = r'''
(() => document.body?.classList.contains('app-mounted') === true)();
''';
const _h5SnapshotScript = r'''
(() => {
const shrinkAssetUrl = (value) => {
try {
const absolute = new URL(value, window.location.href).href;
const match = absolute.match(/\/assets\/[^?#]+/);
return match ? match[0] : absolute;
} catch (_) {
return value || '';
}
};
const scripts = Array.from(document.scripts)
.map((script) => script.src)
.filter(Boolean)
.map(shrinkAssetUrl)
.filter((src) => src.includes('/assets/'));
const bodyText = (document.body?.innerText || '')
.replace(/\s+/g, ' ')
.slice(0, 180);
let shellBrand = '';
try {
shellBrand = window.sessionStorage.getItem('OPENIM_FLUTTER_SHELL_BRAND') || '';
} catch (_) {}
return JSON.stringify({
href: window.location.href,
scripts,
bodyText,
shellBrand,
});
})();
''';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -137,7 +97,6 @@ class _H5ShellPageState extends State<H5ShellPage> {
int _progress = 0; int _progress = 0;
String? _loadError; String? _loadError;
String _h5DebugText = 'H5 loading...';
bool _showShellCover = true; bool _showShellCover = true;
bool _shellBrandingLoaded = false; bool _shellBrandingLoaded = false;
late ShellBranding _shellBranding; late ShellBranding _shellBranding;
@@ -202,9 +161,7 @@ class _H5ShellPageState extends State<H5ShellPage> {
Future<void> _handlePageFinished() async { Future<void> _handlePageFinished() async {
await _loadShellBrandingIfNeeded(); await _loadShellBrandingIfNeeded();
await _waitForH5Mounted();
await _syncShellBranding(); await _syncShellBranding();
await _updateH5DebugSnapshot();
if (mounted) { if (mounted) {
setState(() { setState(() {
_progress = 100; _progress = 100;
@@ -247,58 +204,6 @@ class _H5ShellPageState extends State<H5ShellPage> {
return _runJavaScriptSafely(script); return _runJavaScriptSafely(script);
} }
Future<void> _waitForH5Mounted() async {
for (var index = 0; index < 20; index += 1) {
try {
final result = await _controller.runJavaScriptReturningResult(
_h5MountedCheckScript,
);
if (result == true || result.toString() == 'true') {
return;
}
} catch (_) {}
await Future<void>.delayed(const Duration(milliseconds: 100));
}
}
Future<void> _updateH5DebugSnapshot() async {
if (!_showH5DebugOverlay) {
return;
}
try {
final result = await _controller.runJavaScriptReturningResult(
_h5SnapshotScript,
);
final snapshot = _decodeJavaScriptStringResult(result);
debugPrint('[H5Shell] snapshot: $snapshot');
if (mounted) {
setState(() => _h5DebugText = snapshot);
}
} catch (error) {
debugPrint('[H5Shell] snapshot failed: $error');
if (mounted) {
setState(() => _h5DebugText = '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 { Future<void> _loadHome() async {
await _loadUrl(_freshHomeUrl()); await _loadUrl(_freshHomeUrl());
} }
@@ -314,18 +219,9 @@ class _H5ShellPageState extends State<H5ShellPage> {
}); });
} }
await _clearWebViewHttpCache();
await _controller.loadRequest(Uri.parse(targetUrl)); await _controller.loadRequest(Uri.parse(targetUrl));
} }
Future<void> _clearWebViewHttpCache() async {
try {
await _controller.clearCache();
} catch (_) {
// Some WebView implementations can reject cache operations before first use.
}
}
Future<NavigationDecision> _handleNavigationRequest( Future<NavigationDecision> _handleNavigationRequest(
NavigationRequest request, NavigationRequest request,
) async { ) async {
@@ -392,15 +288,6 @@ class _H5ShellPageState extends State<H5ShellPage> {
message: _loadError!, message: _loadError!,
onRetry: () => unawaited(_loadHome()), onRetry: () => unawaited(_loadHome()),
), ),
if (_showH5DebugOverlay)
Positioned(
left: 8,
right: 8,
bottom: 10,
child: IgnorePointer(
child: _H5DebugOverlay(text: _h5DebugText),
),
),
], ],
), ),
), ),
@@ -450,36 +337,6 @@ class _ErrorPanel extends StatelessWidget {
} }
} }
class _H5DebugOverlay extends StatelessWidget {
const _H5DebugOverlay({required this.text});
final String text;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.72),
borderRadius: BorderRadius.circular(6),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Text(
text,
maxLines: 5,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 10,
height: 1.25,
letterSpacing: 0,
),
),
),
);
}
}
class _ShellFallback extends StatelessWidget { class _ShellFallback extends StatelessWidget {
const _ShellFallback(); const _ShellFallback();

View File

@@ -51,7 +51,7 @@ void main() {
test('rewrites legacy H5 host main-frame URLs to the canonical host', () { test('rewrites legacy H5 host main-frame URLs to the canonical host', () {
final uri = Uri.parse( final uri = Uri.parse(
AppConfig.canonicalizeMainFrameUrl( AppConfig.canonicalizeMainFrameUrl(
'https://h5-im.imharry.work/login?from=runtime' 'https://h5-test.imharry.work/login?from=runtime'
'&shell_app_name=Old#shell_app_logo=old', '&shell_app_name=Old#shell_app_logo=old',
), ),
); );
@@ -60,7 +60,6 @@ void main() {
expect(uri.host, 'h5-im.imharry.work'); expect(uri.host, 'h5-im.imharry.work');
expect(uri.path, '/login'); expect(uri.path, '/login');
expect(uri.queryParameters['from'], 'runtime'); expect(uri.queryParameters['from'], 'runtime');
expect(uri.queryParameters.containsKey('_h5_t'), isTrue);
expect(uri.queryParameters.containsKey('shell_app_name'), isFalse); expect(uri.queryParameters.containsKey('shell_app_name'), isFalse);
expect(Uri.splitQueryString(uri.fragment).containsKey('shell_app_logo'), expect(Uri.splitQueryString(uri.fragment).containsKey('shell_app_logo'),
isFalse); isFalse);