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

Commit

Permalink
Closes #2315: Adding support for site configurations permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
Amejia481 committed Mar 20, 2019
1 parent fcc85b1 commit d3068f3
Show file tree
Hide file tree
Showing 4 changed files with 338 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ typealias OnNeedToRequestPermissions = (permissions: Array<String>) -> Unit
* @property sessionManager the [SessionManager] instance in order to subscribe
* to the selected [Session].
* @property storage the object in charge of persisting all the [SitePermissions] objects.
* @property sitePermissionsRules indicates how permissions should behave per permission category.
* @property onNeedToRequestPermissions a callback invoked when permissions
* need to be requested. Once the request is completed, [onPermissionsResult] needs to be invoked.
**/
Expand All @@ -63,6 +64,7 @@ class SitePermissionsFeature(
private val anchorView: View,
private val sessionManager: SessionManager,
private val storage: SitePermissionsStorage = SitePermissionsStorage(anchorView.context),
var sitePermissionsRules: SitePermissionsRules? = null,
private val onNeedToRequestPermissions: OnNeedToRequestPermissions
) : LifecycleAwareFeature {

Expand Down Expand Up @@ -166,9 +168,27 @@ class SitePermissionsFeature(

internal suspend fun onContentPermissionRequested(
session: Session,
permissionRequest: PermissionRequest
request: PermissionRequest
): DoorhangerPrompt? {

return if (shouldApplyRules(request.host)) {
handleRuledFlow(request, session)
} else {
handleNoRuledFlow(request, session)
}
}

@VisibleForTesting
internal fun findDoNotAskAgainCheckBox(controls: List<Control>?): CheckBox? {
return controls?.find {
(it is CheckBox)
} as CheckBox?
}

private suspend fun handleNoRuledFlow(
permissionRequest: PermissionRequest,
session: Session
): DoorhangerPrompt? {
val permissionFromStorage = withContext(ioCoroutineScope.coroutineContext) {
storage.findSitePermissionsBy(permissionRequest.host)
}
Expand All @@ -181,17 +201,11 @@ class SitePermissionsFeature(
} else {
permissionRequest.reject()
}
session.contentPermissionRequest.consume { true }
null
}
}

@VisibleForTesting
internal fun findDoNotAskAgainCheckBox(controls: List<Control>?): CheckBox? {
return controls?.find {
(it is CheckBox)
} as CheckBox?
}

private fun shouldShowPrompt(
permissionRequest: PermissionRequest,
permissionFromStorage: SitePermissions?
Expand All @@ -201,6 +215,23 @@ class SitePermissionsFeature(
!permissionRequest.doNotAskAgain(permissionFromStorage))
}

private fun handleRuledFlow(permissionRequest: PermissionRequest, session: Session): DoorhangerPrompt? {
val action = requireNotNull(sitePermissionsRules).getActionFrom(permissionRequest)
return when (action) {
SitePermissionsRules.Action.BLOCKED -> {
permissionRequest.reject()
session.contentPermissionRequest.consume { true }
null
}
SitePermissionsRules.Action.ASK_TO_ALLOW -> {
createPrompt(permissionRequest, session)
}
}
}

private fun shouldApplyRules(host: String) =
sitePermissionsRules != null && !requireNotNull(sitePermissionsRules).isHostInExceptions(host)

private fun PermissionRequest.doNotAskAgain(permissionFromStore: SitePermissions): Boolean {
return permissions.any { permission ->
when (permission) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* 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.sitepermissions

import android.net.Uri
import mozilla.components.concept.engine.permission.Permission
import mozilla.components.concept.engine.permission.PermissionRequest
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ASK_TO_ALLOW
import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.BLOCKED

/**
* Indicate how site permissions must behave by permission category.
*/
data class SitePermissionsRules(
val camera: Action,
val location: Action,
val notification: Action,
val microphone: Action,
val exceptions: List<Uri>? = null
) {
enum class Action {
BLOCKED, ASK_TO_ALLOW;
}

internal fun getActionFrom(request: PermissionRequest): Action {
return if (request.containsVideoAndAudioSources()) {
getActionForCombinedPermission()
} else {
getActionForSinglePermission(request.permissions.first())
}
}

internal fun isHostInExceptions(host: String): Boolean {
if (exceptions == null || exceptions.isEmpty()) {
return false
}

return exceptions.any {
it.host == host
}
}

private fun getActionForSinglePermission(permission: Permission): Action {
return when (permission) {
is Permission.ContentGeoLocation -> {
location
}
is Permission.ContentNotification -> {
notification
}
is Permission.ContentAudioCapture, is Permission.ContentAudioMicrophone -> {
microphone
}
is Permission.ContentVideoCamera, is Permission.ContentVideoCapture -> {
camera
}
else -> ASK_TO_ALLOW
}
}

private fun getActionForCombinedPermission(): Action {
return if (camera == BLOCKED || microphone == BLOCKED) {
BLOCKED
} else {
ASK_TO_ALLOW
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,69 @@ class SitePermissionsFeatureTest {
}
}

@Test
fun `onContentPermissionRequested with rules must behave according to the rules object`() {
val permissions = listOf(
ContentGeoLocation(),
ContentNotification(),
ContentAudioCapture(),
ContentAudioMicrophone()
)

val rules = SitePermissionsRules(
location = SitePermissionsRules.Action.BLOCKED,
camera = SitePermissionsRules.Action.ASK_TO_ALLOW,
notification = SitePermissionsRules.Action.ASK_TO_ALLOW,
microphone = SitePermissionsRules.Action.BLOCKED
)

sitePermissionFeature.sitePermissionsRules = rules

permissions.forEach { permission ->
val session = getSelectedSession()
var grantWasCalled = false
var rejectWasCalled = false

val permissionRequest: PermissionRequest = object : PermissionRequest {
override val uri: String?
get() = "http://www.mozilla.org"
override val permissions: List<Permission>
get() = listOf(permission)

override fun grant(permissions: List<Permission>) {
grantWasCalled = true
}

override fun reject() {
rejectWasCalled = true
}
}

mockStorage = mock()
session.contentPermissionRequest = Consumable.from(permissionRequest)

runBlocking {
val prompt = sitePermissionFeature.onContentPermissionRequested(session, permissionRequest)

when (permission) {
is ContentGeoLocation, is ContentAudioCapture, is ContentAudioMicrophone -> {
assertTrue(rejectWasCalled)
assertFalse(grantWasCalled)
assertNull(prompt)
assertTrue(session.contentPermissionRequest.isConsumed())
}
is ContentVideoCamera, is ContentVideoCapture, is ContentNotification -> {
assertFalse(rejectWasCalled)
assertFalse(grantWasCalled)
assertNotNull(prompt)
}

else -> throw InvalidParameterException()
}
}
}
}

@Test
fun `storing a new SitePermissions must call save on the store`() {
val sitePermissionsList = listOf(ContentGeoLocation())
Expand Down Expand Up @@ -226,17 +289,21 @@ class SitePermissionsFeatureTest {
}

@Test
fun `requesting a content permissions with an already stored allowed permission will auto granted it and not show a prompt`() {
fun `requesting a content permissions with an already stored allowed permissions will auto granted it and not show a prompt`() {
val request: PermissionRequest = mock()
val mockSession: Session = mock()
val mockConsumable: Consumable<PermissionRequest> = mock()
val sitePermissionFromStorage: SitePermissions = mock()
val permissionList = listOf(ContentGeoLocation())

doReturn(permissionList).`when`(request).permissions
doReturn(sitePermissionFromStorage).`when`(mockStorage).findSitePermissionsBy(anyString())
doReturn(ALLOWED).`when`(sitePermissionFromStorage).location
doReturn(mockConsumable).`when`(mockSession).contentPermissionRequest
doReturn(true).`when`(mockConsumable).consume { true }

runBlocking {
val prompt = sitePermissionFeature.onContentPermissionRequested(mock(), request)
val prompt = sitePermissionFeature.onContentPermissionRequested(mockSession, request)
verify(mockStorage).findSitePermissionsBy(anyString())
verify(request).grant(permissionList)
assertNull(prompt)
Expand All @@ -246,15 +313,19 @@ class SitePermissionsFeatureTest {
@Test
fun `requesting a content permissions with an already stored blocked permission will auto block it and not show a prompt`() {
val request: PermissionRequest = mock()
val mockSession: Session = mock()
val mockConsumable: Consumable<PermissionRequest> = mock()
val sitePermissionFromStorage: SitePermissions = mock()
val permissionList = listOf(ContentGeoLocation())

doReturn(permissionList).`when`(request).permissions
doReturn(sitePermissionFromStorage).`when`(mockStorage).findSitePermissionsBy(anyString())
doReturn(BLOCKED).`when`(sitePermissionFromStorage).location
doReturn(mockConsumable).`when`(mockSession).contentPermissionRequest
doReturn(true).`when`(mockConsumable).consume { true }

runBlocking {
val prompt = sitePermissionFeature.onContentPermissionRequested(mock(), request)
val prompt = sitePermissionFeature.onContentPermissionRequested(mockSession, request)
verify(mockStorage).findSitePermissionsBy(anyString())
verify(request).reject()
assertNull(prompt)
Expand Down
Loading

0 comments on commit d3068f3

Please sign in to comment.