From 2a0a7362b0877d036327660226221722db86138d Mon Sep 17 00:00:00 2001 From: nian1 Date: Sun, 25 Aug 2024 20:55:53 +0800 Subject: [PATCH] feat(logger): Implement a custom logger with configurable log levels --- app/proguard-rules.pro | 9 +++ .../ding1ding/jsbridge/app/MainActivity.kt | 21 +++-- library/consumer-rules.pro | 8 -- .../java/com/ding1ding/jsbridge/JsonUtils.kt | 5 +- .../java/com/ding1ding/jsbridge/Logger.kt | 77 +++++++++++++++++++ .../com/ding1ding/jsbridge/MessageHandler.kt | 4 +- .../ding1ding/jsbridge/MessageSerializer.kt | 5 +- .../jsbridge/WebViewJavascriptBridge.kt | 60 +++++++-------- 8 files changed, 136 insertions(+), 53 deletions(-) create mode 100644 library/src/main/java/com/ding1ding/jsbridge/Logger.kt diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index e946aff..c269c05 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,3 +1,12 @@ +# This will strip `Log.v`, `Log.d`, and `Log.i` statements and will leave `Log.w` and `Log.e` statements intact. +#-assumenosideeffects class android.util.Log { +# public static boolean isLoggable(java.lang.String, int); +# public static int v(...); +# public static int d(...); +# public static int i(...); +#} + +# JavascriptInterface -keepattributes *Annotation* -keepattributes *JavascriptInterface* -keepclassmembers class * { diff --git a/app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt b/app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt index ac6d801..736d7b2 100644 --- a/app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt +++ b/app/src/main/java/com/ding1ding/jsbridge/app/MainActivity.kt @@ -2,7 +2,6 @@ package com.ding1ding.jsbridge.app import android.annotation.SuppressLint import android.os.Bundle -import android.util.Log import android.view.KeyEvent import android.view.View import android.webkit.WebView @@ -11,6 +10,7 @@ import android.widget.LinearLayout import androidx.appcompat.app.AppCompatActivity import com.ding1ding.jsbridge.Callback import com.ding1ding.jsbridge.ConsolePipe +import com.ding1ding.jsbridge.Logger import com.ding1ding.jsbridge.MessageHandler import com.ding1ding.jsbridge.WebViewJavascriptBridge import com.ding1ding.jsbridge.model.Person @@ -27,6 +27,13 @@ class MainActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + if (BuildConfig.DEBUG) { + WebViewJavascriptBridge.setLogLevel(Logger.LogLevel.DEBUG) + } else { + WebViewJavascriptBridge.setLogLevel(Logger.LogLevel.ERROR) + } + setupWebView() setupClickListeners() } @@ -56,7 +63,7 @@ class MainActivity : override fun onDestroy() { releaseWebView() - Log.d(TAG, "onDestroy") + Logger.d(TAG) { "onDestroy" } super.onDestroy() } @@ -93,7 +100,7 @@ class MainActivity : bridge = WebViewJavascriptBridge.create(this, webView, lifecycle).apply { consolePipe = object : ConsolePipe { override fun post(message: String) { - Log.d("[console.log]", message) + Logger.d("[console.log]") { message } } } @@ -110,17 +117,17 @@ class MainActivity : private fun createWebViewClient() = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) { - Log.d(TAG, "onPageStarted") + Logger.d(TAG) { "onPageStarted" } } override fun onPageFinished(view: WebView?, url: String?) { - Log.d(TAG, "onPageFinished") + Logger.d(TAG) { "onPageFinished" } } } private fun createDeviceLoadHandler() = object : MessageHandler, Any> { override fun handle(parameter: Map): Any { - Log.d(TAG, "DeviceLoadJavascriptSuccess, $parameter") + Logger.d(TAG) { "DeviceLoadJavascriptSuccess, $parameter" } return mapOf("result" to "Android") } } @@ -151,7 +158,7 @@ class MainActivity : Person("Wukong", 23), object : Callback { override fun onResult(result: Any) { - Log.d(TAG, "$handlerName, $result") + Logger.d(TAG) { "$handlerName, $result" } } }, ) diff --git a/library/consumer-rules.pro b/library/consumer-rules.pro index 9b188df..2187ddc 100644 --- a/library/consumer-rules.pro +++ b/library/consumer-rules.pro @@ -1,10 +1,2 @@ -# This will strip `Log.v`, `Log.d`, and `Log.i` statements and will leave `Log.w` and `Log.e` statements intact. --assumenosideeffects class android.util.Log { - public static boolean isLoggable(java.lang.String, int); - public static int v(...); - public static int d(...); - public static int i(...); -} - -keep class java.lang.reflect.** { *; } -keep class kotlin.reflect.** { *; } diff --git a/library/src/main/java/com/ding1ding/jsbridge/JsonUtils.kt b/library/src/main/java/com/ding1ding/jsbridge/JsonUtils.kt index ff75b92..5f63070 100644 --- a/library/src/main/java/com/ding1ding/jsbridge/JsonUtils.kt +++ b/library/src/main/java/com/ding1ding/jsbridge/JsonUtils.kt @@ -1,6 +1,5 @@ package com.ding1ding.jsbridge -import android.util.Log import java.math.BigInteger import java.text.SimpleDateFormat import java.util.Date @@ -31,7 +30,7 @@ object JsonUtils { field.name to field.get(any) }.toJsonObject().toString() } catch (e: Exception) { - Log.e("[JsBridge]", "Failed to serialize object of type ${any::class.java.simpleName}", e) + Logger.e(e) { "Failed to serialize object of type ${any::class.java.simpleName}" } JSONObject.quote(any.toString()) } } @@ -68,7 +67,7 @@ object JsonUtils { else -> parseNumber(json) } } catch (e: Exception) { - Log.e("[JsBridge]", "Error parsing JSON: $json", e) + Logger.e(e) { "Error parsing JSON: $json" } json // Return the original string if parsing fails } diff --git a/library/src/main/java/com/ding1ding/jsbridge/Logger.kt b/library/src/main/java/com/ding1ding/jsbridge/Logger.kt new file mode 100644 index 0000000..5e9a510 --- /dev/null +++ b/library/src/main/java/com/ding1ding/jsbridge/Logger.kt @@ -0,0 +1,77 @@ +package com.ding1ding.jsbridge + +import android.util.Log +import kotlin.math.min + +object Logger { + private const val MAX_LOG_LENGTH = 4000 + const val DEFAULT_TAG = "WebViewJsBridge" + + @Volatile + var logLevel = LogLevel.INFO + + enum class LogLevel { VERBOSE, DEBUG, INFO, WARN, ERROR, NONE } + + @JvmStatic + inline fun v(tag: String = DEFAULT_TAG, message: () -> String) = + log(LogLevel.VERBOSE, tag, message) + + @JvmStatic + inline fun d(tag: String = DEFAULT_TAG, message: () -> String) = log(LogLevel.DEBUG, tag, message) + + @JvmStatic + inline fun i(tag: String = DEFAULT_TAG, message: () -> String) = log(LogLevel.INFO, tag, message) + + @JvmStatic + inline fun w(tag: String = DEFAULT_TAG, message: () -> String) = log(LogLevel.WARN, tag, message) + + @JvmStatic + inline fun e(tag: String = DEFAULT_TAG, message: () -> String) = log(LogLevel.ERROR, tag, message) + + @JvmStatic + inline fun e(throwable: Throwable, tag: String = DEFAULT_TAG, message: () -> String = { "" }) { + if (logLevel <= LogLevel.ERROR) { + val fullMessage = buildString { + append(message()) + if (isNotEmpty() && message().isNotEmpty()) append(": ") + append(Log.getStackTraceString(throwable)) + } + logInternal(Log.ERROR, tag, fullMessage) + } + } + + @JvmStatic + inline fun log(level: LogLevel, tag: String = DEFAULT_TAG, message: () -> String) { + if (logLevel <= level) logInternal(level.toAndroidLogLevel(), tag, message()) + } + + fun logInternal(priority: Int, tag: String, message: String) { + if (message.length < MAX_LOG_LENGTH) { + Log.println(priority, tag, message) + return + } + + var i = 0 + val length = message.length + while (i < length) { + var newline = message.indexOf('\n', i) + newline = if (newline != -1) newline else length + do { + val end = min(newline, i + MAX_LOG_LENGTH) + val part = message.substring(i, end) + Log.println(priority, tag, part) + i = end + } while (i < newline) + i++ + } + } + + fun LogLevel.toAndroidLogLevel() = when (this) { + LogLevel.VERBOSE -> Log.VERBOSE + LogLevel.DEBUG -> Log.DEBUG + LogLevel.INFO -> Log.INFO + LogLevel.WARN -> Log.WARN + LogLevel.ERROR -> Log.ERROR + LogLevel.NONE -> Log.ASSERT + } +} diff --git a/library/src/main/java/com/ding1ding/jsbridge/MessageHandler.kt b/library/src/main/java/com/ding1ding/jsbridge/MessageHandler.kt index 6d877a5..4dd4009 100644 --- a/library/src/main/java/com/ding1ding/jsbridge/MessageHandler.kt +++ b/library/src/main/java/com/ding1ding/jsbridge/MessageHandler.kt @@ -1,5 +1,5 @@ package com.ding1ding.jsbridge -interface MessageHandler { - fun handle(parameter: InputType): OutputType +interface MessageHandler { + fun handle(parameter: Input): Output } diff --git a/library/src/main/java/com/ding1ding/jsbridge/MessageSerializer.kt b/library/src/main/java/com/ding1ding/jsbridge/MessageSerializer.kt index 29bfeab..aa51ada 100644 --- a/library/src/main/java/com/ding1ding/jsbridge/MessageSerializer.kt +++ b/library/src/main/java/com/ding1ding/jsbridge/MessageSerializer.kt @@ -1,6 +1,5 @@ package com.ding1ding.jsbridge -import android.util.Log import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import org.json.JSONObject @@ -95,7 +94,7 @@ object MessageSerializer { ) } } catch (e: Exception) { - Log.e("[JsBridge]", "Error deserializing message: ${e.message}") + Logger.e(e) { "Error deserializing message: ${e.message}" } ResponseMessage(null, null, null, null, null) } @@ -131,7 +130,7 @@ object MessageSerializer { constructor.isAccessible = true constructor.newInstance(*data.values.toTypedArray()) } catch (e: Exception) { - Log.e("[JsBridge]", "Error creating instance of ${clazz.simpleName}: ${e.message}") + Logger.e(e) { "Error creating instance of ${clazz.simpleName}: ${e.message}" } data } } diff --git a/library/src/main/java/com/ding1ding/jsbridge/WebViewJavascriptBridge.kt b/library/src/main/java/com/ding1ding/jsbridge/WebViewJavascriptBridge.kt index 86fb620..62f22bd 100644 --- a/library/src/main/java/com/ding1ding/jsbridge/WebViewJavascriptBridge.kt +++ b/library/src/main/java/com/ding1ding/jsbridge/WebViewJavascriptBridge.kt @@ -2,7 +2,6 @@ package com.ding1ding.jsbridge import android.annotation.SuppressLint import android.content.Context -import android.util.Log import android.webkit.JavascriptInterface import android.webkit.WebView import android.webkit.WebViewClient @@ -41,7 +40,7 @@ class WebViewJavascriptBridge private constructor( webView.settings.javaScriptEnabled = true webView.addJavascriptInterface(JsBridgeInterface(), "normalPipe") webView.addJavascriptInterface(JsBridgeInterface(), "consolePipe") - Log.d(TAG, "Bridge setup completed") + Logger.d { "Bridge setup completed" } } private fun setupWebViewClient() { @@ -49,7 +48,7 @@ class WebViewJavascriptBridge private constructor( override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) isWebViewReady.set(true) - Log.d(TAG, "WebView page finished loading") + Logger.d { "WebView page finished loading" } injectJavascriptIfNeeded() } } @@ -58,35 +57,34 @@ class WebViewJavascriptBridge private constructor( @MainThread private fun injectJavascriptIfNeeded() { if (isInjected.get() || !isWebViewReady.get()) { - Log.d( - TAG, - "JavaScript injection skipped. Injected: ${isInjected.get()}, WebView ready: ${isWebViewReady.get()}", - ) + Logger.d { + "JavaScript injection skipped. Injected: ${isInjected.get()}, WebView ready: ${isWebViewReady.get()}" + } return } - Log.d(TAG, "Injecting JavaScript") + Logger.d { "Injecting JavaScript" } webView.post { webView.evaluateJavascript("javascript:$bridgeScript", null) webView.evaluateJavascript("javascript:$consoleHookScript", null) isInjected.set(true) - Log.d(TAG, "JavaScript injection completed") + Logger.d { "JavaScript injection completed" } } } fun registerHandler(handlerName: String, messageHandler: MessageHandler<*, *>) { messageHandlers[handlerName] = messageHandler - Log.d(TAG, "Handler registered: $handlerName") + Logger.d { "Handler registered: $handlerName" } } fun removeHandler(handlerName: String) { messageHandlers.remove(handlerName) - Log.d(TAG, "Handler removed: $handlerName") + Logger.d { "Handler removed: $handlerName" } } @JvmOverloads fun callHandler(handlerName: String, data: Any? = null, callback: Callback<*>? = null) { if (!isInjected.get()) { - Log.e(TAG, "Bridge is not injected. Cannot call handler: $handlerName") + Logger.e { "Bridge is not injected. Cannot call handler: $handlerName" } return } val callbackId = callback?.let { "native_cb_${uniqueId.incrementAndGet()}" } @@ -95,7 +93,7 @@ class WebViewJavascriptBridge private constructor( val message = CallMessage(handlerName, data, callbackId) val messageString = MessageSerializer.serializeCallMessage(message) dispatchMessage(messageString) - Log.d(TAG, "Handler called: $handlerName") + Logger.d { "Handler called: $handlerName" } } private fun processMessage(messageString: String) { @@ -110,7 +108,7 @@ class WebViewJavascriptBridge private constructor( else -> handleRequest(message) } } catch (e: Exception) { - Log.e(TAG, "Error processing message: ${e.message}") + Logger.e(e) { "Error processing message" } } } @@ -119,7 +117,7 @@ class WebViewJavascriptBridge private constructor( if (callback is Callback<*>) { @Suppress("UNCHECKED_CAST") (callback as Callback).onResult(responseMessage.responseData) - Log.d(TAG, "Response handled for ID: ${responseMessage.responseId}") + Logger.d { "Response handled for ID: ${responseMessage.responseId}" } } } @@ -133,10 +131,10 @@ class WebViewJavascriptBridge private constructor( val response = ResponseMessage(callbackId, responseData, null, null, null) val responseString = MessageSerializer.serializeResponseMessage(response) dispatchMessage(responseString) - Log.d(TAG, "Request handled: ${message.handlerName}") + Logger.d { "Request handled: ${message.handlerName}" } } } else { - Log.e(TAG, "No handler found for: ${message.handlerName}") + Logger.e { "No handler found for: ${message.handlerName}" } } } @@ -144,14 +142,14 @@ class WebViewJavascriptBridge private constructor( val script = "WebViewJavascriptBridge.handleMessageFromNative('$messageString');" webView.post { webView.evaluateJavascript(script, null) - Log.d(TAG, "Message dispatched to JavaScript") + Logger.d { "Message dispatched to JavaScript" } } } private fun loadAsset(fileName: String): String = try { context.assets.open(fileName).bufferedReader().use { it.readText() } } catch (e: Exception) { - Log.e(TAG, "Error loading asset $fileName: ${e.message}") + Logger.e(e) { "Error loading asset $fileName" } "" }.trimIndent() @@ -160,13 +158,13 @@ class WebViewJavascriptBridge private constructor( uniqueId.set(0) isInjected.set(false) isWebViewReady.set(false) - Log.d(TAG, "Bridge state cleared") + Logger.d { "Bridge state cleared" } } private fun removeJavascriptInterface() { webView.removeJavascriptInterface("normalPipe") webView.removeJavascriptInterface("consolePipe") - Log.d(TAG, "JavaScript interfaces removed") + Logger.d { "JavaScript interfaces removed" } } private fun release() { @@ -175,23 +173,23 @@ class WebViewJavascriptBridge private constructor( responseCallbacks.clear() messageHandlers.clear() clearState() - Log.d(TAG, "Bridge released") + Logger.d { "Bridge released" } } fun reinitialize() { release() setupBridge() setupWebViewClient() - Log.d(TAG, "Bridge reinitialized") + Logger.d { "Bridge reinitialized" } } override fun onResume(owner: LifecycleOwner) { - Log.d(TAG, "onResume") + Logger.d { "onResume" } injectJavascriptIfNeeded() } override fun onDestroy(owner: LifecycleOwner) { - Log.d(TAG, "onDestroy") + Logger.d { "onDestroy" } release() } @@ -199,28 +197,30 @@ class WebViewJavascriptBridge private constructor( @JavascriptInterface fun postMessage(data: String?) { data?.let { - Log.d(TAG, "Message received from JavaScript: $it") + Logger.d { "Message received from JavaScript: $it" } processMessage(it) } } @JavascriptInterface fun receiveConsole(data: String?) { - Log.d(TAG, "Console message received: $data") + Logger.d { "Console message received: $data" } consolePipe?.post(data.orEmpty()) } } companion object { - private const val TAG = "WebViewJsBridge" - fun create( context: Context, webView: WebView, lifecycle: Lifecycle? = null, ): WebViewJavascriptBridge = WebViewJavascriptBridge(context, webView).also { bridge -> lifecycle?.addObserver(bridge) - Log.d(TAG, "Bridge created and lifecycle observer added") + Logger.d { "Bridge created and lifecycle observer added" } + } + + fun setLogLevel(level: Logger.LogLevel) { + Logger.logLevel = level } } }