diff --git a/chat-android/build.gradle.kts b/chat-android/build.gradle.kts index 9f116f4..a33f902 100644 --- a/chat-android/build.gradle.kts +++ b/chat-android/build.gradle.kts @@ -27,6 +27,7 @@ android { } compileOptions { + isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } @@ -45,7 +46,7 @@ buildConfig { dependencies { api(libs.ably.android) implementation(libs.gson) - + coreLibraryDesugaring(libs.desugar.jdk.libs) testImplementation(libs.junit) testImplementation(libs.mockk) testImplementation(libs.coroutine.test) diff --git a/chat-android/src/main/java/com/ably/chat/ChatApi.kt b/chat-android/src/main/java/com/ably/chat/ChatApi.kt index 2bc74d9..42d9415 100644 --- a/chat-android/src/main/java/com/ably/chat/ChatApi.kt +++ b/chat-android/src/main/java/com/ably/chat/ChatApi.kt @@ -15,7 +15,11 @@ private const val PROTOCOL_VERSION_PARAM_NAME = "v" private const val RESERVED_ABLY_CHAT_KEY = "ably-chat" private val apiProtocolParam = Param(PROTOCOL_VERSION_PARAM_NAME, API_PROTOCOL_VERSION.toString()) -internal class ChatApi(private val realtimeClient: RealtimeClient, private val clientId: String) { +internal class ChatApi( + private val realtimeClient: RealtimeClient, + private val clientId: String, + private val logger: Logger, +) { /** * Get messages from the Chat Backend @@ -134,6 +138,17 @@ internal class ChatApi(private val realtimeClient: RealtimeClient, private val c } override fun onError(reason: ErrorInfo?) { + logger.error( + "ChatApi.makeAuthorizedRequest(); failed to make request", + context = LogContext( + contextMap = mapOf( + "url" to url, + "statusCode" to reason?.statusCode.toString(), + "errorCode" to reason?.code.toString(), + "errorMessage" to reason?.message.toString(), + ), + ), + ) // (CHA-M3e) continuation.resumeWithException(AblyException.fromErrorInfo(reason)) } @@ -159,6 +174,17 @@ internal class ChatApi(private val realtimeClient: RealtimeClient, private val c } override fun onError(reason: ErrorInfo?) { + logger.error( + "ChatApi.makeAuthorizedPaginatedRequest(); failed to make request", + context = LogContext( + contextMap = mapOf( + "url" to url, + "statusCode" to reason?.statusCode.toString(), + "errorCode" to reason?.code.toString(), + "errorMessage" to reason?.message.toString(), + ), + ), + ) continuation.resumeWithException(AblyException.fromErrorInfo(reason)) } }, diff --git a/chat-android/src/main/java/com/ably/chat/ChatClient.kt b/chat-android/src/main/java/com/ably/chat/ChatClient.kt index e933666..60befc8 100644 --- a/chat-android/src/main/java/com/ably/chat/ChatClient.kt +++ b/chat-android/src/main/java/com/ably/chat/ChatClient.kt @@ -45,7 +45,13 @@ internal class DefaultChatClient( override val clientOptions: ClientOptions, ) : ChatClient { - private val chatApi = ChatApi(realtime, clientId) + private val logger: Logger = if (clientOptions.logHandler != null) { + CustomLogger(clientOptions.logHandler, clientOptions.logLevel) + } else { + AndroidLogger(clientOptions.logLevel) + } + + private val chatApi = ChatApi(realtime, clientId, logger.withContext(LogContext(tag = "AblyChatAPI"))) override val rooms: Rooms = DefaultRooms( realtimeClient = realtime, diff --git a/chat-android/src/main/java/com/ably/chat/ClientOptions.kt b/chat-android/src/main/java/com/ably/chat/ClientOptions.kt index adc3312..62e89c9 100644 --- a/chat-android/src/main/java/com/ably/chat/ClientOptions.kt +++ b/chat-android/src/main/java/com/ably/chat/ClientOptions.kt @@ -1,7 +1,5 @@ package com.ably.chat -import io.ably.lib.util.Log.LogHandler - /** * Configuration options for the chat client. */ diff --git a/chat-android/src/main/java/com/ably/chat/Logger.kt b/chat-android/src/main/java/com/ably/chat/Logger.kt new file mode 100644 index 0000000..62ca022 --- /dev/null +++ b/chat-android/src/main/java/com/ably/chat/Logger.kt @@ -0,0 +1,108 @@ +package com.ably.chat + +import android.util.Log +import java.time.LocalDateTime + +fun interface LogHandler { + fun log(message: String, level: LogLevel, throwable: Throwable?, context: LogContext) +} + +data class LogContext(val tag: String? = null, val contextMap: Map = mapOf()) + +internal interface Logger { + val defaultContext: LogContext + fun withContext(additionalContext: LogContext): Logger + fun log(message: String, level: LogLevel, throwable: Throwable? = null, context: LogContext? = null) +} + +internal fun Logger.trace(message: String, throwable: Throwable? = null, context: LogContext? = null) { + log(message, LogLevel.Trace, throwable, context) +} + +internal fun Logger.debug(message: String, throwable: Throwable? = null, context: LogContext? = null) { + log(message, LogLevel.Debug, throwable, context) +} + +internal fun Logger.info(message: String, throwable: Throwable? = null, context: LogContext? = null) { + log(message, LogLevel.Info, throwable, context) +} + +internal fun Logger.warn(message: String, throwable: Throwable? = null, context: LogContext? = null) { + log(message, LogLevel.Info, throwable, context) +} + +internal fun Logger.error(message: String, throwable: Throwable? = null, context: LogContext? = null) { + log(message, LogLevel.Error, throwable, context) +} + +internal fun LogContext.mergeWith(other: LogContext): LogContext { + return LogContext( + tag = other.tag ?: tag, + contextMap = contextMap + other.contextMap, + ) +} + +internal class AndroidLogger( + private val minimalVisibleLogLevel: LogLevel, + override val defaultContext: LogContext = LogContext(), +) : Logger { + + override fun withContext(additionalContext: LogContext): Logger { + return AndroidLogger( + minimalVisibleLogLevel = minimalVisibleLogLevel, + defaultContext = defaultContext.mergeWith(additionalContext), + ) + } + + override fun log(message: String, level: LogLevel, throwable: Throwable?, context: LogContext?) { + if (level.logLevelValue <= minimalVisibleLogLevel.logLevelValue) return + val finalContext = context?.let { defaultContext.mergeWith(it) } ?: this.defaultContext + val tag = finalContext.tag ?: "AblyChatSDK" + + val contextString = if (this.defaultContext.contextMap.isEmpty()) "" else ", context: $finalContext" + val formattedMessage = "[${LocalDateTime.now()}] ${level.name} ably-chat: ${message}$contextString" + when (level) { + // We use Logcat's info level for Trace and Debug + LogLevel.Trace -> Log.i(tag, formattedMessage, throwable) + LogLevel.Debug -> Log.i(tag, formattedMessage, throwable) + LogLevel.Info -> Log.i(tag, formattedMessage, throwable) + LogLevel.Warn -> Log.w(tag, formattedMessage, throwable) + LogLevel.Error -> Log.e(tag, formattedMessage, throwable) + LogLevel.Silent -> {} + } + } +} + +internal class CustomLogger( + private val logHandler: LogHandler, + private val minimalVisibleLogLevel: LogLevel, + override val defaultContext: LogContext = LogContext(), +) : Logger { + + override fun withContext(additionalContext: LogContext): Logger { + return CustomLogger( + logHandler = logHandler, + minimalVisibleLogLevel = minimalVisibleLogLevel, + defaultContext = defaultContext.mergeWith(additionalContext), + ) + } + + override fun log(message: String, level: LogLevel, throwable: Throwable?, context: LogContext?) { + if (level.logLevelValue <= minimalVisibleLogLevel.logLevelValue) return + val finalContext = context?.let { defaultContext.mergeWith(it) } ?: this.defaultContext + logHandler.log( + message = message, + level = level, + throwable = throwable, + context = finalContext, + ) + } +} + +internal object EmptyLogger : Logger { + override val defaultContext: LogContext = LogContext() + + override fun withContext(additionalContext: LogContext): Logger = this + + override fun log(message: String, level: LogLevel, throwable: Throwable?, context: LogContext?) = Unit +} diff --git a/chat-android/src/test/java/com/ably/chat/ChatApiTest.kt b/chat-android/src/test/java/com/ably/chat/ChatApiTest.kt index e2df40d..1a7e984 100644 --- a/chat-android/src/test/java/com/ably/chat/ChatApiTest.kt +++ b/chat-android/src/test/java/com/ably/chat/ChatApiTest.kt @@ -13,7 +13,7 @@ import org.junit.Test class ChatApiTest { private val realtime = mockk(relaxed = true) - private val chatApi = ChatApi(realtime, "clientId") + private val chatApi = ChatApi(realtime, "clientId", logger = EmptyLogger) /** * @nospec diff --git a/chat-android/src/test/java/com/ably/chat/MessagesTest.kt b/chat-android/src/test/java/com/ably/chat/MessagesTest.kt index 0c5811f..9702e82 100644 --- a/chat-android/src/test/java/com/ably/chat/MessagesTest.kt +++ b/chat-android/src/test/java/com/ably/chat/MessagesTest.kt @@ -28,7 +28,7 @@ class MessagesTest { private val realtimeClient = mockk(relaxed = true) private val realtimeChannels = mockk(relaxed = true) private val realtimeChannel = spyk(buildRealtimeChannel()) - private val chatApi = spyk(ChatApi(realtimeClient, "clientId")) + private val chatApi = spyk(ChatApi(realtimeClient, "clientId", EmptyLogger)) private lateinit var messages: DefaultMessages private val channelStateListenerSlot = slot() diff --git a/example/build.gradle.kts b/example/build.gradle.kts index 2307a66..bc3a226 100644 --- a/example/build.gradle.kts +++ b/example/build.gradle.kts @@ -37,6 +37,7 @@ android { } } compileOptions { + isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } @@ -67,6 +68,7 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + coreLibraryDesugaring(libs.desugar.jdk.libs) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3817db2..f1719e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ [versions] ably-chat = "0.0.1" ably = "1.2.43" +desugar-jdk-libs = "2.1.2" junit = "4.13.2" agp = "8.5.2" detekt = "1.23.6" @@ -21,6 +22,7 @@ coroutine = "1.8.1" build-config = "5.4.0" [libraries] +desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar-jdk-libs" } junit = { group = "junit", name = "junit", version.ref = "junit" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } ably-android = { module = "io.ably:ably-android", version.ref = "ably" }