Skip to content

Commit

Permalink
Closes mozilla-mobile#4257: Migrate feature-customtabs from browser-s…
Browse files Browse the repository at this point in the history
…ession to browser-state.
  • Loading branch information
pocmo committed Jan 11, 2021
1 parent b44c304 commit 5784c27
Show file tree
Hide file tree
Showing 11 changed files with 814 additions and 475 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,18 @@ fun createCustomTab(
url: String,
id: String = UUID.randomUUID().toString(),
config: CustomTabConfig = CustomTabConfig(),
title: String = "",
contextId: String? = null,
engineSession: EngineSession? = null,
mediaSessionState: MediaSessionState? = null,
crashed: Boolean = false
): CustomTabSessionState {
return CustomTabSessionState(
id = id,
content = ContentState(url),
content = ContentState(
url = url,
title = title
),
config = config,
mediaSessionState = mediaSessionState,
contextId = contextId,
Expand Down
6 changes: 4 additions & 2 deletions components/feature/customtabs/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {

dependencies {
implementation project(':browser-menu')
implementation project(':browser-session')
implementation project(':browser-state')
implementation project(':browser-toolbar')
implementation project(':concept-engine')
implementation project(':concept-fetch')
implementation project(':concept-menu')
implementation project(':feature-session')
implementation project(':feature-intent')
implementation project(':feature-tabs')
implementation project(':service-digitalassetlinks')
implementation project(':support-base')
implementation project(':support-ktx')
Expand All @@ -49,7 +50,8 @@ dependencies {
api Dependencies.androidx_browser

testImplementation project(':support-test')

testImplementation project(':support-test-libstate')
testImplementation project(':browser-session')
testImplementation Dependencies.androidx_test_core
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.testing_coroutines
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,17 @@ import android.content.Intent.ACTION_VIEW
import android.content.res.Resources
import android.provider.Browser
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.EngineSession
import mozilla.components.feature.intent.ext.putSessionId
import mozilla.components.feature.intent.processing.IntentProcessor
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tabs.CustomTabsUseCases
import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent

/**
* Processor for intents which trigger actions related to custom tabs.
*/
class CustomTabIntentProcessor(
private val sessionManager: SessionManager,
private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase,
private val addCustomTabUseCase: CustomTabsUseCases.AddCustomTabUseCase,
private val resources: Resources,
private val isPrivate: Boolean = false
) : IntentProcessor {
Expand Down Expand Up @@ -58,12 +53,9 @@ class CustomTabIntentProcessor(
val url = safeIntent.dataString

return if (!url.isNullOrEmpty() && matches(intent)) {
val session = Session(url, private = isPrivate, source = SessionState.Source.CUSTOM_TAB)
session.customTabConfig = createCustomTabConfigFromIntent(intent, resources)

sessionManager.add(session)
loadUrlUseCase(url, session.id, EngineSession.LoadUrlFlags.external(), getAdditionalHeaders(safeIntent))
intent.putSessionId(session.id)
val config = createCustomTabConfigFromIntent(intent, resources)
val customTabId = addCustomTabUseCase(url, config, isPrivate, getAdditionalHeaders(safeIntent))
intent.putSessionId(customTabId)

true
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,31 @@ import androidx.annotation.ColorInt
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.core.graphics.drawable.toDrawable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.BrowserMenuItem
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.runWithSession
import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.state.CustomTabActionButtonConfig
import mozilla.components.browser.state.state.CustomTabMenuItem
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.feature.customtabs.feature.CustomTabSessionTitleObserver
import mozilla.components.feature.customtabs.menu.sendWithUrl
import mozilla.components.feature.tabs.CustomTabsUseCases
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.android.content.share
import mozilla.components.support.ktx.android.util.dpToPx
import mozilla.components.support.ktx.android.view.setNavigationBarTheme
import mozilla.components.support.ktx.android.view.setStatusBarTheme
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.utils.ColorUtils.getReadableTextColor

/**
Expand All @@ -42,50 +49,54 @@ import mozilla.components.support.utils.ColorUtils.getReadableTextColor
* @param shareListener Invoked when the share button is pressed.
* @param closeListener Invoked when the close button is pressed.
*/
@Suppress("LargeClass", "Deprecation")
// SessionManager observable is deprecated. This feature needs to be migrated to browser-state:
// https://github.com/mozilla-mobile/android-components/issues/4257
@Suppress("LargeClass", "LongParameterList")
class CustomTabsToolbarFeature(
private val sessionManager: SessionManager,
private val store: BrowserStore,
private val toolbar: BrowserToolbar,
private val sessionId: String? = null,
private val useCases: CustomTabsUseCases,
private val menuBuilder: BrowserMenuBuilder? = null,
private val menuItemIndex: Int = menuBuilder?.items?.size ?: 0,
private val window: Window? = null,
private val shareListener: (() -> Unit)? = null,
private val closeListener: () -> Unit
) : LifecycleAwareFeature, UserInteractionHandler {

private val sessionObserver = CustomTabSessionTitleObserver(toolbar)
private var initialized: Boolean = false
private val titleObserver = CustomTabSessionTitleObserver(toolbar)
private val context get() = toolbar.context
internal var initialized = false
internal var readableColor = Color.WHITE
private var scope: CoroutineScope? = null

/**
* Initializes the feature and registers the [CustomTabSessionTitleObserver].
*/
override fun start() {
if (initialized) return
val tabId = sessionId ?: return
val tab = store.state.findCustomTab(tabId) ?: return

scope = store.flowScoped { flow ->
flow
.mapNotNull { state -> state.findCustomTab(tabId) }
.ifAnyChanged { tab -> arrayOf(tab.content.title, tab.content.url) }
.collect { tab -> titleObserver.onTab(tab) }
}

initialized = sessionManager.runWithSession(sessionId) {
it.register(sessionObserver)
initialize(it)
if (!initialized) {
initialized = true
init(tab)
}
}

/**
* Unregisters the [CustomTabSessionTitleObserver].
*/
override fun stop() {
sessionManager.runWithSession(sessionId) {
it.unregister(sessionObserver)
true
}
scope?.cancel()
}

@VisibleForTesting
internal fun initialize(session: Session): Boolean {
val config = session.customTabConfig ?: return false
internal fun init(tab: CustomTabSessionState) {
val config = tab.config

// Don't allow clickable toolbar so a custom tab can't switch to edit mode.
toolbar.display.onUrlClicked = { false }
Expand All @@ -100,23 +111,21 @@ class CustomTabsToolbarFeature(

// Add navigation close action
if (config.showCloseButton) {
addCloseButton(session, config.closeButtonIcon)
addCloseButton(tab, config.closeButtonIcon)
}

// Add action button
addActionButton(session, config.actionButtonConfig)
addActionButton(tab, config.actionButtonConfig)

// Show share button
if (config.showShareMenuItem) {
addShareButton(session)
addShareButton(tab)
}

// Add menu items
if (config.menuItems.isNotEmpty() || menuBuilder?.items?.isNotEmpty() == true) {
addMenuItems(session, config.menuItems, menuItemIndex)
addMenuItems(tab, config.menuItems, menuItemIndex)
}

return true
}

@VisibleForTesting
Expand Down Expand Up @@ -145,7 +154,7 @@ class CustomTabsToolbarFeature(
* When clicked, it calls [closeListener].
*/
@VisibleForTesting
internal fun addCloseButton(session: Session, bitmap: Bitmap?) {
internal fun addCloseButton(tab: CustomTabSessionState, bitmap: Bitmap?) {
val drawableIcon = bitmap?.toDrawable(context.resources)
?: getDrawable(context, R.drawable.mozac_ic_close)!!.mutate()

Expand All @@ -156,7 +165,7 @@ class CustomTabsToolbarFeature(
context.getString(R.string.mozac_feature_customtabs_exit_button)
) {
emitCloseFact()
sessionManager.remove(session)
useCases.remove(tab.id)
closeListener.invoke()
}
toolbar.addNavigationAction(button)
Expand All @@ -167,7 +176,7 @@ class CustomTabsToolbarFeature(
* When clicked, it activates the corresponding [PendingIntent].
*/
@VisibleForTesting
internal fun addActionButton(session: Session, buttonConfig: CustomTabActionButtonConfig?) {
internal fun addActionButton(tab: CustomTabSessionState, buttonConfig: CustomTabActionButtonConfig?) {
buttonConfig?.let { config ->
val drawableIcon = Bitmap.createScaledBitmap(
config.icon,
Expand All @@ -184,7 +193,7 @@ class CustomTabsToolbarFeature(
config.description
) {
emitActionButtonFact()
config.pendingIntent.sendWithUrl(context, session.url)
config.pendingIntent.sendWithUrl(context, tab.content.url)
}

toolbar.addBrowserAction(button)
Expand All @@ -196,15 +205,15 @@ class CustomTabsToolbarFeature(
* When clicked, it activates [shareListener] and defaults to the [share] KTX helper.
*/
@VisibleForTesting
internal fun addShareButton(session: Session) {
internal fun addShareButton(tab: CustomTabSessionState) {
val drawableIcon = getDrawable(context, R.drawable.mozac_ic_share)!!
drawableIcon.setTint(readableColor)

val button = Toolbar.ActionButton(
drawableIcon,
context.getString(R.string.mozac_feature_customtabs_share_link)
) {
val listener = shareListener ?: { context.share(session.url) }
val listener = shareListener ?: { context.share(tab.content.url) }
emitActionButtonFact()
listener.invoke()
}
Expand All @@ -217,13 +226,13 @@ class CustomTabsToolbarFeature(
*/
@VisibleForTesting
internal fun addMenuItems(
session: Session,
tab: CustomTabSessionState,
menuItems: List<CustomTabMenuItem>,
index: Int
) {
menuItems.map { item ->
SimpleBrowserMenuItem(item.name) {
item.pendingIntent.sendWithUrl(context, session.url)
item.pendingIntent.sendWithUrl(context, tab.content.url)
}
}.also { items ->
val combinedItems = menuBuilder?.let { builder ->
Expand Down Expand Up @@ -253,10 +262,11 @@ class CustomTabsToolbarFeature(
return if (!initialized) {
false
} else {
sessionManager.runWithSession(sessionId) {
if (sessionId != null && useCases.remove(sessionId)) {
closeListener.invoke()
remove(it)
true
} else {
false
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,47 @@

package mozilla.components.feature.customtabs.feature

import mozilla.components.browser.session.Session
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.concept.toolbar.Toolbar

/**
* Sets the title of the custom tab toolbar based on the session title and URL.
*/
class CustomTabSessionTitleObserver(
private val toolbar: Toolbar
) : Session.Observer {
private var receivedTitle = false
) {
private var url: String? = null
private var title: String? = null
private var showedTitle = false

override fun onUrlChanged(session: Session, url: String) {
internal fun onTab(tab: CustomTabSessionState) {
if (tab.content.title != title) {
onTitleChanged(tab)
title = tab.content.title
}

if (tab.content.url != url) {
onUrlChanged(tab)
url = tab.content.url
}
}

private fun onUrlChanged(tab: CustomTabSessionState) {
// If we showed a title once in a custom tab then we are going to continue displaying
// a title (to avoid the layout bouncing around). However if no title is available then
// we just use the URL.
if (receivedTitle && session.title.isEmpty()) {
toolbar.title = url
if (showedTitle && tab.content.title.isEmpty()) {
toolbar.title = tab.content.url
}
}

override fun onTitleChanged(session: Session, title: String) {
if (title.isNotEmpty()) {
toolbar.title = title
receivedTitle = true
} else if (receivedTitle) {
private fun onTitleChanged(tab: CustomTabSessionState) {
if (tab.content.title.isNotEmpty()) {
toolbar.title = tab.content.title
showedTitle = true
} else if (showedTitle) {
// See comment in OnUrlChanged().
toolbar.title = session.url
toolbar.title = tab.content.url
}
}
}
Loading

0 comments on commit 5784c27

Please sign in to comment.