diff --git a/ketchsdk/src/main/java/com/ketch/android/Ketch.kt b/ketchsdk/src/main/java/com/ketch/android/Ketch.kt index a34046bd..e4e0937f 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) { @@ -278,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) { @@ -288,20 +279,17 @@ class Ketch private constructor( } override fun showPreferences() { - if (!isActivityActive()) { - Log.d(TAG, "Not showing as activity is not active") - return - } - - if (findDialogFragment() != null) { + if (dialog != null) { Log.d(TAG, "Not showing as dialog already exists") return } - val dialog = KetchDialogFragment.newInstance() - - fragmentManager.get()?.let { - dialog.show(it, webView) + fragmentManager?.let { + dialog = KetchDialogFragment.newInstance() { + dialog = null + }.apply { + show(it, webView) + } this@Ketch.listener?.onShow() } } @@ -359,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) @@ -383,36 +367,31 @@ 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 (!isActivityActive()) { - Log.d(TAG, "Not showing as activity is not active") - return - } - - 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.get()?.let { - dialog.show(it, webView) + fragmentManager?.let { + 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 } - showConsent = false } private fun getDisposableContentInteractions(display: ContentDisplay): Boolean = @@ -428,12 +407,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 +497,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..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 @@ -53,7 +54,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 @@ -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 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() {