Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Closes #9131: Add site permission indicators
Browse files Browse the repository at this point in the history
in the toolbar.
  • Loading branch information
Amejia481 authored and mergify[bot] committed Dec 10, 2020
1 parent 30e3ea9 commit d8a8dd1
Show file tree
Hide file tree
Showing 21 changed files with 478 additions and 11 deletions.
Expand Up @@ -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
Expand Down Expand Up @@ -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].
*/
Expand Down
Expand Up @@ -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)
}
}
}
}
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<PermissionRequest> = emptyList(),
val appPermissionRequestsList: List<PermissionRequest> = emptyList(),
val pictureInPictureEnabled: Boolean = false,
Expand Down
@@ -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
)
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
Expand Up @@ -77,7 +77,8 @@ class DisplayToolbar internal constructor(
enum class Indicators {
SECURITY,
TRACKING_PROTECTION,
EMPTY
EMPTY,
PERMISSION_HIGHLIGHTS
}

/**
Expand All @@ -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.
Expand All @@ -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?
)

/**
Expand All @@ -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.
Expand Down Expand Up @@ -162,7 +175,8 @@ class DisplayToolbar internal constructor(
}
}
}
}
},
permissionIndicator = rootView.findViewById(R.id.mozac_browser_toolbar_permission_indicator)
)

/**
Expand All @@ -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
Expand All @@ -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)
}
}

/**
Expand All @@ -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) {
Expand All @@ -221,6 +244,7 @@ class DisplayToolbar internal constructor(
value.trackingProtectionTrackersBlocked,
value.trackingProtectionException
)
views.permissionIndicator.setIcons(value.permissionHighlights)
}

/**
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -609,6 +672,7 @@ class DisplayToolbar internal constructor(
/**
* Internal holder for view references.
*/
@Suppress("LongParameterList")
internal class DisplayToolbarViews(
val browserActions: ActionContainer,
val pageActions: ActionContainer,
Expand All @@ -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
)
@@ -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
)
}
}

0 comments on commit d8a8dd1

Please sign in to comment.