From 4fcb781a3cf1a0e3b1414a6096deac40495250eb Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 25 Oct 2023 17:49:20 +0500 Subject: [PATCH] feat: persistant in-app messages (#269) --- messaginginapp/api/messaginginapp.api | 1 + .../gist/data/listeners/Queue.kt | 88 ++++++++++++++----- .../messaginginapp/gist/data/model/Message.kt | 2 +- .../gist/presentation/GistModalActivity.kt | 20 ++++- .../gist/presentation/GistModalManager.kt | 9 +- .../gist/presentation/GistSdk.kt | 4 + 6 files changed, 100 insertions(+), 24 deletions(-) diff --git a/messaginginapp/api/messaginginapp.api b/messaginginapp/api/messaginginapp.api index a2af954f6..5f408c80c 100644 --- a/messaginginapp/api/messaginginapp.api +++ b/messaginginapp/api/messaginginapp.api @@ -201,6 +201,7 @@ public final class io/customer/messaginginapp/gist/presentation/GistSdk : androi public static field dataCenter Ljava/lang/String; public static field siteId Ljava/lang/String; public final fun addListener (Lio/customer/messaginginapp/gist/presentation/GistListener;)V + public final fun clearCurrentMessage ()V public final fun clearListeners ()V public final fun clearUserToken ()V public final fun dismissMessage ()V diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/listeners/Queue.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/listeners/Queue.kt index 0ab37cd0a..6dcc9ad8f 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/listeners/Queue.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/listeners/Queue.kt @@ -1,5 +1,6 @@ package io.customer.messaginginapp.gist.data.listeners +import android.content.Context import android.util.Base64 import android.util.Log import io.customer.messaginginapp.gist.data.NetworkUtilities @@ -9,47 +10,94 @@ import io.customer.messaginginapp.gist.data.repository.GistQueueService import io.customer.messaginginapp.gist.presentation.GIST_TAG import io.customer.messaginginapp.gist.presentation.GistListener import io.customer.messaginginapp.gist.presentation.GistSdk +import java.io.File import java.util.regex.PatternSyntaxException import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import okhttp3.Cache import okhttp3.OkHttpClient -import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory class Queue : GistListener { private var localMessageStore: MutableList = mutableListOf() + private val cacheMap = mutableMapOf() init { GistSdk.addListener(this) } + private val cacheSize = 10 * 1024 * 1024 // 10 MB + private val cacheDirectory by lazy { File(GistSdk.application.cacheDir, "http_cache") } + private val cache by lazy { Cache(cacheDirectory, cacheSize.toLong()) } + + private fun saveToPrefs(context: Context, key: String, value: String) { + val prefs = context.getSharedPreferences("network_cache", Context.MODE_PRIVATE) + prefs.edit().putString(key, value).apply() + } + + private fun getFromPrefs(context: Context, key: String): String? { + val prefs = context.getSharedPreferences("network_cache", Context.MODE_PRIVATE) + return prefs.getString(key, null) + } + private val gistQueueService by lazy { - val httpClient: OkHttpClient = OkHttpClient.Builder() - .addInterceptor { chain -> - GistSdk.getUserToken()?.let { userToken -> - val request: Request = chain.request().newBuilder() - .addHeader(NetworkUtilities.CIO_SITE_ID_HEADER, GistSdk.siteId) - .addHeader(NetworkUtilities.CIO_DATACENTER_HEADER, GistSdk.dataCenter) - .addHeader( - NetworkUtilities.USER_TOKEN_HEADER, - // The NO_WRAP flag will omit all line terminators (i.e., the output will be on one long line). - Base64.encodeToString(userToken.toByteArray(), Base64.NO_WRAP) - ) - .build() + // Interceptor to set up request headers like site ID, data center, and user token. + val httpClient: OkHttpClient = + OkHttpClient.Builder().cache(cache) + .addInterceptor { chain -> + val originalRequest = chain.request() - chain.proceed(request) - } ?: run { - val request: Request = chain.request().newBuilder() + val networkRequest = originalRequest.newBuilder() .addHeader(NetworkUtilities.CIO_SITE_ID_HEADER, GistSdk.siteId) .addHeader(NetworkUtilities.CIO_DATACENTER_HEADER, GistSdk.dataCenter) + .apply { + GistSdk.getUserToken()?.let { userToken -> + addHeader( + NetworkUtilities.USER_TOKEN_HEADER, + // The NO_WRAP flag will omit all line terminators (i.e., the output will be on one long line). + Base64.encodeToString(userToken.toByteArray(), Base64.NO_WRAP) + ) + } + } + .header("Cache-Control", "no-cache") .build() - chain.proceed(request) + val response = chain.proceed(networkRequest) + + when (response.code) { + 200 -> { + response.body?.let { responseBody -> + val responseBodyString = responseBody.string() + saveToPrefs( + GistSdk.application, + originalRequest.url.toString(), + responseBodyString + ) + return@addInterceptor response.newBuilder().body( + responseBodyString.toResponseBody(responseBody.contentType()) + ).build() + } + } + + 304 -> { + val cachedResponse = + getFromPrefs(GistSdk.application, originalRequest.url.toString()) + cachedResponse?.let { + return@addInterceptor response.newBuilder() + .body(it.toResponseBody(null)).code(200).build() + } ?: return@addInterceptor response + } + + else -> return@addInterceptor response + } + + response } - } - .build() + .build() Retrofit.Builder() .baseUrl(GistSdk.gistEnvironment.getGistQueueApiUrl()) @@ -72,8 +120,6 @@ class Queue : GistListener { try { Log.i(GIST_TAG, "Fetching user messages") val latestMessagesResponse = gistQueueService.fetchMessagesForUser() - // If there's no change (304), move on. - if (latestMessagesResponse.code() == 304) { return@launch } // To prevent us from showing expired / revoked messages, clear user messages from local queue. clearUserMessagesFromLocalStore() diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/model/Message.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/model/Message.kt index 045074670..9f3c1dda2 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/model/Message.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/data/model/Message.kt @@ -55,7 +55,7 @@ class GistMessageProperties { } } gistProperties["persistent"]?.let { id -> - (id as Boolean).let { persistentValue -> + (id as? Boolean)?.let { persistentValue -> persistent = persistentValue } } diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalActivity.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalActivity.kt index 8bb70b3e4..2a90a5628 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalActivity.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalActivity.kt @@ -8,6 +8,7 @@ import android.util.Log import android.view.Gravity import android.view.View import android.view.WindowManager +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.animation.doOnEnd import com.google.gson.Gson @@ -61,6 +62,12 @@ class GistModalActivity : AppCompatActivity(), GistListener, GistViewListener { } ?: run { finish() } + + // Update back button to handle in-app message behavior, disable back press for persistent messages, true otherwise + val onBackPressedCallback = object : OnBackPressedCallback(isPersistentMessage()) { + override fun handleOnBackPressed() {} + } + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) } override fun onResume() { @@ -91,10 +98,21 @@ class GistModalActivity : AppCompatActivity(), GistListener, GistViewListener { override fun onDestroy() { GistSdk.removeListener(this) - GistSdk.dismissMessage() + // If the message is not persistent, dismiss it and inform the callback + if (!isPersistentMessage()) { + GistSdk.dismissMessage() + } else { + GistSdk.clearCurrentMessage() + } super.onDestroy() } + private fun isPersistentMessage(): Boolean = currentMessage?.let { + GistMessageProperties.getGistProperties( + it + ).persistent + } ?: false + override fun onMessageShown(message: Message) { runOnUiThread { window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalManager.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalManager.kt index 1babc1b51..a1d5eb1e9 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalManager.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistModalManager.kt @@ -15,7 +15,10 @@ internal class GistModalManager : GistListener { internal fun showModalMessage(message: Message, position: MessagePosition? = null): Boolean { currentMessage?.let { currentMessage -> - Log.i(GIST_TAG, "Message ${message.messageId} not shown, ${currentMessage.messageId} is already showing.") + Log.i( + GIST_TAG, + "Message ${message.messageId} not shown, ${currentMessage.messageId} is already showing." + ) return false } @@ -55,4 +58,8 @@ internal class GistModalManager : GistListener { override fun onMessageShown(message: Message) {} override fun onAction(message: Message, currentRoute: String, action: String, name: String) {} + + internal fun clearCurrentMessage() { + currentMessage = null + } } diff --git a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistSdk.kt b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistSdk.kt index c0fc3710b..5a5f12073 100644 --- a/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistSdk.kt +++ b/messaginginapp/src/main/java/io/customer/messaginginapp/gist/presentation/GistSdk.kt @@ -160,6 +160,10 @@ object GistSdk : Application.ActivityLifecycleCallbacks { gistModalManager.dismissActiveMessage() } + fun clearCurrentMessage() { + gistModalManager.clearCurrentMessage() + } + // Listeners fun addListener(listener: GistListener) {