From 76aac0d05b067195721f7e412150964643e5da83 Mon Sep 17 00:00:00 2001 From: Roman Kuziv Date: Mon, 3 Mar 2025 16:26:51 +0200 Subject: [PATCH 1/2] ApplicationContext, fixed issues --- .../src/main/java/com/ketch/android/Ketch.kt | 243 ++++++++---------- .../main/java/com/ketch/android/KetchSdk.kt | 18 +- .../ketch/android/ui/KetchDialogFragment.kt | 2 +- .../java/com/ketch/android/ui/KetchWebView.kt | 3 +- 4 files changed, 122 insertions(+), 144 deletions(-) diff --git a/ketchsdk/src/main/java/com/ketch/android/Ketch.kt b/ketchsdk/src/main/java/com/ketch/android/Ketch.kt index a34046bd..72f48f7c 100644 --- a/ketchsdk/src/main/java/com/ketch/android/Ketch.kt +++ b/ketchsdk/src/main/java/com/ketch/android/Ketch.kt @@ -1,10 +1,8 @@ package com.ketch.android -import android.content.Context +import android.app.Application import android.util.Log import androidx.fragment.app.FragmentManager -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner import com.ketch.android.data.Consent import com.ketch.android.data.ContentDisplay import com.ketch.android.data.HideExperienceStatus @@ -12,26 +10,33 @@ import com.ketch.android.data.KetchConfig import com.ketch.android.data.WillShowExperienceType import com.ketch.android.ui.KetchDialogFragment import com.ketch.android.ui.KetchWebView -import java.lang.ref.WeakReference /** * Main Ketch SDK class **/ class Ketch private constructor( - private val context: WeakReference, - private val fragmentManager: WeakReference, + private val application: Application, private val orgCode: String, private val property: String, private val environment: String?, private val listener: Listener?, private val ketchUrl: String?, - private val logLevel: LogLevel + private val logLevel: LogLevel, + private val shouldRetry: Boolean, + private val synchronousPreferences: Boolean ) { private var identities: Map = emptyMap() private var language: String? = null private var jurisdiction: String? = null private var region: String? = null + private var fragmentManager: FragmentManager? = null + + private val webView: KetchWebView by lazy { + createWebView(shouldRetry, synchronousPreferences) + } + + /** * Retrieve a String value from the preferences. * @@ -64,37 +69,45 @@ class Ketch private constructor( fun getGPPHDRGppString() = getPreferences().getSavedValue(KetchSharedPreferences.IAB_GPP_HDR_GPP_STRING) + /** + * Sets FragmentManager + */ + fun setFragmentManager(fragmentManager: FragmentManager) { + this.fragmentManager = fragmentManager + } + + /** + * Stops the current load. + */ + fun stop() { + dismissDialog() + webView.stop() + this.fragmentManager = null + } + /** * Loads a web page and shows a popup if necessary * * @param bottomPadding: Pixels of padding to add to the bottom of the experience (if shown) */ fun load( - shouldRetry: Boolean = false, - synchronousPreferences: Boolean = false, bottomPadding: Int = 0, - ): Boolean { - val webView = createWebView(shouldRetry, synchronousPreferences) - return if (webView != null) { - webView.load( - orgCode, - property, - language, - jurisdiction, - region, - environment, - identities, - null, - emptyList(), - null, - ketchUrl, - logLevel, - bottomPadding, - ) - true - } else { - false - } + ) { + webView.load( + orgCode, + property, + language, + jurisdiction, + region, + environment, + identities, + null, + emptyList(), + null, + ketchUrl, + logLevel, + bottomPadding, + ) } /** @@ -103,31 +116,23 @@ class Ketch private constructor( * @param bottomPadding: Pixels of padding to add to the bottom of the experience */ fun showConsent( - shouldRetry: Boolean = false, - synchronousPreferences: Boolean = false, bottomPadding: Int = 0, - ): Boolean { - val webView = createWebView(shouldRetry, synchronousPreferences) - return if (webView != null) { - webView.load( - orgCode, - property, - language, - jurisdiction, - region, - environment, - identities, - KetchWebView.ExperienceType.CONSENT, - emptyList(), - null, - ketchUrl, - logLevel, - bottomPadding - ) - true - } else { - false - } + ) { + webView.load( + orgCode, + property, + language, + jurisdiction, + region, + environment, + identities, + KetchWebView.ExperienceType.CONSENT, + emptyList(), + null, + ketchUrl, + logLevel, + bottomPadding + ) } /** @@ -136,31 +141,23 @@ class Ketch private constructor( * @param bottomPadding: Pixels of padding to add to the bottom of the experience */ fun showPreferences( - shouldRetry: Boolean = false, - synchronousPreferences: Boolean = false, bottomPadding: Int = 0, - ): Boolean { - val webView = createWebView(shouldRetry, synchronousPreferences) - return if (webView != null) { - webView.load( - orgCode, - property, - language, - jurisdiction, - region, - environment, - identities, - KetchWebView.ExperienceType.PREFERENCES, - emptyList(), - null, - ketchUrl, - logLevel, - bottomPadding - ) - true - } else { - false - } + ) { + webView.load( + orgCode, + property, + language, + jurisdiction, + region, + environment, + identities, + KetchWebView.ExperienceType.PREFERENCES, + emptyList(), + null, + ketchUrl, + logLevel, + bottomPadding + ) } /** @@ -173,31 +170,23 @@ class Ketch private constructor( fun showPreferencesTab( tabs: List, tab: PreferencesTab, - shouldRetry: Boolean = false, - synchronousPreferences: Boolean = false, bottomPadding: Int = 0, - ): Boolean { - val webView = createWebView(shouldRetry, synchronousPreferences) - return if (webView != null) { - webView.load( - orgCode, - property, - language, - jurisdiction, - region, - environment, - identities, - KetchWebView.ExperienceType.PREFERENCES, - tabs, - tab, - ketchUrl, - logLevel, - bottomPadding - ) - true - } else { - false - } + ) { + webView.load( + orgCode, + property, + language, + jurisdiction, + region, + environment, + identities, + KetchWebView.ExperienceType.PREFERENCES, + tabs, + tab, + ketchUrl, + logLevel, + bottomPadding + ) } /** @@ -258,16 +247,17 @@ class Ketch private constructor( // Get the singleton KetchSharedPreferences object private fun getPreferences(): KetchSharedPreferences { - context.get()?.let { - // Initialize will create KetchSharedPreferences if it doesn't already exist - KetchSharedPreferences.initialize(it) - } + // Initialize will create KetchSharedPreferences if it doesn't already exist + KetchSharedPreferences.initialize(application) return KetchSharedPreferences } - private fun createWebView(shouldRetry: Boolean = false, synchronousPreferences: Boolean = false): KetchWebView? { + private fun createWebView( + shouldRetry: Boolean = false, + synchronousPreferences: Boolean = false + ): KetchWebView { - val webView = context.get()?.let { KetchWebView(it, shouldRetry) } ?: return null + val webView = KetchWebView(application, shouldRetry) // Enable debug mode if (logLevel === LogLevel.DEBUG) { @@ -288,11 +278,6 @@ class Ketch private constructor( } override fun showPreferences() { - if (!isActivityActive()) { - Log.d(TAG, "Not showing as activity is not active") - return - } - if (findDialogFragment() != null) { Log.d(TAG, "Not showing as dialog already exists") return @@ -300,7 +285,7 @@ class Ketch private constructor( val dialog = KetchDialogFragment.newInstance() - fragmentManager.get()?.let { + fragmentManager?.let { dialog.show(it, webView) this@Ketch.listener?.onShow() } @@ -392,11 +377,6 @@ class Ketch private constructor( } private fun showConsentPopup() { - if (!isActivityActive()) { - Log.d(TAG, "Not showing as activity is not active") - return - } - if (findDialogFragment() != null) { Log.d(TAG, "Not showing as dialog already exists") return @@ -408,11 +388,11 @@ class Ketch private constructor( ) isCancelable = !disableContentInteractions } - fragmentManager.get()?.let { + fragmentManager?.let { dialog.show(it, webView) this@Ketch.listener?.onShow() + showConsent = false } - showConsent = false } private fun getDisposableContentInteractions(display: ContentDisplay): Boolean = @@ -428,12 +408,7 @@ class Ketch private constructor( } private fun findDialogFragment() = - fragmentManager.get()?.findFragmentByTag(KetchDialogFragment.TAG) - - private fun isActivityActive(): Boolean { - return (context.get() as? LifecycleOwner)?.lifecycle?.currentState?.isAtLeast(Lifecycle.State.STARTED) - ?: false - } + fragmentManager?.findFragmentByTag(KetchDialogFragment.TAG) enum class PreferencesTab { OVERVIEW, @@ -523,23 +498,25 @@ class Ketch private constructor( companion object { val TAG = Ketch::class.java.simpleName fun create( - context: Context, - fragmentManager: FragmentManager, + application: Application, orgCode: String, property: String, environment: String?, listener: Listener?, ketchUrl: String?, logLevel: LogLevel, + shouldRetry: Boolean = false, + synchronousPreferences: Boolean = false ) = Ketch( - WeakReference(context), - WeakReference(fragmentManager), + application, orgCode, property, environment, listener, ketchUrl, - logLevel + logLevel, + shouldRetry, + synchronousPreferences ) } } diff --git a/ketchsdk/src/main/java/com/ketch/android/KetchSdk.kt b/ketchsdk/src/main/java/com/ketch/android/KetchSdk.kt index 320015a4..bad39c63 100644 --- a/ketchsdk/src/main/java/com/ketch/android/KetchSdk.kt +++ b/ketchsdk/src/main/java/com/ketch/android/KetchSdk.kt @@ -1,5 +1,6 @@ package com.ketch.android +import android.app.Application import android.content.Context import androidx.fragment.app.FragmentManager @@ -21,8 +22,7 @@ object KetchSdk { /** * Creates the Ketch * - * @param context - an Activity Context to access application assets - * @param fragmentManager - The FragmentManager this KetchDialogFragment will be added to. + * @param application - Application Context * @param organization - your organization code * @param property - the property name * @param environment - the environment name. @@ -31,24 +31,26 @@ object KetchSdk { * @param logLevel - the log level, can be TRACE, DEBUG, INFO, WARN, ERROR. Default is ERROR */ fun create( - context: Context, - fragmentManager: FragmentManager, + application: Application, organization: String, property: String, environment: String? = null, listener: Ketch.Listener? = null, ketchUrl: String? = null, - logLevel: Ketch.LogLevel = Ketch.LogLevel.ERROR + logLevel: Ketch.LogLevel = Ketch.LogLevel.ERROR, + shouldRetry: Boolean = false, + synchronousPreferences: Boolean = false ): Ketch { return Ketch.create( - context = context, - fragmentManager = fragmentManager, + application = application, orgCode = organization, property = property, environment = environment, listener = listener, ketchUrl = ketchUrl, - logLevel = logLevel + logLevel = logLevel, + shouldRetry = shouldRetry, + synchronousPreferences = synchronousPreferences ) } } \ No newline at end of file diff --git a/ketchsdk/src/main/java/com/ketch/android/ui/KetchDialogFragment.kt b/ketchsdk/src/main/java/com/ketch/android/ui/KetchDialogFragment.kt index 3f969252..3bfc8051 100644 --- a/ketchsdk/src/main/java/com/ketch/android/ui/KetchDialogFragment.kt +++ b/ketchsdk/src/main/java/com/ketch/android/ui/KetchDialogFragment.kt @@ -53,7 +53,7 @@ internal class KetchDialogFragment() : DialogFragment() { binding.root.removeView(webView) // Cancel any coroutines in KetchWebView and fully tear down webview to prevent memory leaks - webView?.kill() + webView?.stop() // Set webview reference to null to prevent memory leaks webView = null diff --git a/ketchsdk/src/main/java/com/ketch/android/ui/KetchWebView.kt b/ketchsdk/src/main/java/com/ketch/android/ui/KetchWebView.kt index bad32aa6..35fcb239 100644 --- a/ketchsdk/src/main/java/com/ketch/android/ui/KetchWebView.kt +++ b/ketchsdk/src/main/java/com/ketch/android/ui/KetchWebView.kt @@ -76,14 +76,13 @@ class KetchWebView(context: Context, shouldRetry: Boolean = false) : WebView(con } // Cancel any coroutines in KetchWebView and fully tear down webview to prevent memory leaks - fun kill() { + fun stop() { localContentWebViewClient.cancelCoroutines() stopLoading() clearHistory() clearCache(true) loadUrl("about:blank") removeAllViews() - destroy() } class LocalContentWebViewClient(private var shouldRetry: Boolean = false) : WebViewClientCompat() { From fe35c66b202a06ae3adf8a6133d312c1b503ab26 Mon Sep 17 00:00:00 2001 From: Roman Kuziv Date: Mon, 3 Mar 2025 18:18:40 +0200 Subject: [PATCH 2/2] KetchDialogFragment: onDismiss --- .../src/main/java/com/ketch/android/Ketch.kt | 47 +++++++++---------- .../ketch/android/ui/KetchDialogFragment.kt | 12 +++-- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/ketchsdk/src/main/java/com/ketch/android/Ketch.kt b/ketchsdk/src/main/java/com/ketch/android/Ketch.kt index 72f48f7c..e4e0937f 100644 --- a/ketchsdk/src/main/java/com/ketch/android/Ketch.kt +++ b/ketchsdk/src/main/java/com/ketch/android/Ketch.kt @@ -268,6 +268,7 @@ class Ketch private constructor( private var config: KetchConfig? = null private var showConsent: Boolean = false + private var dialog: KetchDialogFragment? = null override fun showConsent() { if (config == null) { @@ -278,15 +279,17 @@ class Ketch private constructor( } override fun showPreferences() { - if (findDialogFragment() != null) { + if (dialog != null) { Log.d(TAG, "Not showing as dialog already exists") return } - val dialog = KetchDialogFragment.newInstance() - fragmentManager?.let { - dialog.show(it, webView) + dialog = KetchDialogFragment.newInstance() { + dialog = null + }.apply { + show(it, webView) + } this@Ketch.listener?.onShow() } } @@ -344,18 +347,14 @@ class Ketch private constructor( } override fun changeDialog(display: ContentDisplay) { - findDialogFragment()?.let { - (it as? KetchDialogFragment)?.apply { - isCancelable = getDisposableContentInteractions(display) - } + dialog?.let { + it.isCancelable = getDisposableContentInteractions(display) } } override fun onClose(status: HideExperienceStatus) { // Dismiss dialog fragment - findDialogFragment()?.let { - (it as? KetchDialogFragment)?.dismissAllowingStateLoss() - } + dismissDialog() // Execute onDismiss event listener this@Ketch.listener?.onDismiss(status) @@ -368,28 +367,28 @@ class Ketch private constructor( override fun onTapOutside() { // Dismiss dialog fragment - findDialogFragment()?.let { - (it as? KetchDialogFragment)?.dismissAllowingStateLoss() + dialog?.dismissAllowingStateLoss() - // Execute onDismiss event listener - this@Ketch.listener?.onDismiss(HideExperienceStatus.None) - } + // Execute onDismiss event listener + this@Ketch.listener?.onDismiss(HideExperienceStatus.None) } private fun showConsentPopup() { - if (findDialogFragment() != null) { + if (dialog != null) { Log.d(TAG, "Not showing as dialog already exists") return } - val dialog = KetchDialogFragment.newInstance().apply { - val disableContentInteractions = getDisposableContentInteractions( - config?.experiences?.consent?.display ?: ContentDisplay.Banner - ) - isCancelable = !disableContentInteractions - } fragmentManager?.let { - dialog.show(it, webView) + dialog = KetchDialogFragment.newInstance() { + dialog = null + }.apply { + val disableContentInteractions = getDisposableContentInteractions( + config?.experiences?.consent?.display ?: ContentDisplay.Banner + ) + isCancelable = !disableContentInteractions + show(it, webView) + } this@Ketch.listener?.onShow() showConsent = false } diff --git a/ketchsdk/src/main/java/com/ketch/android/ui/KetchDialogFragment.kt b/ketchsdk/src/main/java/com/ketch/android/ui/KetchDialogFragment.kt index 3bfc8051..56316b39 100644 --- a/ketchsdk/src/main/java/com/ketch/android/ui/KetchDialogFragment.kt +++ b/ketchsdk/src/main/java/com/ketch/android/ui/KetchDialogFragment.kt @@ -1,6 +1,7 @@ package com.ketch.android.ui import android.app.Dialog +import android.content.DialogInterface import android.content.res.Configuration import android.graphics.Color import android.graphics.drawable.ColorDrawable @@ -17,7 +18,7 @@ import androidx.fragment.app.FragmentManager import com.ketch.android.R import com.ketch.android.databinding.KetchDialogLayoutBinding -internal class KetchDialogFragment() : DialogFragment() { +internal class KetchDialogFragment(private val dismissListener: () -> Unit) : DialogFragment() { private lateinit var binding: KetchDialogLayoutBinding @@ -80,6 +81,11 @@ internal class KetchDialogFragment() : DialogFragment() { } } + override fun onDismiss(dialog: DialogInterface) { + dismissListener.invoke() + super.onDismiss(dialog) + } + override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) dialog?.window?.also { window -> @@ -106,8 +112,8 @@ internal class KetchDialogFragment() : DialogFragment() { companion object { internal val TAG = KetchDialogFragment::class.java.simpleName - fun newInstance(): KetchDialogFragment { - return KetchDialogFragment() + fun newInstance(dismissListener: () -> Unit): KetchDialogFragment { + return KetchDialogFragment(dismissListener) } } } \ No newline at end of file