Commit 3abc0819 authored by Arturo Mejia's avatar Arturo Mejia Committed by Christian Sadilek
Browse files

Closes #1949: Adding feature-sitepermissions component

parent e244b3e9
......@@ -80,6 +80,10 @@ projects:
path: components/feature/findinpage
description: 'Feature that will subscribe to the selected session and show an UI for results of find in page.'
publish: true
feature-sitepermissions:
path: components/feature/sitepermissions
description: 'A feature for showing site permission request prompts.'
publish: true
browser-awesomebar:
path: components/browser/awesomebar
description: 'A customizable awesomebar component for browsers.'
......
......@@ -128,6 +128,8 @@ _Combined components to implement feature-specific use cases._
* 🔴 [**Find In Page**](components/feature/findinpage/README.md) - A component that provides an UI widget for [find in page functionality](https://support.mozilla.org/en-US/kb/search-contents-current-page-text-or-links).
* 🔴 [**Site Permissions**](components/feature/sidepermissions/README.md) - A feature for showing site permission request prompts.
## UI
_Generic low-level UI components for building apps._
......
......@@ -111,6 +111,10 @@ sealed class GeckoPermissionRequest constructor(
callback.grant(videos.firstOrNull(), audios.firstOrNull())
}
override fun containsVideoAndAudioSources(): Boolean {
return videoSources.isNotEmpty() && audioSources.isNotEmpty()
}
override fun reject() {
callback.reject()
}
......
......@@ -4,6 +4,7 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'org.jetbrains.kotlin.android.extensions'
android {
compileSdkVersion config.compileSdkVersion
......@@ -19,6 +20,9 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
androidExtensions {
experimental = true
}
}
dependencies {
......
......@@ -4,6 +4,9 @@
package mozilla.components.concept.engine.permission
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
/**
* Represents a permission request, used when engines need access to protected
* resources. Every request must be handled by either calling [grant] or [reject].
......@@ -47,6 +50,8 @@ interface PermissionRequest {
* Rejects the requested permissions.
*/
fun reject()
fun containsVideoAndAudioSources() = false
}
/**
......@@ -55,26 +60,46 @@ interface PermissionRequest {
* @property id an optional native engine-specific ID of this permission.
* @property desc an optional description of what this permission type is for.
*/
sealed class Permission(open val id: String? = "", open val desc: String? = "") {
sealed class Permission(open val id: String? = "", open val desc: String? = "") : Parcelable {
@Parcelize
data class ContentAudioCapture(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentAudioMicrophone(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentAudioOther(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentAutoplayMedia(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentGeoLocation(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentNotification(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentProtectedMediaId(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentVideoApplication(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentVideoBrowser(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentVideoCamera(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentVideoCapture(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentVideoScreen(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentVideoWindow(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class ContentVideoOther(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class AppCamera(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class AppAudio(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class AppLocationCoarse(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class AppLocationFine(override val id: String? = "", override val desc: String? = "") : Permission(id)
@Parcelize
data class Generic(override val id: String?, override val desc: String? = "") : Permission(id)
}
# [Android Components](../../../README.md) > Feature > Site Permissions
A feature for showing site permission request prompts.
## Usage
### Setting up the dependency
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
```Groovy
implementation "org.mozilla.components:feature-sitepermissions:{latest-version}"
```
### SitePermissionsFeature
```
Add these permissions to your ``AndroidManifest.xml`` file.
```XML
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
```
```kotlin
val onNeedToRequestPermissions : (Array<String>) -> Unit = { permissions ->
/* You are in charge of triggering the request for the permissions needed,
* this way you can control, when you request the permissions,
* in case that you want to show an informative dialog,
* to clarify the use of these permissions.
*/
this.requestPermissions(permissions, REQUEST_CODE_APP_PERMISSIONS)
}
val sitePermissionsFeature = SitePermissionsFeature(
sessionManager = components.sessionManager,
fragmentManager = requireFragmentManager(),
onNeedToRequestPermissions = onNeedToRequestPermissions
)
// It will start listing for new permissionRequest.
sitePermissionsFeature.start()
// It will stop listing for new permissionRequest.
sitePermissionsFeature.stop()
// Notify the feature if the permissions requested were granted or rejected.
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
when (requestCode) {
REQUEST_CODE_APP_PERMISSIONS -> sitePermissionsFeature.onPermissionsResult(grantResults)
}
}
## License
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/
/* 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/. */
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion config.compileSdkVersion
defaultConfig {
minSdkVersion config.minSdkVersion
targetSdkVersion config.targetSdkVersion
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation project(':browser-session')
implementation project(':concept-engine')
implementation project(':ui-icons')
implementation Dependencies.kotlin_stdlib
implementation Dependencies.support_constraintlayout
testImplementation Dependencies.androidx_test_core
testImplementation Dependencies.testing_junit
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
testImplementation project(':support-test')
}
apply from: '../../../publish.gradle'
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
<!-- 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/. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="mozilla.components.feature.sitepermissions" />
/* 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.content.pm.PackageManager
import mozilla.components.browser.session.SelectionAwareSessionObserver
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.permission.Permission
import mozilla.components.concept.engine.permission.PermissionRequest
import mozilla.components.support.base.feature.LifecycleAwareFeature
typealias OnNeedToRequestPermissions = (permissions: Array<String>) -> Unit
internal const val FRAGMENT_TAG = "mozac_feature_site_permissions"
/**
* This feature will subscribe to the currently selected [Session] and display
* a suitable dialogs based on [Session.Observer.onAppPermissionRequested] or
* [Session.Observer.onContentPermissionRequested] events.
* Once the dialog is closed the [PermissionRequest] will be consumed.
*
* @property sessionManager the [SessionManager] instance in order to subscribe
* to the selected [Session].
* @property onNeedToRequestPermissions a callback invoked when permissions
* need to be requested. Once the request is completed, [onPermissionsResult] needs to be invoked.
**/
class SitePermissionsFeature(
private val sessionManager: SessionManager,
private val onNeedToRequestPermissions: OnNeedToRequestPermissions
) : LifecycleAwareFeature {
private val observer = SitePermissionsRequestObserver(sessionManager, feature = this)
override fun start() {
observer.observeSelected()
}
override fun stop() {
observer.stop()
}
/**
* Notifies the feature that the permissions requested were completed.
*
* @param grantResults the grant results for the corresponding permissions
* @see [onNeedToRequestPermissions].
*/
fun onPermissionsResult(grantResults: IntArray) {
sessionManager.selectedSession?.apply {
appPermissionRequest.consume { permissionsRequest ->
val allPermissionWereGranted = grantResults.all { grantResult ->
grantResult == PackageManager.PERMISSION_GRANTED
}
if (grantResults.isNotEmpty() && allPermissionWereGranted) {
permissionsRequest.grant()
} else {
permissionsRequest.reject()
}
true
}
}
}
/**
* Notifies that the list of [permissions] have been granted for the [sessionId] and [url].
*
* @param sessionId this is the id of the session which requested the permissions.
* @param url the url which requested the permissions.
* @param permissions the list of [permissions] that have been granted.
*/
fun onContentPermissionGranted(sessionId: String, url: String, permissions: List<Permission>) {
sessionManager.findSessionById(sessionId)?.apply {
contentPermissionRequest.consume {
it.grant()
// Update the DB
true
}
}
}
/**
* Notifies that the permissions requested by this [sessionId] were rejected.
*
* @param sessionId this is the id of the session which requested the permissions.
* @param url the url which requested the permissions.
*/
fun onContentPermissionDeny(sessionId: String, url: String) {
sessionManager.findSessionById(sessionId)?.apply {
contentPermissionRequest.consume {
it.reject()
// Update the DB
true
}
}
}
private fun onAppPermissionRequested(permissionRequest: PermissionRequest): Boolean {
val permissions = permissionRequest.permissions.map { it.id ?: "" }
onNeedToRequestPermissions(permissions.toTypedArray())
return false
}
internal class SitePermissionsRequestObserver(
sessionManager: SessionManager,
private val feature: SitePermissionsFeature
) : SelectionAwareSessionObserver(sessionManager) {
override fun onAppPermissionRequested(session: Session, permissionRequest: PermissionRequest): Boolean {
return feature.onAppPermissionRequested(permissionRequest)
}
}
}
<?xml version="1.0" encoding="utf-8"?><!-- 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/. -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
android:paddingTop="?android:attr/listPreferredItemPaddingLeft"
android:paddingEnd="?android:attr/listPreferredItemPaddingLeft">
<TextView
android:id="@+id/title"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Allow wikipedia.org to use your camera and microphone?" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/mozac_feature_sitepermissions_permission_group_spacing">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/icon_camara_group"
android:layout_width="@dimen/mozac_feature_sitepermissions_permission_icon_width"
android:layout_height="@dimen/mozac_feature_sitepermissions_permission_icon_height"
android:layout_marginTop="@dimen/mozac_feature_sitepermissions_permission_group_spacing"
android:layout_marginEnd="@dimen/mozac_feature_sitepermissions_permission_group_spacing"
android:background="@android:color/transparent"
android:clickable="false"
android:contentDescription="@string/mozac_feature_sitepermissions_accessibility_permission_camera_icon"
android:src="@drawable/mozac_ic_video"
tools:tint="@android:color/black" />
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.AppCompatRadioButton
android:id="@+id/option1_camera_group"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
tools:text="Back facing camera" />
<android.support.v7.widget.AppCompatRadioButton
android:id="@+id/option2_camera_group"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
tools:text="Selfie" />
</RadioGroup>
</LinearLayout>
<View
style="@style/Mozac.Feature.Site.Permissions.Item.Divider.Horizontal"
android:importantForAccessibility="no" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="@dimen/mozac_feature_sitepermissions_permission_group_spacing">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/icon_microphone_group"
android:layout_width="@dimen/mozac_feature_sitepermissions_permission_icon_width"
android:layout_height="@dimen/mozac_feature_sitepermissions_permission_icon_height"
android:layout_marginTop="@dimen/mozac_feature_sitepermissions_permission_group_spacing"
android:layout_marginEnd="@dimen/mozac_feature_sitepermissions_permission_group_spacing"
android:background="@android:color/transparent"
android:clickable="false"
android:contentDescription="@string/mozac_feature_sitepermissions_accessibility_permission_microphone_icon"
android:src="@drawable/mozac_ic_microphone"
tools:tint="@android:color/black" />
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<android.support.v7.widget.AppCompatRadioButton
android:id="@+id/option1_microphone_group"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
tools:text="Microphone 1" />
</RadioGroup>
</LinearLayout>
</LinearLayout>
</ScrollView>
<?xml version="1.0" encoding="utf-8"?><!-- 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/. -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
android:paddingTop="?android:attr/listPreferredItemPaddingLeft"
android:paddingEnd="?android:attr/listPreferredItemPaddingLeft">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<android.support.v7.widget.AppCompatImageView
android:id="@+id/icon"
android:layout_width="@dimen/mozac_feature_sitepermissions_permission_icon_width"
android:layout_height="@dimen/mozac_feature_sitepermissions_permission_icon_height"
android:layout_marginEnd="@dimen/mozac_feature_sitepermissions_permission_group_spacing"
android:background="@android:color/transparent"
android:clickable="false"
tools:src="@drawable/mozac_ic_video"
tools:tint="@android:color/black" />
<TextView
android:id="@+id/title"
style="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Select camera to use" />
</LinearLayout>
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/mozac_feature_sitepermissions_permission_group_spacing"
android:orientation="vertical"
android:paddingStart="30dp"
android:paddingEnd="30dp">
<android.support.v7.widget.AppCompatRadioButton
android:id="@+id/option1"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
tools:text="option1" />
<android.support.v7.widget.AppCompatRadioButton
android:id="@+id/option2"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
tools:text="option2" />
</RadioGroup>
</LinearLayout>
</ScrollView>
<?xml version="1.0" encoding="utf-8"?><!-- 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/. -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="?android:attr/listPreferredItemPaddingLeft"