Skip to content

Commit

Permalink
Closes mozilla-mobile#9131: Add site permission indicators
Browse files Browse the repository at this point in the history
in the toolbar.
  • Loading branch information
Amejia481 committed Dec 8, 2020
1 parent 50845c0 commit fb8a8b7
Show file tree
Hide file tree
Showing 21 changed files with 479 additions and 11 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
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)
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
)
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Loading

0 comments on commit fb8a8b7

Please sign in to comment.