NTP-63026 | Generic Custom Chrome Tab Redirection Support (#16141)

This commit is contained in:
Soumya Ranjan Patra
2025-05-12 21:16:36 +05:30
committed by GitHub
parent 39493db486
commit bff22a380a
7 changed files with 268 additions and 40 deletions

View File

@@ -1421,6 +1421,8 @@ class NaviAnalytics private constructor() {
const val CHROME_TAB_SESSION_ENDED = "chrome_tab_session_ended"
const val WEB_TAB_REDIRECTION_INTENT = "web_tab_redirection_intent"
const val CHROME_CUSTOM_TAB_WARMUP_FAILED = "chrome_tab_warm_up_failed"
const val CHROME_CUSTOM_TAB_WARMUP_AND_LAUNCH_FAILED =
"chrome_tab_warm_up_and_launch_failed"
const val ENGAGEMENT_SIGNALS_NOT_SUPPORTED = "engagement_signals_not_supported"
const val CHROME_TAB_SERVICE_DISCONNECTION_EXCEPTION =
"chrome_tab_service_disconnection_exception"

View File

@@ -43,6 +43,7 @@ import com.navi.base.sharedpref.CommonPrefConstants.USER_EXTERNAL_ID
import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.BaseUtils
import com.navi.base.utils.EMPTY
import com.navi.base.utils.isNotNullAndNotEmpty
import com.navi.base.utils.orFalse
import com.navi.base.utils.orZero
import com.navi.base.utils.replaceAngularBrackets
@@ -60,6 +61,7 @@ import com.navi.chat.utils.TRACKING_UUID
import com.navi.chat.utils.getCrmWebViewIntent
import com.navi.coin.navigator.NaviCoinDeepLinkNavigator
import com.navi.common.alchemist.genericalchemistscreen.ui.GenericAlchemistActivity
import com.navi.common.constants.VERTICAL
import com.navi.common.model.ModuleNameV2
import com.navi.common.navigation.NavArgs
import com.navi.common.ui.activity.NaviWebViewActivity
@@ -142,10 +144,12 @@ import com.naviapp.utils.deleteCacheAndOpenLoginPage
import com.naviapp.utils.openPlayStore
import com.naviapp.utils.openWhatsAppChatConversation
import com.naviapp.utils.toggleNaviPayIntentActivityState
import com.naviapp.webredirection.presentation.chrometab.CustomChromeTabManager.warmUpAndLaunchCCT
import com.naviapp.webredirection.presentation.utils.BUSINESS_UNIT
import com.naviapp.webredirection.presentation.utils.WEB_PLATFORM_IDENTIFIER
import com.naviapp.webredirection.presentation.utils.WEB_REDIRECTION_SUBTITLE
import com.naviapp.webredirection.presentation.utils.WEB_REDIRECTION_TITLE
import com.naviapp.webredirection.presentation.utils.getAdditionalCCTEvents
import com.naviapp.webredirection.presentation.utils.getWebRedirectionIntent
import java.io.ByteArrayOutputStream
import java.lang.ref.WeakReference
@@ -197,6 +201,7 @@ object NaviDeepLinkNavigator : DeepLinkListener {
private const val ICON_TITLE_DESC_BOTTOMSHEET = "icon_title_desc_bottomsheet"
private const val GOLD = "gold"
const val WEB_URL = "webUrl"
private const val CUSTOM_CHROME_TAB = "customChromeTab"
private const val NAVI_WEB_VIEW = "NAVI_WEB_VIEW"
private const val OPEN_EMAIL = "openEmail"
const val VIEW_VIDEO = "view_video"
@@ -634,8 +639,11 @@ object NaviDeepLinkNavigator : DeepLinkListener {
}
WEB_URL -> {
var url: String? = null
var isCustomChromeTab: Boolean? = false
ctaData.parameters?.forEach { keyValue ->
if (keyValue.key == URL) url = keyValue.value
else if (keyValue.key == CUSTOM_CHROME_TAB)
isCustomChromeTab = keyValue.value.toBoolean()
}
if (url.isNullOrEmpty() && bundle.containsKey(URL)) {
url = bundle.getString(URL)
@@ -643,6 +651,25 @@ object NaviDeepLinkNavigator : DeepLinkListener {
if (url.isNullOrEmpty() && bundle.containsKey(WEB_URL)) {
url = bundle.getString(WEB_URL)
}
if (isCustomChromeTab.orFalse()) {
var vertical: String? = null
val eventAttributes = buildMap {
ctaData.parameters?.forEach { keyValue ->
if (keyValue.key == VERTICAL) vertical = keyValue.value
else if (keyValue.key.isNotNullAndNotEmpty())
put(keyValue.key!!, keyValue.value.toString())
}
}
activity?.let {
warmUpAndLaunchCCT(
activity = activity,
url = url.orEmpty(),
events =
getAdditionalCCTEvents(vertical, eventMap = eventAttributes),
)
return
}
}
url?.let { intent = Intent(Intent.ACTION_VIEW, url?.toUri()) }
}
NAVI_WEB_VIEW -> {

View File

@@ -15,9 +15,7 @@ import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.Lifecycle
@@ -28,7 +26,6 @@ import com.navi.analytics.utils.NaviTrackEvent
import com.navi.base.utils.isNotNullAndNotEmpty
import com.navi.base.utils.orFalse
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.CHROME_CUSTOM_TAB_HEADER_COLOR
import com.navi.common.model.ModuleNameV2
import com.navi.common.ui.activity.BaseActivity
import com.navi.common.ui.errorview.FullScreenErrorComposeView
@@ -42,7 +39,6 @@ import com.navi.common.utils.Constants.VERTICAL_TYPE
import com.navi.common.utils.EMPTY
import com.navi.common.utils.log
import com.navi.common.utils.observeWithTimeout
import com.navi.design.utils.parseColorSafe
import com.naviapp.R
import com.naviapp.analytics.utils.NaviAnalytics
import com.naviapp.analytics.utils.NaviAnalytics.Companion.OPENING_CUSTOM_CHROME_TAB
@@ -204,24 +200,10 @@ class WebRedirectionTransparentActivity : BaseActivity() {
mapOf(URL to url),
)
}
chromeTabManager.setWebUrl(url)
val headerColor =
FirebaseRemoteConfigHelper.getString(CHROME_CUSTOM_TAB_HEADER_COLOR, "")
val headerIntColor: Int =
if (headerColor.isEmpty()) this.getColor(R.color.navi_purple)
else headerColor.parseColorSafe()
val defaultColorSchemeParams =
CustomTabColorSchemeParams.Builder().setToolbarColor(headerIntColor).build()
val customTabsIntent =
CustomTabsIntent.Builder(chromeTabManager.customTabsSession)
.setDefaultColorSchemeParams(defaultColorSchemeParams)
.setShowTitle(false)
.build()
customTabsIntent.intent.setFlags(
Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
CustomChromeTabManager.launchWebPageInCustomChromeTab(
url = url,
activity = this@WebRedirectionTransparentActivity,
)
customTabsIntent.launchUrl(this@WebRedirectionTransparentActivity, Uri.parse(url))
finish()
} else {
/** Fallback to opening the Url in browser, if custom tab is not available * */

View File

@@ -8,12 +8,20 @@
package com.naviapp.webredirection.presentation.chrometab
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsCallback
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsServiceConnection
import androidx.browser.customtabs.CustomTabsSession
import androidx.browser.customtabs.EngagementSignalsCallback
@@ -22,19 +30,33 @@ import com.navi.analytics.utils.NaviTrackEvent
import com.navi.base.AppServiceManager
import com.navi.base.utils.SUCCESS
import com.navi.common.constants.MESSAGE_TEXT
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper
import com.navi.common.firebaseremoteconfig.FirebaseRemoteConfigHelper.CHROME_CUSTOM_TAB_HEADER_COLOR
import com.navi.common.utils.Constants.URL
import com.navi.common.utils.log
import com.navi.design.utils.parseColorSafe
import com.naviapp.R
import com.naviapp.analytics.utils.NaviAnalytics.Companion.CHROME_CUSTOM_TAB_WARMUP_AND_LAUNCH_FAILED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.CHROME_CUSTOM_TAB_WARMUP_FAILED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.CHROME_TAB_MINIMISED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.CHROME_TAB_SERVICE_DISCONNECTION_EXCEPTION
import com.naviapp.analytics.utils.NaviAnalytics.Companion.CHROME_TAB_SESSION_ENDED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.CHROME_TAB_UN_MINIMISED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.ENGAGEMENT_SIGNALS_NOT_SUPPORTED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.OPEN_CHROME_TAB_FAILED_FALLBACK_TO_BROWSER
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_CHROME_CUSTOM_TAB_ABORTED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_CHROME_CUSTOM_TAB_FAILED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_CHROME_CUSTOM_TAB_STARTED
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_CHROME_CUSTOM_TAB_SUCCESS
import com.naviapp.analytics.utils.NaviAnalytics.Companion.REDIRECTION_TO_CHROME_CUSTOM_TAB_UNRESOLVED
import com.naviapp.webredirection.presentation.activity.WebRedirectionTransparentActivity.Companion.CHROME_BROWSER_FOUND
import com.naviapp.webredirection.presentation.activity.WebRedirectionTransparentActivity.Companion.CHROME_PACKAGE
import com.naviapp.webredirection.presentation.activity.WebRedirectionTransparentActivity.Companion.DEFAULT_BROWSER_AVAILABLE
import com.naviapp.webredirection.presentation.activity.WebRedirectionTransparentActivity.Companion.NO_BROWSER_AVAILABLE
import com.naviapp.webredirection.presentation.activity.WebRedirectionTransparentActivity.Companion.REDIRECTION_CHROME_BROWSER_FOUND
import com.naviapp.webredirection.presentation.activity.WebRedirectionTransparentActivity.Companion.REDIRECTION_DEFAULT_BROWSER_AVAILABLE
import com.naviapp.webredirection.presentation.activity.WebRedirectionTransparentActivity.Companion.REDIRECTION_NO_BROWSER_AVAILABLE
import com.naviapp.webredirection.presentation.models.CustomChromeTabCustomEvents
object CustomChromeTabManager {
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
@@ -46,7 +68,9 @@ object CustomChromeTabManager {
private var url: String? = null
private val customTabsCallback: CustomTabsCallback =
private fun customTabsCallback(
additionalEvents: CustomChromeTabCustomEvents
): CustomTabsCallback =
object : CustomTabsCallback() {
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
when (navigationEvent) {
@@ -57,6 +81,7 @@ object CustomChromeTabManager {
isNeededForAppsflyer = false,
isNeededForFirebase = false,
)
additionalEvents.navigationStarted()
}
NAVIGATION_FINISHED -> {
NaviTrackEvent.trackEvent(
@@ -66,6 +91,7 @@ object CustomChromeTabManager {
isNeededForAppsflyer = false,
isNeededForFirebase = false,
)
additionalEvents.navigationFinished()
}
NAVIGATION_FAILED -> {
NaviTrackEvent.trackEvent(
@@ -75,6 +101,7 @@ object CustomChromeTabManager {
isNeededForAppsflyer = false,
isNeededForFirebase = false,
)
additionalEvents.navigationFailed()
}
NAVIGATION_ABORTED -> {
NaviTrackEvent.trackEvent(
@@ -83,6 +110,7 @@ object CustomChromeTabManager {
isNeededForAppsflyer = false,
isNeededForFirebase = false,
)
additionalEvents.navigationAborted()
}
else -> {
NaviTrackEvent.trackEvent(
@@ -91,6 +119,7 @@ object CustomChromeTabManager {
isNeededForAppsflyer = false,
isNeededForFirebase = false,
)
additionalEvents.navigationUnresolved()
}
}
}
@@ -104,6 +133,7 @@ object CustomChromeTabManager {
isNeededForAppsflyer = false,
isNeededForFirebase = false,
)
additionalEvents.minimized()
}
@OptIn(ExperimentalMinimizationCallback::class)
@@ -115,10 +145,11 @@ object CustomChromeTabManager {
isNeededForAppsflyer = false,
isNeededForFirebase = false,
)
additionalEvents.unMinimized()
}
}
private val engagementCallback =
private fun engagementCallback(events: CustomChromeTabCustomEvents) =
object : EngagementSignalsCallback {
override fun onSessionEnded(didUserInteract: Boolean, extras: Bundle) {
super.onSessionEnded(didUserInteract, extras)
@@ -128,15 +159,18 @@ object CustomChromeTabManager {
isNeededForAppsflyer = false,
isNeededForFirebase = false,
)
events.onSessionEnded()
cleanup()
}
}
fun setWebUrl(url: String) {
this.url = url
}
fun warmUpCustomTabs(url: String?) {
/**
* This function should be called to warm up the custom tab and launch it. Used to open url in
* custom tab for cases where we do not need any token exchange. For Ex:- Ads
*
* @param activity The activity from which the custom tab is launched.
*/
fun warmUpAndLaunchCCT(url: String, events: CustomChromeTabCustomEvents, activity: Activity) {
this.url = url
// Use applicationContext to avoid leaking an Activity reference.
val appContext = AppServiceManager.application.applicationContext
@@ -149,17 +183,16 @@ object CustomChromeTabManager {
name: ComponentName,
client: CustomTabsClient,
) {
customTabsClient = client
// Warm up the browser process.
client.warmup(0)
customTabsSession = client.newSession(customTabsCallback)
if (isEngagementSignalsSupported()) {
customTabsSession?.setEngagementSignalsCallback(
engagementCallback,
Bundle(),
)
warmUp(client = client, events = events)
if (packageName != null) {
launchWebPageInCustomChromeTab(activity, url)
} else {
/**
* Fallback to opening the Url in browser, if custom tab is not
* available *
*/
launchWebPageInChrome(activity, url)
}
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
}
override fun onServiceDisconnected(name: ComponentName?) {
@@ -180,6 +213,73 @@ object CustomChromeTabManager {
}
}
fun launchWebPageInCustomChromeTab(activity: Activity, url: String) {
val headerColor = FirebaseRemoteConfigHelper.getString(CHROME_CUSTOM_TAB_HEADER_COLOR, "")
val headerIntColor: Int =
if (headerColor.isEmpty())
AppServiceManager.application.applicationContext.getColor(R.color.navi_purple)
else headerColor.parseColorSafe()
val defaultColorSchemeParams =
CustomTabColorSchemeParams.Builder().setToolbarColor(headerIntColor).build()
val customTabsIntent =
CustomTabsIntent.Builder(customTabsSession)
.setDefaultColorSchemeParams(defaultColorSchemeParams)
.setShowTitle(false)
.build()
customTabsIntent.intent.setFlags(FLAG_ACTIVITY_SINGLE_TOP or FLAG_ACTIVITY_CLEAR_TOP)
customTabsIntent.launchUrl(activity, Uri.parse(url))
}
fun warmUpCustomTabs(
url: String?,
events: CustomChromeTabCustomEvents = CustomChromeTabCustomEvents(),
) {
this.url = url
// Use applicationContext to avoid leaking an Activity reference.
val appContext = AppServiceManager.application.applicationContext
val packageName = CustomTabsClient.getPackageName(appContext, null)
try {
customTabsServiceConnection =
object : CustomTabsServiceConnection() {
@SuppressLint("RequiresFeature")
override fun onCustomTabsServiceConnected(
name: ComponentName,
client: CustomTabsClient,
) {
warmUp(client = client, events = events)
customTabsSession?.mayLaunchUrl(Uri.parse(url), null, null)
}
override fun onServiceDisconnected(name: ComponentName?) {
customTabsClient = null
}
}
customTabsServiceConnection?.let {
CustomTabsClient.bindCustomTabsService(appContext, packageName, it)
}
} catch (e: Exception) {
NaviTrackEvent.trackEvent(
eventName = CHROME_CUSTOM_TAB_WARMUP_AND_LAUNCH_FAILED,
eventValues = mapOf(MESSAGE_TEXT to e.message.orEmpty()),
isNeededForAppsflyer = false,
isNeededForFirebase = false,
)
e.log()
}
}
@SuppressLint("RequiresFeature")
private fun warmUp(client: CustomTabsClient, events: CustomChromeTabCustomEvents) {
customTabsClient = client
// Warm up the browser process.
client.warmup(0)
customTabsSession = client.newSession(customTabsCallback(events))
if (isEngagementSignalsSupported()) {
customTabsSession?.setEngagementSignalsCallback(engagementCallback(events), Bundle())
}
}
fun cleanup() {
// Unbind the service to avoid memory leaks.
customTabsServiceConnection?.let { connection ->
@@ -198,6 +298,7 @@ object CustomChromeTabManager {
customTabsServiceConnection = null
customTabsClient = null
customTabsSession = null
journeyTypeIdentifier = null
}
private fun isEngagementSignalsSupported(): Boolean {
@@ -228,4 +329,48 @@ object CustomChromeTabManager {
}
return false
}
/**
* Opens a web page in the default web browser, specifically targeting Google Chrome.
*
* This function attempts to launch an intent that directs the user to a specified URL. If
* Google Chrome is not installed on the device, it falls back to any available web browser.
*
* @param url The URL of the web page to be opened. It should be a valid URL format (e.g.,
* "https://www.example.com").
* @throws ActivityNotFoundException if no applications can handle the intent to view the URL.
*/
private fun launchWebPageInChrome(activity: Activity, url: String) {
NaviTrackEvent.trackEvent(
eventName =
"redirection_${journeyTypeIdentifier}_$OPEN_CHROME_TAB_FAILED_FALLBACK_TO_BROWSER",
mapOf(URL to url, "reason" to "chrome_tab_not_available"),
)
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { setPackage(CHROME_PACKAGE) }
try {
activity.startActivity(intent)
NaviTrackEvent.trackEvent(eventName = CHROME_BROWSER_FOUND)
NaviTrackEvent.trackEvent(eventName = REDIRECTION_CHROME_BROWSER_FOUND)
} catch (e: ActivityNotFoundException) {
intent.setPackage(null)
e.log()
try {
activity.startActivity(intent)
NaviTrackEvent.trackEvent(eventName = DEFAULT_BROWSER_AVAILABLE)
NaviTrackEvent.trackEvent(eventName = REDIRECTION_DEFAULT_BROWSER_AVAILABLE)
} catch (e: Exception) {
NaviTrackEvent.trackEvent(eventName = NO_BROWSER_AVAILABLE)
NaviTrackEvent.trackEvent(eventName = REDIRECTION_NO_BROWSER_AVAILABLE)
Toast.makeText(
activity,
activity.baseContext.getString(R.string.no_browser_found),
Toast.LENGTH_LONG,
)
.show()
e.log()
}
}
}
}

View File

@@ -0,0 +1,19 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.webredirection.presentation.models
data class CustomChromeTabCustomEvents(
val navigationStarted: () -> Unit = {},
val navigationFinished: () -> Unit = {},
val navigationFailed: () -> Unit = {},
val navigationAborted: () -> Unit = {},
val navigationUnresolved: () -> Unit = {},
val minimized: () -> Unit = {},
val unMinimized: () -> Unit = {},
val onSessionEnded: () -> Unit = {},
)

View File

@@ -0,0 +1,16 @@
/*
*
* * Copyright © 2025 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
package com.naviapp.webredirection.presentation.models
/**
* enum class represents type of external redirection for custom chrome tab For different type we
* can configure different events
*/
enum class CustomChromeTabVerticalType {
ADS
}

View File

@@ -9,7 +9,8 @@ package com.naviapp.webredirection.presentation.utils
import android.app.Activity
import android.content.Intent
import com.navi.analytics.utils.NaviTrackEvent
import com.navi.adverse.sdk.utils.AdverseTrackEvent
import com.navi.analytics.utils.NaviTrackEvent.trackEvent
import com.navi.base.sharedpref.PreferenceManager
import com.navi.base.utils.orFalse
import com.navi.chat.ui.activities.CRMWebViewActivity
@@ -21,6 +22,8 @@ import com.navi.common.useruploaddata.model.PreSignedUrlListResponse
import com.naviapp.analytics.utils.NaviAnalytics.Companion.WEB_TAB_REDIRECTION_INTENT
import com.naviapp.webredirection.presentation.activity.WebRedirectionActivity
import com.naviapp.webredirection.presentation.activity.WebRedirectionTransparentActivity
import com.naviapp.webredirection.presentation.models.CustomChromeTabCustomEvents
import com.naviapp.webredirection.presentation.models.CustomChromeTabVerticalType
import com.naviapp.webredirection.presentation.viewModel.WebRedirectionData
fun isFirstLaunchIn24Hours(): Boolean {
@@ -77,7 +80,7 @@ fun getWebRedirectionIntent(
}
}
}
NaviTrackEvent.trackEvent(
trackEvent(
eventName = WEB_TAB_REDIRECTION_INTENT,
eventValues = mapOf("identifier" to identifier),
isNeededForAppsflyer = false,
@@ -94,3 +97,37 @@ fun getWebRedirectionIntent(
}
return Intent(sourceActivity, targetActivity)
}
fun getAdditionalCCTEvents(
type: String?,
eventMap: Map<String, String>,
): CustomChromeTabCustomEvents {
return when (type) {
CustomChromeTabVerticalType.ADS.name -> getAdsEvents(eventMap)
else -> CustomChromeTabCustomEvents()
}
}
private fun getAdsEvents(eventMap: Map<String, String>): CustomChromeTabCustomEvents {
return CustomChromeTabCustomEvents(
navigationStarted = {
AdverseTrackEvent.trackEvent("NaviApp_adverse_custom_tab_redirection_started", eventMap)
},
navigationFinished = {
AdverseTrackEvent.trackEvent("NaviApp_adverse_custom_tab_redirection_success", eventMap)
},
navigationFailed = {
AdverseTrackEvent.trackEvent("NaviApp_adverse_custom_tab_redirection_failed", eventMap)
},
minimized = {
AdverseTrackEvent.trackEvent("NaviApp_adverse_custom_tab_minimised", eventMap)
},
unMinimized = {
AdverseTrackEvent.trackEvent("NaviApp_adverse_custom_tab_maximised", eventMap)
},
onSessionEnded = {
AdverseTrackEvent.trackEvent("NaviApp_adverse_custom_tab_closed_click", eventMap)
},
)
}