diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt index f45ac20..7e595c8 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/helpers/OSIABCustomTabsSessionHelper.kt @@ -62,54 +62,22 @@ class OSIABCustomTabsSessionHelper: OSIABCustomTabsSessionHelperInterface { flowHelper: OSIABFlowHelperInterface, ) : CustomTabsCallback() { - private var isCustomTabsActivityOnTop = false - private var pendingTabHiddenEvent = false - init { var browserEventsJob: Job? = null - browserEventsJob = flowHelper.listenToEvents(browserId, lifecycleScope) { event -> - if(event is OSIABEvents.OSIABCustomTabsEvent) { - when (event.action) { - OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_RESUMED -> { - isCustomTabsActivityOnTop = true - if (pendingTabHiddenEvent) { - pendingTabHiddenEvent = false - lifecycleScope.launch { - OSIABEvents.postEvent(OSIABEvents.BrowserFinished(browserId)) - } - } - } - OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_PAUSED -> { - isCustomTabsActivityOnTop = false - pendingTabHiddenEvent = false - } - OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_DESTROYED -> { - browserEventsJob?.cancel() - } - } + if (event is OSIABEvents.OSIABCustomTabsEvent + && event.action == OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_DESTROYED) { + browserEventsJob?.cancel() } } } override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) { super.onNavigationEvent(navigationEvent, extras) - val browserEvent = when (navigationEvent) { - NAVIGATION_FINISHED -> OSIABEvents.BrowserPageLoaded(browserId) - TAB_HIDDEN -> { - if(isCustomTabsActivityOnTop) { - OSIABEvents.BrowserFinished(browserId) - } - else { - // App not open but custom tabs is hidden (home button, recent apps, etc.) - pendingTabHiddenEvent = true - return - } + if (navigationEvent == NAVIGATION_FINISHED) { + lifecycleScope.launch { + OSIABEvents.postEvent(OSIABEvents.BrowserPageLoaded(browserId)) } - else -> return - } - lifecycleScope.launch { - OSIABEvents.postEvent(browserEvent) } } } diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt index d6d1cb5..293e905 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/routeradapters/OSIABCustomTabsRouterAdapter.kt @@ -51,23 +51,28 @@ class OSIABCustomTabsRouterAdapter( var closeEventJob: Job? = null closeEventJob = flowHelper.listenToEvents(browserId, lifecycleScope) { event -> - if(event is OSIABEvents.OSIABCustomTabsEvent) { - when(event.action) { - OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_READY -> { - completionHandler(false) - } - OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_DESTROYED -> { - completionHandler(true) - } - else -> { - return@listenToEvents - } - } + if(event is OSIABEvents.OSIABCustomTabsEvent + && event.action == OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_DESTROYED) { + completionHandler(true) closeEventJob?.cancel() } } - startCustomTabsControllerActivity(true) + startCustomTabsControllerActivity(doClose = true) + } + + private fun resolveStartAnimationRes(animation: OSIABAnimation): Pair = when (animation) { + OSIABAnimation.FADE_IN -> android.R.anim.fade_in to android.R.anim.fade_out + OSIABAnimation.FADE_OUT -> android.R.anim.fade_out to android.R.anim.fade_in + OSIABAnimation.SLIDE_IN_LEFT -> android.R.anim.slide_in_left to android.R.anim.slide_out_right + OSIABAnimation.SLIDE_OUT_RIGHT -> android.R.anim.slide_out_right to android.R.anim.slide_in_left + } + + private fun resolveExitAnimationRes(animation: OSIABAnimation): Pair = when (animation) { + OSIABAnimation.FADE_IN -> android.R.anim.fade_out to android.R.anim.fade_in + OSIABAnimation.FADE_OUT -> android.R.anim.fade_in to android.R.anim.fade_out + OSIABAnimation.SLIDE_IN_LEFT -> android.R.anim.slide_out_right to android.R.anim.slide_in_left + OSIABAnimation.SLIDE_OUT_RIGHT -> android.R.anim.slide_in_left to android.R.anim.slide_out_right } private fun buildCustomTabsIntent(customTabsSession: CustomTabsSession?): CustomTabsIntent { @@ -76,57 +81,11 @@ class OSIABCustomTabsRouterAdapter( builder.setShowTitle(options.showTitle) builder.setUrlBarHidingEnabled(options.hideToolbarOnScroll) - when (options.startAnimation) { - OSIABAnimation.FADE_IN -> builder.setStartAnimations( - context, - android.R.anim.fade_in, - android.R.anim.fade_out - ) - - OSIABAnimation.FADE_OUT -> builder.setStartAnimations( - context, - android.R.anim.fade_out, - android.R.anim.fade_in - ) - - OSIABAnimation.SLIDE_IN_LEFT -> builder.setStartAnimations( - context, - android.R.anim.slide_in_left, - android.R.anim.slide_out_right - ) - - OSIABAnimation.SLIDE_OUT_RIGHT -> builder.setStartAnimations( - context, - android.R.anim.slide_out_right, - android.R.anim.slide_in_left - ) - } - - when (options.exitAnimation) { - OSIABAnimation.FADE_IN -> builder.setExitAnimations( - context, - android.R.anim.fade_out, - android.R.anim.fade_in - ) - - OSIABAnimation.FADE_OUT -> builder.setExitAnimations( - context, - android.R.anim.fade_in, - android.R.anim.fade_out - ) + val (startEnter, startExit) = resolveStartAnimationRes(options.startAnimation) + builder.setStartAnimations(context, startEnter, startExit) - OSIABAnimation.SLIDE_IN_LEFT -> builder.setExitAnimations( - context, - android.R.anim.slide_out_right, - android.R.anim.slide_in_left - ) - - OSIABAnimation.SLIDE_OUT_RIGHT -> builder.setExitAnimations( - context, - android.R.anim.slide_in_left, - android.R.anim.slide_out_right - ) - } + val (exitEnter, exitExit) = resolveExitAnimationRes(options.exitAnimation) + builder.setExitAnimations(context, exitEnter, exitExit) if (options.viewStyle == OSIABViewStyle.BOTTOM_SHEET) { options.bottomSheetOptions?.let { bottomSheetOptions -> @@ -161,9 +120,7 @@ class OSIABCustomTabsRouterAdapter( completionHandler(false) return@generateNewCustomTabsSession } - openCustomTabsIntent(it, uri, completionHandler) - startCustomTabsControllerActivity() } ) } catch (e: Exception) { @@ -174,21 +131,13 @@ class OSIABCustomTabsRouterAdapter( private fun openCustomTabsIntent(session: CustomTabsSession, uri: Uri, completionHandler: (Boolean) -> Unit) { val customTabsIntent = buildCustomTabsIntent(session) + customTabsIntent.intent.data = uri + var eventsJob: Job? = null eventsJob = flowHelper.listenToEvents(browserId, lifecycleScope) { event -> when (event) { is OSIABEvents.OSIABCustomTabsEvent -> { - if(event.action == OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_READY) { - try { - event.context?.let { ctx -> - customTabsIntent.launchUrl(ctx, uri) - completionHandler(true) - } ?: completionHandler(false) - } catch (e: Exception) { - completionHandler(false) - } - } - else if(event.action == OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_DESTROYED) { + if(event.action == OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_DESTROYED) { isFinished = true onBrowserFinished() eventsJob?.cancel() @@ -200,19 +149,30 @@ class OSIABCustomTabsRouterAdapter( isFirstLoad = false } } - is OSIABEvents.BrowserFinished -> { - // Ensure that custom tabs controller activity is fully destroyed - startCustomTabsControllerActivity(true) - isFinished = true - onBrowserFinished() - eventsJob?.cancel() - } else -> {} } } + + try { + val (startEnter, startExit) = resolveStartAnimationRes(options.startAnimation) + startCustomTabsControllerActivity( + customTabsIntent = customTabsIntent.intent, + startEnterAnimRes = startEnter, + startExitAnimRes = startExit + ) + completionHandler(true) + } catch (e: Exception) { + eventsJob?.cancel() + completionHandler(false) + } } - private fun startCustomTabsControllerActivity(doClose: Boolean = false) { + private fun startCustomTabsControllerActivity( + doClose: Boolean = false, + customTabsIntent: Intent? = null, + startEnterAnimRes: Int = 0, + startExitAnimRes: Int = 0 + ) { val intent = Intent(context, OSIABCustomTabsControllerActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) putExtra(OSIABEvents.EXTRA_BROWSER_ID, browserId) @@ -223,6 +183,12 @@ class OSIABCustomTabsRouterAdapter( intent.putExtra(OSIABCustomTabsControllerActivity.ACTION_CLOSE_CUSTOM_TABS, true) } + if (customTabsIntent != null) { + intent.putExtra(OSIABCustomTabsControllerActivity.EXTRA_CUSTOM_TABS_INTENT, customTabsIntent) + intent.putExtra(OSIABCustomTabsControllerActivity.EXTRA_START_ENTER_ANIM_RES, startEnterAnimRes) + intent.putExtra(OSIABCustomTabsControllerActivity.EXTRA_START_EXIT_ANIM_RES, startExitAnimRes) + } + context.startActivity(intent) } } diff --git a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABCustomTabsControllerActivity.kt b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABCustomTabsControllerActivity.kt index 3532ecf..fb27621 100644 --- a/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABCustomTabsControllerActivity.kt +++ b/src/main/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/views/OSIABCustomTabsControllerActivity.kt @@ -3,7 +3,11 @@ package com.outsystems.plugins.inappbrowser.osinappbrowserlib.views import android.content.Intent import android.os.Bundle import android.view.WindowManager +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.IntentCompat import androidx.lifecycle.lifecycleScope import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents import com.outsystems.plugins.inappbrowser.osinappbrowserlib.OSIABEvents.OSIABCustomTabsEvent @@ -18,12 +22,16 @@ import kotlinx.coroutines.launch class OSIABCustomTabsControllerActivity: AppCompatActivity() { companion object { const val EVENT_CUSTOM_TABS_DESTROYED = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EVENT_CUSTOM_TABS_DESTROYED" - const val EVENT_CUSTOM_TABS_READY = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EVENT_CUSTOM_TABS_READY" - const val EVENT_CUSTOM_TABS_PAUSED = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EVENT_CUSTOM_TABS_PAUSED" - const val EVENT_CUSTOM_TABS_RESUMED = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EVENT_CUSTOM_TABS_RESUMED" const val ACTION_CLOSE_CUSTOM_TABS = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.ACTION_CLOSE_CUSTOM_TABS" + const val EXTRA_CUSTOM_TABS_INTENT = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EXTRA_CUSTOM_TABS_INTENT" + const val EXTRA_START_ENTER_ANIM_RES = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EXTRA_START_ENTER_ANIM_RES" + const val EXTRA_START_EXIT_ANIM_RES = "com.outsystems.plugins.inappbrowser.osinappbrowserlib.EXTRA_START_EXIT_ANIM_RES" } + private var customTabsLauncher: ActivityResultLauncher? = null + private var hasLaunchedCustomTabs = false + + private fun setup(intent: Intent) { window.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) @@ -32,16 +40,30 @@ class OSIABCustomTabsControllerActivity: AppCompatActivity() { if (intent.getBooleanExtra(ACTION_CLOSE_CUSTOM_TABS, false)) { finish() + return } - else { - intent.getStringExtra(OSIABEvents.EXTRA_BROWSER_ID)?.let { browserId -> - sendCustomTabsEvent(lifecycleScope, browserId, EVENT_CUSTOM_TABS_READY) + + if (!hasLaunchedCustomTabs) { + val customTabsIntent = IntentCompat.getParcelableExtra(intent, EXTRA_CUSTOM_TABS_INTENT, Intent::class.java) + if (customTabsIntent != null) { + hasLaunchedCustomTabs = true + val enterAnimRes = intent.getIntExtra(EXTRA_START_ENTER_ANIM_RES, 0) + val exitAnimRes = intent.getIntExtra(EXTRA_START_EXIT_ANIM_RES, 0) + val options = if (enterAnimRes != 0 && exitAnimRes != 0) { + ActivityOptionsCompat.makeCustomAnimation(this, enterAnimRes, exitAnimRes) + } else { + null + } + customTabsLauncher?.launch(customTabsIntent, options) } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + customTabsLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + finish() + } setup(intent) } @@ -50,20 +72,6 @@ class OSIABCustomTabsControllerActivity: AppCompatActivity() { setup(intent) } - override fun onPause() { - super.onPause() - intent.getStringExtra(OSIABEvents.EXTRA_BROWSER_ID)?.let { browserId -> - sendCustomTabsEvent(lifecycleScope, browserId, EVENT_CUSTOM_TABS_PAUSED) - } - } - - override fun onResume() { - super.onResume() - intent.getStringExtra(OSIABEvents.EXTRA_BROWSER_ID)?.let { browserId -> - sendCustomTabsEvent(lifecycleScope, browserId, EVENT_CUSTOM_TABS_RESUMED) - } - } - override fun onDestroy() { intent.getStringExtra(OSIABEvents.EXTRA_BROWSER_ID)?.let { browserId -> val customScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -95,4 +103,4 @@ class OSIABCustomTabsControllerActivity: AppCompatActivity() { scope.cancel() } } -} \ No newline at end of file +} diff --git a/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABCustomTabsRouterAdapterTests.kt b/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABCustomTabsRouterAdapterTests.kt index df33978..2cc80c3 100644 --- a/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABCustomTabsRouterAdapterTests.kt +++ b/src/test/java/com.outsystems.plugins.inappbrowser/osinappbrowserlib/OSIABCustomTabsRouterAdapterTests.kt @@ -7,10 +7,15 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.net.Uri +import androidx.browser.customtabs.CustomTabsSession +import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABCustomTabsSessionHelperInterface import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABCustomTabsSessionHelperMock +import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABFlowHelperInterface import com.outsystems.plugins.inappbrowser.osinappbrowserlib.helpers.OSIABFlowHelperMock import com.outsystems.plugins.inappbrowser.osinappbrowserlib.models.OSIABCustomTabsOptions import com.outsystems.plugins.inappbrowser.osinappbrowserlib.routeradapters.OSIABCustomTabsRouterAdapter +import com.outsystems.plugins.inappbrowser.osinappbrowserlib.views.OSIABCustomTabsControllerActivity +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -56,9 +61,21 @@ class OSIABCustomTabsRouterAdapterTests { @Test - fun test_handleOpen_withInvalidURL_returnsFalse() { + fun test_handleOpen_whenNoCustomTabsAvailable_returnsFalse() { runTest(StandardTestDispatcher()) { val context = mockContext(useValidURL = false, ableToOpenURL = false) + val nullSessionHelper = object : OSIABCustomTabsSessionHelperInterface { + override suspend fun generateNewCustomTabsSession( + browserId: String, + context: Context, + lifecycleScope: CoroutineScope, + flowHelper: OSIABFlowHelperInterface, + customTabsSessionCallback: (CustomTabsSession?) -> Unit + ) { + customTabsSessionCallback(null) + } + } + val sut = OSIABCustomTabsRouterAdapter( context = context, lifecycleScope = this, @@ -66,10 +83,10 @@ class OSIABCustomTabsRouterAdapterTests { options = options, onBrowserPageLoaded = {}, onBrowserFinished = {}, - customTabsSessionHelper = OSIABCustomTabsSessionHelperMock(), + customTabsSessionHelper = nullSessionHelper, ) - sut.handleOpen("invalid_url") { success -> + sut.handleOpen(uri.toString()) { success -> assertFalse(success) } } @@ -131,10 +148,17 @@ class OSIABCustomTabsRouterAdapterTests { } @Test - fun test_handleOpen_withValidURL_launchesCustomTab_when_browserFinished_then_browserFinishedTriggered() { + fun test_handleOpen_withValidURL_launchesCustomTab_when_customTabsDestroyed_then_browserFinishedTriggered() { runTest(StandardTestDispatcher()) { val context = mockContext(useValidURL = true, ableToOpenURL = true) - val flowHelperMock = OSIABFlowHelperMock().apply { events = listOf(OSIABEvents.BrowserFinished("")) } + val flowHelperMock = OSIABFlowHelperMock().apply { + events = listOf( + OSIABEvents.OSIABCustomTabsEvent( + browserId = "", + action = OSIABCustomTabsControllerActivity.EVENT_CUSTOM_TABS_DESTROYED + ) + ) + } val sut = OSIABCustomTabsRouterAdapter( context = context, lifecycleScope = this,