From d01cab7f161f2b4e746519e26167b8cefebae6ff Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Tue, 16 Jul 2019 16:33:23 -0400 Subject: [PATCH] Fixes #1884 - Intent code to feature-customtabs --- .../browser/session/tab/CustomTabConfig.kt | 115 +++++----- .../components/browser/session/SessionTest.kt | 9 +- .../session/tab/CustomTabConfigTest.kt | 37 +--- .../customtabs/CustomTabConfigHelper.kt | 134 ++++++++++++ .../customtabs/CustomTabIntentProcessor.kt | 12 +- .../customtabs/src/main/res/values/dimens.xml | 7 + .../customtabs/CustomTabConfigHelperTest.kt | 198 ++++++++++++++++++ .../CustomTabIntentProcessorTest.kt | 2 +- .../feature/intent/IntentProcessor.kt | 3 +- .../feature/intent/IntentProcessorTest.kt | 1 + .../components/support/utils/SafeBundle.kt | 35 ++-- .../components/support/utils/SafeIntent.kt | 7 +- .../support/utils/SafeBundleTest.kt | 67 ++++++ docs/changelog.md | 12 ++ .../samples/browser/DefaultComponents.kt | 2 +- 15 files changed, 519 insertions(+), 122 deletions(-) create mode 100644 components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt create mode 100644 components/feature/customtabs/src/main/res/values/dimens.xml create mode 100644 components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt create mode 100644 components/support/utils/src/test/java/mozilla/components/support/utils/SafeBundleTest.kt diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/tab/CustomTabConfig.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/tab/CustomTabConfig.kt index e7910a4c259..70e155149f6 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/tab/CustomTabConfig.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/tab/CustomTabConfig.kt @@ -8,30 +8,60 @@ import android.app.PendingIntent import android.graphics.Bitmap import android.os.Bundle import android.os.Parcelable -import androidx.browser.customtabs.CustomTabsIntent import android.util.DisplayMetrics import androidx.annotation.ColorInt - +import androidx.browser.customtabs.CustomTabsIntent import mozilla.components.support.utils.SafeBundle import mozilla.components.support.utils.SafeIntent import java.util.ArrayList -import java.util.Collections import java.util.UUID /** - * Holds configuration data for a Custom Tab. Use [createFromIntent] to - * create instances. + * Holds configuration data for a Custom Tab. + * + * @property toolbarColor Background color for the toolbar. + * @property closeButtonIcon Custom icon of the back button on the toolbar. + * @property enableUrlbarHiding Enables the toolbar to hide as the user scrolls down on the page. + * @property actionButtonConfig Custom action button on the toolbar. + * @property showShareMenuItem Specifies whether a default share button will be shown in the menu. + * @property menuItems Custom overflow menu items. + * @property exitAnimations Bundle containing custom exit animations for the tab. + * @property titleVisible Whether the title should be shown in the custom tab. */ -class CustomTabConfig internal constructor( +data class CustomTabConfig( val id: String, @ColorInt val toolbarColor: Int?, val closeButtonIcon: Bitmap?, - val disableUrlbarHiding: Boolean, + val enableUrlbarHiding: Boolean, val actionButtonConfig: CustomTabActionButtonConfig?, val showShareMenuItem: Boolean, - val menuItems: List, - val options: List + val menuItems: List = emptyList(), + val exitAnimations: Bundle? = null, + val titleVisible: Boolean = false ) { + inline val disableUrlbarHiding + get() = !enableUrlbarHiding + + val options: List by lazy { generateOptions() } + + @Suppress("ComplexMethod") + private fun generateOptions(): List { + val options = mutableListOf() + + if (toolbarColor != null) options.add(TOOLBAR_COLOR_OPTION) + if (closeButtonIcon != null) options.add(CLOSE_BUTTON_OPTION) + if (enableUrlbarHiding) options.add(DISABLE_URLBAR_HIDING_OPTION) + if (actionButtonConfig != null) options.add(ACTION_BUTTON_OPTION) + if (showShareMenuItem) options.add(SHARE_MENU_ITEM_OPTION) + if (menuItems.isNotEmpty()) options.add(CUSTOMIZED_MENU_OPTION) + if (actionButtonConfig?.tint == true) options.add(ACTION_BUTTON_TINT_OPTION) + + if (exitAnimations != null) options.add(EXIT_ANIMATION_OPTION) + if (titleVisible) options.add(PAGE_TITLE_OPTION) + + return options + } + companion object { internal const val TOOLBAR_COLOR_OPTION = "hasToolbarColor" internal const val CLOSE_BUTTON_OPTION = "hasCloseButton" @@ -57,9 +87,6 @@ class CustomTabConfig internal constructor( private val EXTRA_DEFAULT_SHARE_MENU_ITEM = StringBuilder("support.customtabs.extra.SHARE_MENU_ITEM").toExtra() private val EXTRA_MENU_ITEMS = StringBuilder("support.customtabs.extra.MENU_ITEMS").toExtra() private val KEY_MENU_ITEM_TITLE = StringBuilder("support.customtabs.customaction.MENU_ITEM_TITLE").toExtra() - private val EXTRA_TINT_ACTION_BUTTON = StringBuilder("support.customtabs.extra.TINT_ACTION_BUTTON").toExtra() - private val EXTRA_REMOTEVIEWS = StringBuilder("support.customtabs.extra.EXTRA_REMOTEVIEWS").toExtra() - private val EXTRA_TOOLBAR_ITEMS = StringBuilder("support.customtabs.extra.TOOLBAR_ITEMS").toExtra() /** * TODO remove when fixed: https://github.com/mozilla-mobile/android-components/issues/1884 @@ -72,6 +99,8 @@ class CustomTabConfig internal constructor( * @param intent the intent to check, wrapped as a SafeIntent. * @return true if the intent is a custom tab intent, otherwise false. */ + @Deprecated("Use isCustomTabIntent in feature-customtabs", + ReplaceWith("isCustomTabIntent(intent.unsafe)", "mozilla.components.feature.customtabs.isCustomTabIntent")) fun isCustomTabIntent(intent: SafeIntent): Boolean { return intent.hasExtra(EXTRA_SESSION) } @@ -84,14 +113,16 @@ class CustomTabConfig internal constructor( * @param displayMetrics needed in-order to verify that icons of a max size are only provided. * @return the CustomTabConfig instance. */ - @Suppress("ComplexMethod") + @Deprecated("Use createCustomTabConfigFromIntent in feature-customtabs", + ReplaceWith( + "createCustomTabConfigFromIntent(intent.unsafe)", + "mozilla.components.feature.customtabs.createCustomTabConfigFromIntent" + )) + @Suppress("LongMethod", "ComplexMethod") fun createFromIntent(intent: SafeIntent, displayMetrics: DisplayMetrics? = null): CustomTabConfig { val id = UUID.randomUUID().toString() - val options = mutableListOf() - val toolbarColor = if (intent.hasExtra(EXTRA_TOOLBAR_COLOR)) { - options.add(TOOLBAR_COLOR_OPTION) intent.getIntExtra(EXTRA_TOOLBAR_COLOR, -1) } else { null @@ -104,57 +135,25 @@ class CustomTabConfig internal constructor( icon.width / density <= MAX_CLOSE_BUTTON_SIZE_DP && icon.height / density <= MAX_CLOSE_BUTTON_SIZE_DP ) { - options.add(CLOSE_BUTTON_OPTION) icon } else { null } } - val disableUrlbarHiding = !intent.getBooleanExtra(EXTRA_ENABLE_URLBAR_HIDING, true) - if (!disableUrlbarHiding) { - options.add(DISABLE_URLBAR_HIDING_OPTION) - } + val enableUrlbarHiding = intent.getBooleanExtra(EXTRA_ENABLE_URLBAR_HIDING, true) val actionButtonConfig = getActionButtonConfig(intent) - if (actionButtonConfig != null) { - options.add(ACTION_BUTTON_OPTION) - } val showShareMenuItem = intent.getBooleanExtra(EXTRA_DEFAULT_SHARE_MENU_ITEM, false) - if (showShareMenuItem) { - options.add(SHARE_MENU_ITEM_OPTION) - } val menuItems = getMenuItems(intent) - if (menuItems.isNotEmpty()) { - options.add(CUSTOMIZED_MENU_OPTION) - } - - if (intent.hasExtra(EXTRA_TINT_ACTION_BUTTON)) { - options.add(ACTION_BUTTON_TINT_OPTION) - } - - if (intent.hasExtra(EXTRA_REMOTEVIEWS) || intent.hasExtra(EXTRA_TOOLBAR_ITEMS)) { - options.add(BOTTOM_TOOLBAR_OPTION) - } - - if (intent.hasExtra(CustomTabsIntent.EXTRA_EXIT_ANIMATION_BUNDLE)) { - options.add(EXIT_ANIMATION_OPTION) - } - - if (intent.hasExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE)) { - val titleVisibility = intent.getIntExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, 0) - if (titleVisibility == CustomTabsIntent.SHOW_PAGE_TITLE) { - options.add(PAGE_TITLE_OPTION) - } - } // We are currently ignoring EXTRA_SECONDARY_TOOLBAR_COLOR and EXTRA_ENABLE_INSTANT_APPS // due to https://github.com/mozilla-mobile/focus-android/issues/629 - return CustomTabConfig(id, toolbarColor, closeButtonIcon, disableUrlbarHiding, actionButtonConfig, - showShareMenuItem, menuItems, Collections.unmodifiableList(options)) + return CustomTabConfig(id, toolbarColor, closeButtonIcon, enableUrlbarHiding, actionButtonConfig, + showShareMenuItem, menuItems) } private fun getActionButtonConfig(intent: SafeIntent): CustomTabActionButtonConfig? { @@ -193,5 +192,15 @@ class CustomTabConfig internal constructor( } } -data class CustomTabActionButtonConfig(val description: String, val icon: Bitmap, val pendingIntent: PendingIntent) -data class CustomTabMenuItem(val name: String, val pendingIntent: PendingIntent) +data class CustomTabActionButtonConfig( + val description: String, + val icon: Bitmap, + val pendingIntent: PendingIntent, + val id: Int = CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID, + val tint: Boolean = false +) + +data class CustomTabMenuItem( + val name: String, + val pendingIntent: PendingIntent +) diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt index c654a7321e8..8425be03f40 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt @@ -289,7 +289,14 @@ class SessionTest { assertNull(session.customTabConfig) - val customTabConfig = CustomTabConfig("id", null, null, true, null, true, listOf(), listOf()) + val customTabConfig = CustomTabConfig( + "id", + toolbarColor = null, + closeButtonIcon = null, + enableUrlbarHiding = true, + actionButtonConfig = null, + showShareMenuItem = true + ) session.customTabConfig = customTabConfig assertEquals(customTabConfig, session.customTabConfig) diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/tab/CustomTabConfigTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/tab/CustomTabConfigTest.kt index abfd1a34787..89376d644d8 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/tab/CustomTabConfigTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/tab/CustomTabConfigTest.kt @@ -25,6 +25,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock +@Suppress("Deprecation") @RunWith(AndroidJUnit4::class) class CustomTabConfigTest { @@ -185,40 +186,4 @@ class CustomTabConfigTest { assertNotNull(customTabConfig) assertNull(customTabConfig.actionButtonConfig) } - - @Test - fun createFromIntentWithActionButtonTint() { - val customTabsIntent = CustomTabsIntent.Builder().build() - customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_TINT_ACTION_BUTTON, true) - - val customTabConfig = CustomTabConfig.createFromIntent(SafeIntent((customTabsIntent.intent))) - assertTrue(customTabConfig.options.contains(CustomTabConfig.ACTION_BUTTON_TINT_OPTION)) - } - - @Test - fun createFromIntentWithBottomToolbarOption() { - val customTabsIntent = CustomTabsIntent.Builder().build() - customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_TOOLBAR_ITEMS, Bundle()) - - val customTabConfig = CustomTabConfig.createFromIntent(SafeIntent((customTabsIntent.intent))) - assertTrue(customTabConfig.options.contains(CustomTabConfig.BOTTOM_TOOLBAR_OPTION)) - } - - @Test - fun createFromIntentWithExitAnimationOption() { - val customTabsIntent = CustomTabsIntent.Builder().build() - customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_EXIT_ANIMATION_BUNDLE, Bundle()) - - val customTabConfig = CustomTabConfig.createFromIntent(SafeIntent((customTabsIntent.intent))) - assertTrue(customTabConfig.options.contains(CustomTabConfig.EXIT_ANIMATION_OPTION)) - } - - @Test - fun createFromIntentWithPageTitleOption() { - val customTabsIntent = CustomTabsIntent.Builder().build() - customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.SHOW_PAGE_TITLE) - - val customTabConfig = CustomTabConfig.createFromIntent(SafeIntent((customTabsIntent.intent))) - assertTrue(customTabConfig.options.contains(CustomTabConfig.PAGE_TITLE_OPTION)) - } } diff --git a/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt b/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt new file mode 100644 index 00000000000..81b398669dd --- /dev/null +++ b/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabConfigHelper.kt @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.customtabs + +import android.app.PendingIntent +import android.content.Intent +import android.content.res.Resources +import android.graphics.Bitmap +import android.os.Bundle +import android.os.Parcelable +import androidx.annotation.ColorInt +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_CLOSE_BUTTON_ICON +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_DEFAULT_SHARE_MENU_ITEM +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_ENABLE_URLBAR_HIDING +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_EXIT_ANIMATION_BUNDLE +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_MENU_ITEMS +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_SESSION +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_TINT_ACTION_BUTTON +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE +import androidx.browser.customtabs.CustomTabsIntent.EXTRA_TOOLBAR_COLOR +import androidx.browser.customtabs.CustomTabsIntent.KEY_DESCRIPTION +import androidx.browser.customtabs.CustomTabsIntent.KEY_ICON +import androidx.browser.customtabs.CustomTabsIntent.KEY_ID +import androidx.browser.customtabs.CustomTabsIntent.KEY_MENU_ITEM_TITLE +import androidx.browser.customtabs.CustomTabsIntent.KEY_PENDING_INTENT +import androidx.browser.customtabs.CustomTabsIntent.NO_TITLE +import androidx.browser.customtabs.CustomTabsIntent.SHOW_PAGE_TITLE +import androidx.browser.customtabs.CustomTabsIntent.TOOLBAR_ACTION_BUTTON_ID +import mozilla.components.browser.session.tab.CustomTabActionButtonConfig +import mozilla.components.browser.session.tab.CustomTabConfig +import mozilla.components.browser.session.tab.CustomTabMenuItem +import mozilla.components.support.utils.SafeIntent +import mozilla.components.support.utils.toSafeBundle +import mozilla.components.support.utils.toSafeIntent +import java.util.UUID +import kotlin.math.max + +/** + * Checks if the provided intent is a custom tab intent. + * + * @param intent the intent to check. + * @return true if the intent is a custom tab intent, otherwise false. + */ +fun isCustomTabIntent(intent: Intent) = isCustomTabIntent(intent.toSafeIntent()) + +/** + * Checks if the provided intent is a custom tab intent. + * + * @param intent the intent to check, wrapped as a SafeIntent. + * @return true if the intent is a custom tab intent, otherwise false. + */ +fun isCustomTabIntent(safeIntent: SafeIntent) = safeIntent.hasExtra(EXTRA_SESSION) + +/** + * Creates a [CustomTabConfig] instance based on the provided intent. + * + * @param intent the intent, wrapped as a SafeIntent, which is processed to extract configuration data. + * @param resources needed in-order to verify that icons of a max size are only provided. + * @return the CustomTabConfig instance. + */ +fun createCustomTabConfigFromIntent( + intent: Intent, + resources: Resources +): CustomTabConfig { + val safeIntent = intent.toSafeIntent() + + return CustomTabConfig( + id = UUID.randomUUID().toString(), + toolbarColor = safeIntent.getColorExtra(EXTRA_TOOLBAR_COLOR), + closeButtonIcon = getCloseButtonIcon(safeIntent, resources), + enableUrlbarHiding = safeIntent.getBooleanExtra(EXTRA_ENABLE_URLBAR_HIDING, false), + actionButtonConfig = getActionButtonConfig(safeIntent), + showShareMenuItem = safeIntent.getBooleanExtra(EXTRA_DEFAULT_SHARE_MENU_ITEM, false), + menuItems = getMenuItems(safeIntent), + exitAnimations = safeIntent.getBundleExtra(EXTRA_EXIT_ANIMATION_BUNDLE)?.unsafe, + titleVisible = safeIntent.getIntExtra(EXTRA_TITLE_VISIBILITY_STATE, NO_TITLE) == SHOW_PAGE_TITLE + ) +} + +@ColorInt +private fun SafeIntent.getColorExtra(name: String): Int? = + if (hasExtra(name)) getIntExtra(name, 0) else null + +private fun getCloseButtonIcon(intent: SafeIntent, resources: Resources): Bitmap? { + val icon = intent.getParcelableExtra(EXTRA_CLOSE_BUTTON_ICON) as? Bitmap + val maxSize = resources.getDimension(R.dimen.mozac_feature_customtabs_max_close_button_size) + + return if (icon != null && max(icon.width, icon.height) <= maxSize) { + icon + } else { + null + } +} + +private fun getActionButtonConfig(intent: SafeIntent): CustomTabActionButtonConfig? { + val actionButtonBundle = intent.getBundleExtra(EXTRA_ACTION_BUTTON_BUNDLE) ?: return null + val description = actionButtonBundle.getString(KEY_DESCRIPTION) + val icon = actionButtonBundle.getParcelable(KEY_ICON) as? Bitmap + val pendingIntent = actionButtonBundle.getParcelable(KEY_PENDING_INTENT) as? PendingIntent + val id = actionButtonBundle.getInt(KEY_ID, TOOLBAR_ACTION_BUTTON_ID) + val tint = intent.getBooleanExtra(EXTRA_TINT_ACTION_BUTTON, false) + + return if (description != null && icon != null && pendingIntent != null) { + CustomTabActionButtonConfig( + id = id, + description = description, + icon = icon, + pendingIntent = pendingIntent, + tint = tint + ) + } else { + null + } +} + +private fun getMenuItems(intent: SafeIntent): List = + intent.getParcelableArrayListExtra(EXTRA_MENU_ITEMS).orEmpty() + .mapNotNull { menuItemBundle -> + val bundle = (menuItemBundle as? Bundle)?.toSafeBundle() + val name = bundle?.getString(KEY_MENU_ITEM_TITLE) + val pendingIntent = bundle?.getParcelable(KEY_PENDING_INTENT) as? PendingIntent + + if (name != null && pendingIntent != null) { + CustomTabMenuItem( + name = name, + pendingIntent = pendingIntent + ) + } else { + null + } + } diff --git a/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt b/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt index f59ef48a197..857d1b2527c 100644 --- a/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt +++ b/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt @@ -6,15 +6,15 @@ package mozilla.components.feature.customtabs import android.content.Intent import android.content.Intent.ACTION_VIEW -import android.util.DisplayMetrics +import android.content.res.Resources import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.intent.IntentProcessor import mozilla.components.browser.session.intent.putSessionId -import mozilla.components.browser.session.tab.CustomTabConfig import mozilla.components.concept.engine.EngineSession import mozilla.components.feature.session.SessionUseCases import mozilla.components.support.utils.SafeIntent +import mozilla.components.support.utils.toSafeIntent /** * Processor for intents which trigger actions related to custom tabs. @@ -22,12 +22,12 @@ import mozilla.components.support.utils.SafeIntent class CustomTabIntentProcessor( private val sessionManager: SessionManager, private val loadUrlUseCase: SessionUseCases.DefaultLoadUrlUseCase, - private val displayMetrics: DisplayMetrics + private val resources: Resources ) : IntentProcessor { override fun matches(intent: Intent): Boolean { - val safeIntent = SafeIntent(intent) - return safeIntent.action == ACTION_VIEW && CustomTabConfig.isCustomTabIntent(safeIntent) + val safeIntent = intent.toSafeIntent() + return safeIntent.action == ACTION_VIEW && isCustomTabIntent(safeIntent) } override suspend fun process(intent: Intent): Boolean { @@ -36,7 +36,7 @@ class CustomTabIntentProcessor( return if (!url.isNullOrEmpty() && matches(intent)) { val session = Session(url, private = false, source = Session.Source.CUSTOM_TAB) - session.customTabConfig = CustomTabConfig.createFromIntent(safeIntent, displayMetrics) + session.customTabConfig = createCustomTabConfigFromIntent(intent, resources) sessionManager.add(session) loadUrlUseCase(url, session, EngineSession.LoadUrlFlags.external()) diff --git a/components/feature/customtabs/src/main/res/values/dimens.xml b/components/feature/customtabs/src/main/res/values/dimens.xml new file mode 100644 index 00000000000..7ec404f03f6 --- /dev/null +++ b/components/feature/customtabs/src/main/res/values/dimens.xml @@ -0,0 +1,7 @@ + + + + 24dp + diff --git a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt new file mode 100644 index 00000000000..b9eb1d8edb7 --- /dev/null +++ b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabConfigHelperTest.kt @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.customtabs + +import android.app.PendingIntent +import android.content.Intent +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Color +import android.os.Bundle +import androidx.browser.customtabs.CustomTabsIntent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` + +@RunWith(AndroidJUnit4::class) +class CustomTabConfigHelperTest { + + @Test + fun isCustomTabIntent() { + val customTabsIntent = CustomTabsIntent.Builder().build() + assertTrue(isCustomTabIntent(customTabsIntent.intent)) + assertFalse(isCustomTabIntent(mock())) + } + + @Test + fun createFromIntentAssignsId() { + val customTabsIntent = CustomTabsIntent.Builder().build() + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + assertTrue(customTabConfig.id.isNotBlank()) + } + + @Test + fun createFromIntentWithToolbarColor() { + val builder = CustomTabsIntent.Builder() + builder.setToolbarColor(Color.BLACK) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertEquals(Color.BLACK, customTabConfig.toolbarColor) + } + + @Test + fun createFromIntentWithCloseButton() { + val size = 24 + val builder = CustomTabsIntent.Builder() + val closeButtonIcon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888) + builder.setCloseButtonIcon(closeButtonIcon) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertEquals(closeButtonIcon, customTabConfig.closeButtonIcon) + assertEquals(size, customTabConfig.closeButtonIcon?.width) + assertEquals(size, customTabConfig.closeButtonIcon?.height) + } + + @Test + fun createFromIntentWithMaxOversizedCloseButton() { + val size = 64 + val builder = CustomTabsIntent.Builder() + val closeButtonIcon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888) + builder.setCloseButtonIcon(closeButtonIcon) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertNull(customTabConfig.closeButtonIcon) + } + + @Test + fun createFromIntentUsingDisplayMetricsForCloseButton() { + val size = 64 + val builder = CustomTabsIntent.Builder() + val resources: Resources = mock() + val closeButtonIcon = Bitmap.createBitmap(IntArray(size * size), size, size, Bitmap.Config.ARGB_8888) + builder.setCloseButtonIcon(closeButtonIcon) + + `when`(resources.getDimension(R.dimen.mozac_feature_customtabs_max_close_button_size)).thenReturn(64f) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, resources) + assertEquals(closeButtonIcon, customTabConfig.closeButtonIcon) + } + + @Test + fun createFromIntentWithInvalidCloseButton() { + val customTabsIntent = CustomTabsIntent.Builder().build() + // Intent is a parcelable but not a Bitmap + customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_CLOSE_BUTTON_ICON, Intent()) + + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + assertNull(customTabConfig.closeButtonIcon) + } + + @Test + fun createFromIntentWithUrlbarHiding() { + val builder = CustomTabsIntent.Builder() + builder.enableUrlBarHiding() + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertFalse(customTabConfig.disableUrlbarHiding) + } + + @Test + fun createFromIntentWithShareMenuItem() { + val builder = CustomTabsIntent.Builder() + builder.addDefaultShareMenuItem() + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertTrue(customTabConfig.showShareMenuItem) + } + + @Test + fun createFromIntentWithCustomizedMenu() { + val builder = CustomTabsIntent.Builder() + val pendingIntent = PendingIntent.getActivity(null, 0, null, 0) + builder.addMenuItem("menuitem1", pendingIntent) + builder.addMenuItem("menuitem2", pendingIntent) + + val customTabConfig = createCustomTabConfigFromIntent(builder.build().intent, testContext.resources) + assertEquals(2, customTabConfig.menuItems.size) + assertEquals("menuitem1", customTabConfig.menuItems[0].name) + assertSame(pendingIntent, customTabConfig.menuItems[0].pendingIntent) + assertEquals("menuitem2", customTabConfig.menuItems[1].name) + assertSame(pendingIntent, customTabConfig.menuItems[1].pendingIntent) + } + + @Test + fun createFromIntentWithActionButton() { + val builder = CustomTabsIntent.Builder() + + val bitmap = mock() + val intent = PendingIntent.getActivity(testContext, 0, Intent("testAction"), 0) + builder.setActionButton(bitmap, "desc", intent) + + val customTabsIntent = builder.build() + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertNotNull(customTabConfig.actionButtonConfig) + assertEquals("desc", customTabConfig.actionButtonConfig?.description) + assertEquals(intent, customTabConfig.actionButtonConfig?.pendingIntent) + assertEquals(bitmap, customTabConfig.actionButtonConfig?.icon) + assertFalse(customTabConfig.actionButtonConfig!!.tint) + } + + @Test + fun createFromIntentWithInvalidActionButton() { + val customTabsIntent = CustomTabsIntent.Builder().build() + + val invalid = Bundle() + customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_ACTION_BUTTON_BUNDLE, invalid) + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + + assertNull(customTabConfig.actionButtonConfig) + } + + @Test + fun createFromIntentWithInvalidExtras() { + val customTabsIntent = CustomTabsIntent.Builder().build() + + val extrasField = Intent::class.java.getDeclaredField("mExtras") + extrasField.isAccessible = true + extrasField.set(customTabsIntent.intent, null) + extrasField.isAccessible = false + + assertFalse(isCustomTabIntent(customTabsIntent.intent)) + + // Make sure we're not failing + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + assertNotNull(customTabConfig) + assertNull(customTabConfig.actionButtonConfig) + } + + @Test + fun createFromIntentWithExitAnimationOption() { + val customTabsIntent = CustomTabsIntent.Builder().build() + val bundle = Bundle() + customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_EXIT_ANIMATION_BUNDLE, bundle) + + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + assertEquals(bundle, customTabConfig.exitAnimations) + } + + @Test + fun createFromIntentWithPageTitleOption() { + val customTabsIntent = CustomTabsIntent.Builder().build() + customTabsIntent.intent.putExtra(CustomTabsIntent.EXTRA_TITLE_VISIBILITY_STATE, CustomTabsIntent.SHOW_PAGE_TITLE) + + val customTabConfig = createCustomTabConfigFromIntent(customTabsIntent.intent, testContext.resources) + assertTrue(customTabConfig.titleVisible) + } +} diff --git a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt index cd9c56064a7..f37d2741fda 100644 --- a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt +++ b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt @@ -52,7 +52,7 @@ class CustomTabIntentProcessorTest { val useCases = SessionUseCases(sessionManager) val handler = - CustomTabIntentProcessor(sessionManager, useCases.loadUrl, testContext.resources.displayMetrics) + CustomTabIntentProcessor(sessionManager, useCases.loadUrl, testContext.resources) val intent = mock() whenever(intent.action).thenReturn(Intent.ACTION_VIEW) diff --git a/components/feature/intent/src/main/java/mozilla/components/feature/intent/IntentProcessor.kt b/components/feature/intent/src/main/java/mozilla/components/feature/intent/IntentProcessor.kt index 7d7a08f74bc..505cf3a5d0d 100644 --- a/components/feature/intent/src/main/java/mozilla/components/feature/intent/IntentProcessor.kt +++ b/components/feature/intent/src/main/java/mozilla/components/feature/intent/IntentProcessor.kt @@ -29,6 +29,7 @@ typealias IntentHandler = (Intent) -> Boolean * * @deprecated Use individual intent processors instead. */ +@Deprecated("Use individual processors such as TabIntentProcessor instead.") class IntentProcessor( private val sessionUseCases: SessionUseCases, private val sessionManager: SessionManager, @@ -50,7 +51,7 @@ class IntentProcessor( val customTabIntentProcessor = CustomTabIntentProcessor( sessionManager, sessionUseCases.loadUrl, - context.resources.displayMetrics + context.resources ) val viewHandlers = listOf(customTabIntentProcessor, tabIntentProcessor) diff --git a/components/feature/intent/src/test/java/mozilla/components/feature/intent/IntentProcessorTest.kt b/components/feature/intent/src/test/java/mozilla/components/feature/intent/IntentProcessorTest.kt index 941236eb609..0fdd9df1cb4 100644 --- a/components/feature/intent/src/test/java/mozilla/components/feature/intent/IntentProcessorTest.kt +++ b/components/feature/intent/src/test/java/mozilla/components/feature/intent/IntentProcessorTest.kt @@ -35,6 +35,7 @@ import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions +@Suppress("Deprecation") @RunWith(AndroidJUnit4::class) class IntentProcessorTest { diff --git a/components/support/utils/src/main/java/mozilla/components/support/utils/SafeBundle.kt b/components/support/utils/src/main/java/mozilla/components/support/utils/SafeBundle.kt index 101f737a0a3..f7960609785 100644 --- a/components/support/utils/src/main/java/mozilla/components/support/utils/SafeBundle.kt +++ b/components/support/utils/src/main/java/mozilla/components/support/utils/SafeBundle.kt @@ -15,31 +15,32 @@ import mozilla.components.support.base.log.logger.Logger * * See bug 1090385 for more. */ -class SafeBundle(private val bundle: Bundle) { +class SafeBundle(val unsafe: Bundle) { - @SuppressWarnings("TooGenericExceptionCaught") - fun getString(name: String): String? { - return try { - bundle.getString(name) - } catch (e: OutOfMemoryError) { - Logger.warn("Couldn't get bundle items: OOM. Malformed?") - null - } catch (e: RuntimeException) { - Logger.warn("Couldn't get bundle items.", e) - null - } - } + fun getInt(name: String, defaultValue: Int = 0): Int = + safeAccess(defaultValue) { getInt(name, defaultValue) }!! + + fun getString(name: String): String? = + safeAccess { getString(name) } + + fun getParcelable(name: String): T? = + safeAccess { getParcelable(name) } @SuppressWarnings("TooGenericExceptionCaught") - fun getParcelable(name: String): T? { + private fun safeAccess(default: T? = null, block: Bundle.() -> T): T? { return try { - bundle.getParcelable(name) + block(unsafe) } catch (e: OutOfMemoryError) { Logger.warn("Couldn't get bundle items: OOM. Malformed?") - null + default } catch (e: RuntimeException) { Logger.warn("Couldn't get bundle items.", e) - null + default } } } + +/** + * Returns a [SafeBundle] for the given [Bundle]. + */ +fun Bundle.toSafeBundle() = SafeBundle(this) diff --git a/components/support/utils/src/main/java/mozilla/components/support/utils/SafeIntent.kt b/components/support/utils/src/main/java/mozilla/components/support/utils/SafeIntent.kt index cc12b67217a..015c7647386 100644 --- a/components/support/utils/src/main/java/mozilla/components/support/utils/SafeIntent.kt +++ b/components/support/utils/src/main/java/mozilla/components/support/utils/SafeIntent.kt @@ -57,12 +57,7 @@ class SafeIntent(val unsafe: Intent) { } fun getBundleExtra(name: String): SafeBundle? = safeAccess { - val bundle = unsafe.getBundleExtra(name) - if (bundle != null) { - SafeBundle(bundle) - } else { - null - } + unsafe.getBundleExtra(name)?.toSafeBundle() } fun getCharSequenceExtra(name: String): CharSequence? = safeAccess { diff --git a/components/support/utils/src/test/java/mozilla/components/support/utils/SafeBundleTest.kt b/components/support/utils/src/test/java/mozilla/components/support/utils/SafeBundleTest.kt new file mode 100644 index 00000000000..a8fdb3de8f7 --- /dev/null +++ b/components/support/utils/src/test/java/mozilla/components/support/utils/SafeBundleTest.kt @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.support.utils + +import android.os.Bundle +import android.os.Parcelable +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.support.test.mock +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.doThrow + +@RunWith(AndroidJUnit4::class) +class SafeBundleTest { + + private lateinit var bundle: Bundle + + @Before + fun setup() { + bundle = mock() + } + + @Test + fun `getInt returns default value if bundle throws OutOfMemoryError`() { + + doThrow(OutOfMemoryError::class.java).`when`(bundle).getInt(anyString(), anyInt()) + + val expected = 1 + assertEquals(expected, SafeBundle(bundle).getInt("", expected)) + } + + @Test + fun `getString returns null if bundle throws OutOfMemoryError`() { + + doThrow(OutOfMemoryError::class.java).`when`(bundle).getString(anyString()) + + assertNull(bundle.toSafeBundle().getString("")) + } + + @Test + fun `getParcelable returns null if bundle throws OutOfMemoryError`() { + + doThrow(OutOfMemoryError::class.java).`when`(bundle).getParcelable(anyString()) + + assertNull(SafeBundle(bundle).getParcelable("")) + } + + @Test + fun `getUnsafe returns original bundle`() { + + assertEquals(bundle, SafeBundle(bundle).unsafe) + } + + @Test + fun `WHEN toSafeBundle wraps an bundle THEN it has the same unsafe bundle as the SafeBundle constructor`() { + + // SafeBundle does not override .equals so we have to do comparison with their underlying unsafe bundles. + assertEquals(SafeBundle(bundle).unsafe, bundle.toSafeBundle().unsafe) + } +} diff --git a/docs/changelog.md b/docs/changelog.md index 030b732ab57..8698b3cbd29 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -53,9 +53,21 @@ permalink: /changelog/ * **browser-icons** * Added `BrowserIcons.loadIntoView` to automatically load an icon into an `ImageView`. +* **browser-session** + * Added `IntentProcessor` interface to represent a class that processes intents to create sessions. + * Deprecated `CustomTabConfig.isCustomTabIntent` and `CustomTabConfig.createFromIntent`. Use `isCustomTabIntent` and `createFromCustomTabIntent` in feature-customtabs instead. + +* **feature-customtabs** + * Added `CustomTabIntentProcessor` to create custom tab sessions from intents. + * Added `isCustomTabIntent` to check if an intent is for creating custom tabs. + * Added `createCustomTabConfigFromIntent` to create a `CustomTabConfig` from a custom tab intent. + * **feature-downloads** * `FetchDownloadManager` now determines the filename during the download, resulting in more accurate filenames. +* **feature-intent** + * Deprecated `IntentProcessor` class and moved some of its code to the new `TabIntentProcessor`. + * **feature-push** * Updated the default autopush service endpoint to `updates.push.services.mozilla.com`. diff --git a/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt b/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt index 13afffd70b2..84340709ec3 100644 --- a/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt +++ b/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt @@ -117,7 +117,7 @@ open class DefaultComponents(private val applicationContext: Context) { TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch) } val customTabIntentProcessor by lazy { - CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, applicationContext.resources.displayMetrics) + CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, applicationContext.resources) } // Menu