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 eccea58 commit 6785db9
Show file tree
Hide file tree
Showing 21 changed files with 535 additions and 53 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 @@ -198,6 +198,9 @@ internal object ContentStateReducer {
is ContentAction.SetRecordingDevices -> updateContentState(state, action.sessionId) {
it.copy(recordingDevices = action.devices)
}
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 @@ -77,6 +80,7 @@ data class ContentState(
val webAppManifest: WebAppManifest? = null,
val firstContentfulPaint: Boolean = false,
val history: HistoryState = HistoryState(),
val permissionHighlights: PermissionHighlightsState? = null,
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 permission 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 @@ -669,4 +670,17 @@ class ContentActionTest {
assertTrue(tab.content.loadRequest!!.triggeredByRedirect)
assertFalse(tab.content.loadRequest!!.triggeredByUser)
}

@Test
fun `UpdatePermissionHighlightsStateAction updates permissionHighlights state`() {

assertNull(tab.content.permissionHighlights)

store.dispatch(
ContentAction.UpdatePermissionHighlightsStateAction(tab.id, PermissionHighlightsState(true))
).joinBlocking()

assertNotNull(tab.content.permissionHighlights)
assertTrue(tab.content.permissionHighlights!!.isAutoPlayBlocking)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ class BrowserToolbar @JvmOverloads constructor(
get() = display.siteSecurity
set(value) { display.siteSecurity = value }

override var sitePermission: Toolbar.SitePermission = Toolbar.SitePermission.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
}

/**
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 permission 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 permission: 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 sitePermission 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 sitePermission: SitePermission
) {
/**
* Icons for site permission indicators.
*/
data class SitePermission(
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),
permission = 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.permission != null) {
views.permissionIndicator.setTint(value.permission)
}
}

/**
Expand All @@ -209,6 +228,10 @@ class DisplayToolbar internal constructor(
),
trackingProtectionException = requireNotNull(
getDrawable(context, TrackingProtectionIconView.DEFAULT_ICON_OFF_FOR_A_SITE)
),
sitePermission = Icons.SitePermission(
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.sitePermission)
}

/**
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)) {
setPermissionIndicator(toolbar.sitePermission)
} else {
View.GONE
}

updateSeparatorVisibility()
}

Expand Down Expand Up @@ -496,6 +549,16 @@ class DisplayToolbar internal constructor(
updateSeparatorVisibility()
}

internal fun setPermissionIndicator(state: Toolbar.SitePermission): Int {
if (!indicators.contains(Indicators.PERMISSION)) {
return views.permissionIndicator.visibility
}

views.permissionIndicator.sitePermission = 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: PermissionIconView
)
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.SitePermission
import mozilla.components.concept.toolbar.Toolbar.SitePermission.AUTOPLAY_BLOCKED
import mozilla.components.concept.toolbar.Toolbar.SitePermission.NONE

/**
* Internal widget to display the different icons of site permission.
*/
internal class PermissionIconView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

init {
visibility = GONE
}

var sitePermission: SitePermission = 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.SitePermission) {
this.iconAutoplayBlocked = icons.autoPlayBlocked

updateIcon()
}

@Synchronized
@VisibleForTesting
internal fun updateIcon() {
val update = sitePermission.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 SitePermission.toUpdate(): Update = when (this) {
AUTOPLAY_BLOCKED -> Update(
iconAutoplayBlocked,
R.string.mozac_browser_toolbar_content_description_autoplay_blocked,
true)

NONE -> Update(
null,
null,
false
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 6785db9

Please sign in to comment.