From ca7e21673ca37df41493fc7322300a55ab08e049 Mon Sep 17 00:00:00 2001 From: Arturo Mejia Date: Mon, 7 Dec 2020 22:55:39 -0500 Subject: [PATCH] Closes #9131: Add site permission indicators in the toolbar. --- .../browser/state/action/BrowserAction.kt | 9 ++ .../state/reducer/ContentStateReducer.kt | 3 + .../browser/state/state/ContentState.kt | 6 +- .../content/PermissionHighlightsState.kt | 16 ++++ .../browser/state/action/ContentActionTest.kt | 13 +++ .../browser/toolbar/BrowserToolbar.kt | 9 ++ .../browser/toolbar/display/DisplayToolbar.kt | 79 ++++++++++++++-- .../display/PermissionHighlightsIconView.kt | 91 +++++++++++++++++++ .../display/TrackingProtectionIconView.kt | 2 +- .../mozac_browser_toolbar_displaytoolbar.xml | 14 ++- .../toolbar/src/main/res/values/strings.xml | 2 + .../toolbar/display/DisplayToolbarTest.kt | 73 +++++++++++++++ .../PermissionHighlightsIconViewTest.kt | 66 ++++++++++++++ .../components/concept/toolbar/Toolbar.kt | 19 ++++ .../CustomTabSessionTitleObserverTest.kt | 2 +- .../feature/toolbar/ToolbarPresenter.kt | 12 +++ .../toolbar/ToolbarAutocompleteFeatureTest.kt | 1 + .../feature/toolbar/ToolbarInteractorTest.kt | 1 + .../feature/toolbar/ToolbarPresenterTest.kt | 50 ++++++++++ .../drawable/mozac_ic_autoplay_blocked.xml | 19 ++++ docs/changelog.md | 2 + 21 files changed, 478 insertions(+), 11 deletions(-) create mode 100644 components/browser/state/src/main/java/mozilla/components/browser/state/state/content/PermissionHighlightsState.kt create mode 100644 components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/PermissionHighlightsIconView.kt create mode 100644 components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/PermissionHighlightsIconViewTest.kt create mode 100644 components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_blocked.xml diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt index 3c05d15294c..7de73e78f41 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt @@ -18,6 +18,7 @@ import mozilla.components.browser.state.state.EngineState import mozilla.components.browser.state.state.LoadRequestState import mozilla.components.browser.state.state.MediaSessionState import mozilla.components.browser.state.state.MediaState +import mozilla.components.browser.state.state.content.PermissionHighlightsState import mozilla.components.browser.state.state.ReaderState import mozilla.components.browser.state.state.SecurityInfoState import mozilla.components.browser.state.state.SessionState @@ -277,6 +278,14 @@ sealed class ContentAction : BrowserAction() { */ data class UpdateProgressAction(val sessionId: String, val progress: Int) : ContentAction() + /** + * Updates permissions highlights of the [ContentState] with the given [sessionId]. + */ + data class UpdatePermissionHighlightsStateAction( + val sessionId: String, + val highlights: PermissionHighlightsState + ) : ContentAction() + /** * Updates the title of the [ContentState] with the given [sessionId]. */ diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt index f8c964cc976..34120c479e1 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt @@ -201,6 +201,9 @@ internal object ContentStateReducer { is ContentAction.UpdateDesktopModeAction -> updateContentState(state, action.sessionId) { it.copy(desktopMode = action.enabled) } + is ContentAction.UpdatePermissionHighlightsStateAction -> updateContentState(state, action.sessionId) { + it.copy(permissionHighlights = action.highlights) + } } } } diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt index 4fb8a17ebf0..8a4bb8515cf 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt @@ -8,6 +8,7 @@ import android.graphics.Bitmap import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.FindResultState import mozilla.components.browser.state.state.content.HistoryState +import mozilla.components.browser.state.state.content.PermissionHighlightsState import mozilla.components.concept.engine.HitResult import mozilla.components.concept.engine.manifest.WebAppManifest import mozilla.components.concept.engine.media.RecordingDevice @@ -45,7 +46,9 @@ import mozilla.components.concept.engine.window.WindowRequest * @property firstContentfulPaint whether or not the first contentful paint has happened. * @property pictureInPictureEnabled True if the session is being displayed in PIP mode. * @property loadRequest last [LoadRequestState] if this session. - * @property permissionRequestsList Holds unprocessed content requests. + * @property permissionIndicator Holds the state of any site permission that was granted/denied + * that should be brought to the user's attention, for example when media content is not able to + * play because the autoplay settings. * @property appPermissionRequestsList Holds unprocessed app requests. * @property refreshCanceled Indicates if an intent of refreshing was canceled. * True if a page refresh was cancelled by the user, defaults to false. Note that this is not about @@ -78,6 +81,7 @@ data class ContentState( val webAppManifest: WebAppManifest? = null, val firstContentfulPaint: Boolean = false, val history: HistoryState = HistoryState(), + val permissionHighlights: PermissionHighlightsState = PermissionHighlightsState(), val permissionRequestsList: List = emptyList(), val appPermissionRequestsList: List = emptyList(), val pictureInPictureEnabled: Boolean = false, diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/PermissionHighlightsState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/PermissionHighlightsState.kt new file mode 100644 index 00000000000..981baadfa20 --- /dev/null +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/PermissionHighlightsState.kt @@ -0,0 +1,16 @@ +/* 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.browser.state.state.content + +/** + * Value type that represents any information about permissions that should + * be brought to user's attention. + * + * @property isAutoPlayBlocking indicates if the autoplay setting + * disabled some web content from playing. + */ +data class PermissionHighlightsState( + val isAutoPlayBlocking: Boolean = false +) diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt index 0c72ecde31f..e39789f81ff 100644 --- a/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt +++ b/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt @@ -14,6 +14,7 @@ import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.FindResultState import mozilla.components.browser.state.state.content.HistoryState +import mozilla.components.browser.state.state.content.PermissionHighlightsState import mozilla.components.browser.state.state.createCustomTab import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore @@ -685,4 +686,16 @@ class ContentActionTest { assertFalse(tab.content.desktopMode) assertFalse(otherTab.content.desktopMode) } + + @Test + fun `UpdatePermissionHighlightsStateAction updates permissionHighlights state`() { + + assertFalse(tab.content.permissionHighlights.isAutoPlayBlocking) + + store.dispatch( + ContentAction.UpdatePermissionHighlightsStateAction(tab.id, PermissionHighlightsState(true)) + ).joinBlocking() + + assertTrue(tab.content.permissionHighlights.isAutoPlayBlocking) + } } diff --git a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt index 3a7dc1e2aad..092ecd2a609 100644 --- a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt +++ b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt @@ -25,6 +25,7 @@ import mozilla.components.browser.toolbar.edit.EditToolbar import mozilla.components.concept.toolbar.AutocompleteDelegate import mozilla.components.concept.toolbar.AutocompleteResult import mozilla.components.concept.toolbar.Toolbar +import mozilla.components.concept.toolbar.Toolbar.PermissionHighlights import mozilla.components.support.base.android.Padding import mozilla.components.support.base.log.logger.Logger import mozilla.components.ui.autocomplete.AutocompleteView @@ -111,6 +112,14 @@ class BrowserToolbar @JvmOverloads constructor( get() = display.siteSecurity set(value) { display.siteSecurity = value } + override var permissionHighlights: PermissionHighlights = PermissionHighlights.NONE + set(value) { + if (field != value) { + display.setPermissionIndicator(value) + field = value + } + } + override var siteTrackingProtection: Toolbar.SiteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY set(value) { diff --git a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt index fc860614bea..fb28749a59a 100644 --- a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt +++ b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/DisplayToolbar.kt @@ -77,7 +77,8 @@ class DisplayToolbar internal constructor( enum class Indicators { SECURITY, TRACKING_PROTECTION, - EMPTY + EMPTY, + PERMISSION_HIGHLIGHTS } /** @@ -92,6 +93,7 @@ class DisplayToolbar internal constructor( * @property text Text color of the URL. * @property trackingProtection Color tint for the tracking protection icons. * @property separator Color tint for the separator shown between indicators. + * @property permissionHighlights Color tint for the permission indicator. * * Set/Get the site security icon colours. It uses a pair of color integers which represent the * insecure and secure colours respectively. @@ -105,7 +107,8 @@ class DisplayToolbar internal constructor( @ColorInt val title: Int, @ColorInt val text: Int, @ColorInt val trackingProtection: Int?, - @ColorInt val separator: Int + @ColorInt val separator: Int, + @ColorInt val permissionHighlights: Int? ) /** @@ -118,13 +121,23 @@ class DisplayToolbar internal constructor( * enabled and no trackers have been blocked. * @property trackingProtectionException An icon that is shown if tracking protection is enabled * but the current page is in the "exception list". + * @property permissionHighlights An icon that is shown if any site permission needs to be brought + * to the user's attention. */ data class Icons( val emptyIcon: Drawable?, val trackingProtectionTrackersBlocked: Drawable, val trackingProtectionNothingBlocked: Drawable, - val trackingProtectionException: Drawable - ) + val trackingProtectionException: Drawable, + val permissionHighlights: PermissionHighlights + ) { + /** + * Icons for site permission indicators. + */ + data class PermissionHighlights( + val autoPlayBlocked: Drawable + ) + } /** * Gravity enum for positioning the progress bar. @@ -162,7 +175,8 @@ class DisplayToolbar internal constructor( } } } - } + }, + permissionIndicator = rootView.findViewById(R.id.mozac_browser_toolbar_permission_indicator) ) /** @@ -177,7 +191,8 @@ class DisplayToolbar internal constructor( title = views.origin.titleColor, text = views.origin.textColor, trackingProtection = null, - separator = ContextCompat.getColor(context, R.color.photonGrey80) + separator = ContextCompat.getColor(context, R.color.photonGrey80), + permissionHighlights = null ) set(value) { field = value @@ -194,6 +209,10 @@ class DisplayToolbar internal constructor( views.trackingProtectionIndicator.setTint(value.trackingProtection) views.trackingProtectionIndicator.setColorFilter(value.trackingProtection) } + + if (value.permissionHighlights != null) { + views.permissionIndicator.setTint(value.permissionHighlights) + } } /** @@ -209,6 +228,10 @@ class DisplayToolbar internal constructor( ), trackingProtectionException = requireNotNull( getDrawable(context, TrackingProtectionIconView.DEFAULT_ICON_OFF_FOR_A_SITE) + ), + permissionHighlights = Icons.PermissionHighlights( + autoPlayBlocked = + requireNotNull(getDrawable(context, R.drawable.mozac_ic_autoplay_blocked)) ) ) set(value) { @@ -221,6 +244,7 @@ class DisplayToolbar internal constructor( value.trackingProtectionTrackersBlocked, value.trackingProtectionException ) + views.permissionIndicator.setIcons(value.permissionHighlights) } /** @@ -274,6 +298,29 @@ class DisplayToolbar internal constructor( } } + /** + * Sets a listener to be invoked when the site permission indicator icon is clicked. + */ + fun setOnPermissionIndicatorClickedListener(listener: (() -> Unit)?) { + if (listener == null) { + views.permissionIndicator.setOnClickListener(null) + views.permissionIndicator.background = null + } else { + views.permissionIndicator.setOnClickListener { + listener.invoke() + } + + val outValue = TypedValue() + context.theme.resolveAttribute( + android.R.attr.selectableItemBackgroundBorderless, + outValue, + true + ) + + views.permissionIndicator.setBackgroundResource(outValue.resourceId) + } + } + /** * Sets a lambda to be invoked when the menu is dismissed */ @@ -423,6 +470,12 @@ class DisplayToolbar internal constructor( View.GONE } + views.permissionIndicator.visibility = if (!urlEmpty && indicators.contains(Indicators.PERMISSION_HIGHLIGHTS)) { + setPermissionIndicator(toolbar.permissionHighlights) + } else { + View.GONE + } + updateSeparatorVisibility() } @@ -496,6 +549,16 @@ class DisplayToolbar internal constructor( updateSeparatorVisibility() } + internal fun setPermissionIndicator(state: Toolbar.PermissionHighlights): Int { + if (!indicators.contains(Indicators.PERMISSION_HIGHLIGHTS)) { + return views.permissionIndicator.visibility + } + + views.permissionIndicator.permissionHighlights = state + + return views.permissionIndicator.visibility + } + internal fun onStop() { views.menu.dismissMenu() } @@ -609,6 +672,7 @@ class DisplayToolbar internal constructor( /** * Internal holder for view references. */ +@Suppress("LongParameterList") internal class DisplayToolbarViews( val browserActions: ActionContainer, val pageActions: ActionContainer, @@ -620,5 +684,6 @@ internal class DisplayToolbarViews( val securityIndicator: SiteSecurityIconView, val trackingProtectionIndicator: TrackingProtectionIconView, val origin: OriginView, - val progress: ProgressBar + val progress: ProgressBar, + val permissionIndicator: PermissionHighlightsIconView ) diff --git a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/PermissionHighlightsIconView.kt b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/PermissionHighlightsIconView.kt new file mode 100644 index 00000000000..89a6cc899fa --- /dev/null +++ b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/PermissionHighlightsIconView.kt @@ -0,0 +1,91 @@ +/* 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.browser.toolbar.display + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.annotation.VisibleForTesting +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.view.isVisible +import mozilla.components.browser.toolbar.R +import mozilla.components.concept.toolbar.Toolbar.PermissionHighlights +import mozilla.components.concept.toolbar.Toolbar.PermissionHighlights.AUTOPLAY_BLOCKED +import mozilla.components.concept.toolbar.Toolbar.PermissionHighlights.NONE + +/** + * Internal widget to display the different icons of site permission. + */ +internal class PermissionHighlightsIconView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AppCompatImageView(context, attrs, defStyleAttr) { + + init { + visibility = GONE + } + + var permissionHighlights: PermissionHighlights = NONE + set(value) { + if (value != field) { + field = value + updateIcon() + } + } + + @VisibleForTesting + internal var permissionTint: Int? = null + + private var iconAutoplayBlocked: Drawable = + requireNotNull(AppCompatResources.getDrawable(context, DEFAULT_ICON_AUTOPLAY_BLOCKED)) + + fun setTint(tint: Int) { + permissionTint = tint + setColorFilter(tint) + } + + fun setIcons(icons: DisplayToolbar.Icons.PermissionHighlights) { + this.iconAutoplayBlocked = icons.autoPlayBlocked + + updateIcon() + } + + @Synchronized + @VisibleForTesting + internal fun updateIcon() { + val update = permissionHighlights.toUpdate() + + isVisible = update.visible + + contentDescription = if (update.contentDescription != null) { + context.getString(update.contentDescription) + } else { + null + } + + permissionTint?.let { setColorFilter(it) } + setImageDrawable(update.drawable) + } + + companion object { + val DEFAULT_ICON_AUTOPLAY_BLOCKED = + R.drawable.mozac_ic_autoplay_blocked + } + + private fun PermissionHighlights.toUpdate(): Update = when (this) { + AUTOPLAY_BLOCKED -> Update( + iconAutoplayBlocked, + R.string.mozac_browser_toolbar_content_description_autoplay_blocked, + true) + + NONE -> Update( + null, + null, + false + ) + } +} diff --git a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconView.kt b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconView.kt index b5452f2ce65..0c248b1e7f6 100644 --- a/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconView.kt +++ b/components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/display/TrackingProtectionIconView.kt @@ -126,7 +126,7 @@ internal class TrackingProtectionIconView @JvmOverloads constructor( } } -private class Update( +internal class Update( val drawable: Drawable?, @StringRes val contentDescription: Int?, val visible: Boolean diff --git a/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_displaytoolbar.xml b/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_displaytoolbar.xml index 9a54110e9d6..b438c2c7832 100644 --- a/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_displaytoolbar.xml +++ b/components/browser/toolbar/src/main/res/layout/mozac_browser_toolbar_displaytoolbar.xml @@ -83,6 +83,18 @@ app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/mozac_ic_site_security" /> + + diff --git a/components/browser/toolbar/src/main/res/values/strings.xml b/components/browser/toolbar/src/main/res/values/strings.xml index 49d85a5fcfb..3c2d7dcf1b9 100644 --- a/components/browser/toolbar/src/main/res/values/strings.xml +++ b/components/browser/toolbar/src/main/res/values/strings.xml @@ -16,4 +16,6 @@ Site information Loading + + Some content has been blocked by the autoplay setting diff --git a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/DisplayToolbarTest.kt b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/DisplayToolbarTest.kt index bd5876d2cb3..44b53c9762c 100644 --- a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/DisplayToolbarTest.kt +++ b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/DisplayToolbarTest.kt @@ -16,6 +16,7 @@ import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.R import mozilla.components.concept.menu.MenuButton +import mozilla.components.concept.toolbar.Toolbar import mozilla.components.concept.toolbar.Toolbar.SiteSecurity import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection import mozilla.components.support.base.Component @@ -95,6 +96,18 @@ class DisplayToolbarTest { assertNotNull(displayToolbar.views.trackingProtectionIndicator.trackingProtectionTint) } + @Test + fun `permissionViewColor will change the color of the permissionIconView`() { + val (_, displayToolbar) = createDisplayToolbar() + + assertNull(displayToolbar.views.permissionIndicator.colorFilter) + + displayToolbar.colors = displayToolbar.colors.copy(permissionHighlights = Color.BLUE) + + assertNotNull(displayToolbar.views.permissionIndicator.colorFilter) + assertNotNull(displayToolbar.views.permissionIndicator.permissionTint) + } + @Test fun `tracking protection and separator views become visible when states ON OR ACTIVE are set to siteTrackingProtection`() { val (_, displayToolbar) = createDisplayToolbar() @@ -168,6 +181,33 @@ class DisplayToolbarTest { ) } + @Test + fun `setPermissionIcons will forward to PermissionHighlightsIconView`() { + val (_, displayToolbar) = createDisplayToolbar() + + val oldPermissionIcon = displayToolbar.views.permissionIndicator.drawable + assertNotNull(oldPermissionIcon) + + val drawable1 = testContext.getDrawable(PermissionHighlightsIconView.DEFAULT_ICON_AUTOPLAY_BLOCKED)!! + + displayToolbar.indicators = listOf(DisplayToolbar.Indicators.PERMISSION_HIGHLIGHTS) + displayToolbar.icons = displayToolbar.icons.copy( + permissionHighlights = DisplayToolbar.Icons.PermissionHighlights(drawable1) + ) + + assertNotEquals( + oldPermissionIcon, + displayToolbar.views.permissionIndicator.drawable + ) + + displayToolbar.setPermissionIndicator(Toolbar.PermissionHighlights.AUTOPLAY_BLOCKED) + + assertNotEquals( + oldPermissionIcon, + displayToolbar.views.permissionIndicator.drawable + ) + } + @Test fun `menu view is gone by default`() { val (_, displayToolbar) = createDisplayToolbar() @@ -664,6 +704,39 @@ class DisplayToolbarTest { assertNull(displayToolbar.views.securityIndicator.background) } + @Test + fun `clicking on permission indicator invokes listener`() { + var listenerInvoked = false + + val (_, displayToolbar) = createDisplayToolbar() + + assertNull(displayToolbar.views.permissionIndicator.background) + + displayToolbar.setOnPermissionIndicatorClickedListener { + listenerInvoked = true + } + + assertNotNull(displayToolbar.views.permissionIndicator.background) + + displayToolbar.views.permissionIndicator.performClick() + + assertTrue(listenerInvoked) + + listenerInvoked = false + + displayToolbar.setOnPermissionIndicatorClickedListener { } + + assertNotNull(displayToolbar.views.permissionIndicator.background) + + displayToolbar.views.permissionIndicator.performClick() + + assertFalse(listenerInvoked) + + displayToolbar.setOnPermissionIndicatorClickedListener(null) + + assertNull(displayToolbar.views.permissionIndicator.background) + } + @Test fun `Security icon has proper content description`() { val (_, displayToolbar) = createDisplayToolbar() diff --git a/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/PermissionHighlightsIconViewTest.kt b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/PermissionHighlightsIconViewTest.kt new file mode 100644 index 00000000000..50aab0a1a48 --- /dev/null +++ b/components/browser/toolbar/src/test/java/mozilla/components/browser/toolbar/display/PermissionHighlightsIconViewTest.kt @@ -0,0 +1,66 @@ +/* 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.browser.toolbar.display + +import androidx.core.view.isVisible +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.browser.toolbar.R +import mozilla.components.concept.toolbar.Toolbar.PermissionHighlights.NONE +import mozilla.components.concept.toolbar.Toolbar.PermissionHighlights.AUTOPLAY_BLOCKED +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.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class PermissionHighlightsIconViewTest { + + @Test + fun `after setting tint, can get trackingProtectionTint`() { + val view = PermissionHighlightsIconView(testContext) + view.setTint(android.R.color.black) + assertEquals(android.R.color.black, view.permissionTint) + } + + @Test + fun `setting permissionHighlights status will trigger an icon updated`() { + val view = PermissionHighlightsIconView(testContext) + + view.permissionHighlights = AUTOPLAY_BLOCKED + + assertEquals(AUTOPLAY_BLOCKED, view.permissionHighlights) + assertTrue(view.isVisible) + assertNotNull(view.drawable) + assertEquals( + view.contentDescription, + testContext.getString(R.string.mozac_browser_toolbar_content_description_autoplay_blocked) + ) + + view.permissionHighlights = NONE + + assertEquals(NONE, view.permissionHighlights) + assertNull(view.drawable) + assertFalse(view.isVisible) + assertNull(view.contentDescription) + } + + @Test + fun `setIcons will trigger an icon updated`() { + val view = spy(PermissionHighlightsIconView(testContext)) + + view.setIcons(DisplayToolbar.Icons.PermissionHighlights( + testContext.getDrawable( + TrackingProtectionIconView.DEFAULT_ICON_ON_NO_TRACKERS_BLOCKED + )!!)) + + verify(view).updateIcon() + } +} diff --git a/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt b/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt index c71ed3bc4a0..5cef141a280 100644 --- a/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt +++ b/components/concept/toolbar/src/main/java/mozilla/components/concept/toolbar/Toolbar.kt @@ -47,6 +47,11 @@ interface Toolbar { */ var siteSecure: SiteSecurity + /** + * Sets/Gets the site site permission indicator to be displayed on the toolbar. + */ + var permissionHighlights: PermissionHighlights + /** * Sets/Gets the site tracking protection state to be displayed on the toolbar. */ @@ -408,4 +413,18 @@ interface Toolbar { */ OFF_GLOBALLY, } + + /** + * Indicates which site permission indicator a site should show. + */ + enum class PermissionHighlights { + /** + * The site has autoplay blocked. + */ + AUTOPLAY_BLOCKED, + /** + * The site does not have any permission indicator to show. + */ + NONE + } } diff --git a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt index 85656e5cd14..83eaccb3f39 100644 --- a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt +++ b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/feature/CustomTabSessionTitleObserverTest.kt @@ -63,7 +63,7 @@ class CustomTabSessionTitleObserverTest { private class MockToolbar : Toolbar { override var title: String = "" - + override var permissionHighlights: Toolbar.PermissionHighlights = Toolbar.PermissionHighlights.NONE override var url: CharSequence by ThrowProperty() override var private: Boolean by ThrowProperty() override var siteSecure: Toolbar.SiteSecurity by ThrowProperty() diff --git a/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarPresenter.kt b/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarPresenter.kt index 46aaebba24f..779b1f8e736 100644 --- a/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarPresenter.kt +++ b/components/feature/toolbar/src/main/java/mozilla/components/feature/toolbar/ToolbarPresenter.kt @@ -11,8 +11,10 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.collect import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.toolbar.Toolbar +import mozilla.components.concept.toolbar.Toolbar.PermissionHighlights import mozilla.components.concept.toolbar.Toolbar.SiteTrackingProtection import mozilla.components.feature.toolbar.internal.URLRenderer import mozilla.components.lib.state.ext.flowScoped @@ -78,11 +80,20 @@ class ToolbarPresenter( else -> SiteTrackingProtection.OFF_GLOBALLY } + + updatePermissionIndicator(tab) } else { clear() } } + private fun updatePermissionIndicator(tab: SessionState) { + toolbar.permissionHighlights = when { + tab.content.permissionHighlights.isAutoPlayBlocking -> PermissionHighlights.AUTOPLAY_BLOCKED + else -> PermissionHighlights.NONE + } + } + @VisibleForTesting(otherwise = PRIVATE) internal fun clear() { renderer.post("") @@ -93,5 +104,6 @@ class ToolbarPresenter( toolbar.siteSecure = Toolbar.SiteSecurity.INSECURE toolbar.siteTrackingProtection = SiteTrackingProtection.OFF_GLOBALLY + toolbar.permissionHighlights = PermissionHighlights.NONE } } diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt index efeb3f2a8ed..0fc8c5f9f15 100644 --- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt +++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarAutocompleteFeatureTest.kt @@ -35,6 +35,7 @@ import org.mockito.Mockito.verify class ToolbarAutocompleteFeatureTest { class TestToolbar : Toolbar { + override var permissionHighlights: Toolbar.PermissionHighlights = Toolbar.PermissionHighlights.NONE override var siteTrackingProtection: Toolbar.SiteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY override var title: String = "" diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt index a36a1d3a88c..7e3c96dde64 100644 --- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt +++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarInteractorTest.kt @@ -19,6 +19,7 @@ import org.mockito.Mockito.spy class ToolbarInteractorTest { class TestToolbar : Toolbar { + override var permissionHighlights: Toolbar.PermissionHighlights = Toolbar.PermissionHighlights.NONE override var url: CharSequence = "" override var siteSecure: Toolbar.SiteSecurity = Toolbar.SiteSecurity.INSECURE override var private: Boolean = false diff --git a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt index 14cda6fd889..066bbde800f 100644 --- a/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt +++ b/components/feature/toolbar/src/test/java/mozilla/components/feature/toolbar/ToolbarPresenterTest.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.ContentAction.UpdatePermissionHighlightsStateAction import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.action.TrackingProtectionAction import mozilla.components.browser.state.state.BrowserState @@ -17,6 +18,7 @@ import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.SecurityInfoState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TrackingProtectionState +import mozilla.components.browser.state.state.content.PermissionHighlightsState import mozilla.components.browser.state.state.createCustomTab import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore @@ -30,6 +32,7 @@ import org.junit.Before import org.junit.Test import org.mockito.Mockito.never import org.mockito.Mockito.spy +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions @@ -161,6 +164,7 @@ class ToolbarPresenterTest { verify(toolbar).displayProgress(60) verify(toolbar).siteSecure = Toolbar.SiteSecurity.SECURE verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY + verify(toolbar).permissionHighlights = Toolbar.PermissionHighlights.NONE verifyNoMoreInteractions(toolbarPresenter.renderer) verifyNoMoreInteractions(toolbar) @@ -275,6 +279,7 @@ class ToolbarPresenterTest { verify(toolbar).displayProgress(60) verify(toolbar).siteSecure = Toolbar.SiteSecurity.SECURE verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY + verify(toolbar).permissionHighlights = Toolbar.PermissionHighlights.NONE verifyNoMoreInteractions(toolbarPresenter.renderer) verifyNoMoreInteractions(toolbar) } @@ -300,6 +305,7 @@ class ToolbarPresenterTest { url = "https://www.example.org", securityInfo = SecurityInfoState(false, "example.org", "Example"), searchTerms = "Example", + permissionHighlights = PermissionHighlightsState(true), progress = 90 ), trackingProtection = TrackingProtectionState(enabled = true) @@ -320,6 +326,7 @@ class ToolbarPresenterTest { verify(toolbar).displayProgress(60) verify(toolbar).siteSecure = Toolbar.SiteSecurity.SECURE verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY + verify(toolbar).permissionHighlights = Toolbar.PermissionHighlights.NONE verifyNoMoreInteractions(toolbarPresenter.renderer) verifyNoMoreInteractions(toolbar) @@ -332,6 +339,7 @@ class ToolbarPresenterTest { verify(toolbar).displayProgress(90) verify(toolbar).siteSecure = Toolbar.SiteSecurity.INSECURE verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.ON_NO_TRACKERS_BLOCKED + verify(toolbar).permissionHighlights = Toolbar.PermissionHighlights.AUTOPLAY_BLOCKED verifyNoMoreInteractions(toolbarPresenter.renderer) verifyNoMoreInteractions(toolbar) } @@ -385,6 +393,46 @@ class ToolbarPresenterTest { verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_FOR_A_SITE } + @Test + fun `displaying different permissions highlights states`() { + val toolbar: Toolbar = mock() + val store = spy(BrowserStore( + BrowserState( + tabs = listOf( + TabSessionState( + id = "tab", + content = ContentState( + url = "https://www.mozilla.org", + securityInfo = SecurityInfoState(true, "mozilla.org", "Mozilla"), + searchTerms = "Hello World", + progress = 60 + ) + )), + selectedTabId = "tab") + )) + + val toolbarPresenter = spy(ToolbarPresenter(toolbar, store)) + toolbarPresenter.renderer = mock() + + toolbarPresenter.start() + + testDispatcher.advanceUntilIdle() + + verify(toolbar).permissionHighlights = Toolbar.PermissionHighlights.NONE + + store.dispatch(UpdatePermissionHighlightsStateAction("tab", PermissionHighlightsState(true))).joinBlocking() + + testDispatcher.advanceUntilIdle() + + verify(toolbar).permissionHighlights = Toolbar.PermissionHighlights.AUTOPLAY_BLOCKED + + store.dispatch(UpdatePermissionHighlightsStateAction("tab", PermissionHighlightsState())).joinBlocking() + + testDispatcher.advanceUntilIdle() + + verify(toolbar, times(2)).permissionHighlights = Toolbar.PermissionHighlights.NONE + } + @Test fun `Stopping presenter stops renderer`() { val store = BrowserStore() @@ -417,5 +465,7 @@ class ToolbarPresenterTest { verify(toolbar).setSearchTerms("") verify(toolbar).displayProgress(0) verify(toolbar).siteSecure = Toolbar.SiteSecurity.INSECURE + verify(toolbar).siteTrackingProtection = Toolbar.SiteTrackingProtection.OFF_GLOBALLY + verify(toolbar).permissionHighlights = Toolbar.PermissionHighlights.NONE } } diff --git a/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_blocked.xml b/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_blocked.xml new file mode 100644 index 00000000000..69eacae98ea --- /dev/null +++ b/components/ui/icons/src/main/res/drawable/mozac_ic_autoplay_blocked.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/docs/changelog.md b/docs/changelog.md index 1d899086976..5d0cc400ddf 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,8 @@ permalink: /changelog/ * [Gecko](https://github.com/mozilla-mobile/android-components/blob/master/buildSrc/src/main/java/Gecko.kt) * [Configuration](https://github.com/mozilla-mobile/android-components/blob/master/.config.yml) +* **browser-toolbar** + * 🌟 Added API to show site permission indicators for more information see [#9131](https://github.com/mozilla-mobile/android-components/issues/9131). # 69.0.0