From 1befbad913d901677fdc8eabdd88e0d8e079e8dd Mon Sep 17 00:00:00 2001 From: "codrut.topliceanu" Date: Wed, 30 Sep 2020 14:37:32 +0300 Subject: [PATCH] For #8554 - Migrate permissionFeature to KotlinFlow --- .../permission/GeckoPermissionRequest.kt | 5 +- .../permission/GeckoPermissionRequest.kt | 5 +- .../permission/GeckoPermissionRequest.kt | 5 +- .../permission/SystemPermissionRequest.kt | 1 + .../components/browser/session/Session.kt | 24 - .../browser/session/engine/EngineObserver.kt | 27 +- .../components/browser/session/SessionTest.kt | 56 - .../session/engine/EngineObserverTest.kt | 85 +- .../browser/state/action/BrowserAction.kt | 47 + .../browser/state/ext/PermissionRequest.kt | 15 + .../state/reducer/ContentStateReducer.kt | 61 + .../browser/state/state/ContentState.kt | 5 + .../engine/permission/PermissionRequest.kt | 5 + .../permission/PermissionRequestTest.kt | 3 +- .../feature/sitepermissions/build.gradle | 8 + .../SitePermissionsDialogFragment.kt | 11 +- .../sitepermissions/SitePermissionsFeature.kt | 444 +++--- .../SitePermissionsDialogFragmentTest.kt | 69 +- .../SitePermissionsFeatureTest.kt | 1207 +++++++---------- .../SitePermissionsRulesTest.kt | 4 +- docs/changelog.md | 1 + .../samples/browser/BaseBrowserFragment.kt | 3 +- 22 files changed, 1029 insertions(+), 1062 deletions(-) create mode 100644 components/browser/state/src/main/java/mozilla/components/browser/state/ext/PermissionRequest.kt diff --git a/components/browser/engine-gecko-beta/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt b/components/browser/engine-gecko-beta/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt index 8124b8ed954..f5ea619e403 100644 --- a/components/browser/engine-gecko-beta/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt +++ b/components/browser/engine-gecko-beta/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt @@ -22,16 +22,19 @@ import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE +import java.util.UUID /** * Gecko-based implementation of [PermissionRequest]. * * @property permissions the list of requested permissions. * @property callback the callback to grant/reject the requested permissions. + * @property id a unique identifier for the request. */ sealed class GeckoPermissionRequest constructor( override val permissions: List, - private val callback: PermissionDelegate.Callback? = null + private val callback: PermissionDelegate.Callback? = null, + override val id: String = UUID.randomUUID().toString() ) : PermissionRequest { /** diff --git a/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt b/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt index 8124b8ed954..f5ea619e403 100644 --- a/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt +++ b/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt @@ -22,16 +22,19 @@ import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE +import java.util.UUID /** * Gecko-based implementation of [PermissionRequest]. * * @property permissions the list of requested permissions. * @property callback the callback to grant/reject the requested permissions. + * @property id a unique identifier for the request. */ sealed class GeckoPermissionRequest constructor( override val permissions: List, - private val callback: PermissionDelegate.Callback? = null + private val callback: PermissionDelegate.Callback? = null, + override val id: String = UUID.randomUUID().toString() ) : PermissionRequest { /** diff --git a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt index 8124b8ed954..f5ea619e403 100644 --- a/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt +++ b/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt @@ -22,16 +22,19 @@ import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_AUTOPLAY import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE +import java.util.UUID /** * Gecko-based implementation of [PermissionRequest]. * * @property permissions the list of requested permissions. * @property callback the callback to grant/reject the requested permissions. + * @property id a unique identifier for the request. */ sealed class GeckoPermissionRequest constructor( override val permissions: List, - private val callback: PermissionDelegate.Callback? = null + private val callback: PermissionDelegate.Callback? = null, + override val id: String = UUID.randomUUID().toString() ) : PermissionRequest { /** diff --git a/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequest.kt b/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequest.kt index 36f6adf1423..ac51f33bc01 100644 --- a/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequest.kt +++ b/components/browser/engine-system/src/main/java/mozilla/components/browser/engine/system/permission/SystemPermissionRequest.kt @@ -17,6 +17,7 @@ import android.webkit.PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID */ class SystemPermissionRequest(private val nativeRequest: android.webkit.PermissionRequest) : PermissionRequest { override val uri: String = nativeRequest.origin.toString() + override val id: String = java.util.UUID.randomUUID().toString() override val permissions = nativeRequest.resources.map { resource -> permissionsMap.getOrElse(resource) { Permission.Generic(resource) } diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt index db46a5c39a2..f4f8cf3e024 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/Session.kt @@ -29,7 +29,6 @@ import mozilla.components.concept.engine.content.blocking.Tracker import mozilla.components.concept.engine.manifest.WebAppManifest import mozilla.components.concept.engine.media.RecordingDevice import mozilla.components.concept.engine.permission.PermissionRequest -import mozilla.components.support.base.observer.Consumable import mozilla.components.support.base.observer.Observable import mozilla.components.support.base.observer.ObserverRegistry import java.util.UUID @@ -276,29 +275,6 @@ class Session( // we try to lookup the icon from an attached BrowserStore if possible. get() = store?.state?.findTabOrCustomTab(id)?.content?.icon - /** - * [Consumable] permission request from web content. A [PermissionRequest] - * must be consumed i.e. either [PermissionRequest.grant] or - * [PermissionRequest.reject] must be called. A content permission request - * can also be cancelled, which will result in a new empty [Consumable]. - */ - var contentPermissionRequest: Consumable by Delegates.vetoable(Consumable.empty()) { - _, _, request -> - val consumers = wrapConsumers { onContentPermissionRequested(this@Session, it) } - !request.consumeBy(consumers) - } - - /** - * [Consumable] permission request for the app. A [PermissionRequest] - * must be consumed i.e. either [PermissionRequest.grant] or - * [PermissionRequest.reject] must be called. - */ - var appPermissionRequest: Consumable by Delegates.vetoable(Consumable.empty()) { - _, _, request -> - val consumers = wrapConsumers { onAppPermissionRequested(this@Session, it) } - !request.consumeBy(consumers) - } - /** * List of recording devices (e.g. camera or microphone) currently in use by web content. */ diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineObserver.kt b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineObserver.kt index 9a10e43d9c8..e4e9d9f558f 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineObserver.kt +++ b/components/browser/session/src/main/java/mozilla/components/browser/session/engine/EngineObserver.kt @@ -38,7 +38,6 @@ import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.window.WindowRequest import mozilla.components.concept.fetch.Response import mozilla.components.lib.state.Store -import mozilla.components.support.base.observer.Consumable import mozilla.components.support.ktx.android.net.isInScope import mozilla.components.support.ktx.kotlin.isSameOriginAs @@ -75,10 +74,7 @@ internal class EngineObserver( } if (!session.url.isSameOriginAs(url)) { - session.contentPermissionRequest.consume { - it.reject() - true - } + store?.dispatch(ContentAction.ClearPermissionRequests(session.id)) } session.url = url @@ -255,15 +251,30 @@ internal class EngineObserver( } override fun onContentPermissionRequest(permissionRequest: PermissionRequest) { - session.contentPermissionRequest = Consumable.from(permissionRequest) + store?.dispatch( + ContentAction.UpdatePermissionsRequest( + session.id, + permissionRequest + ) + ) } override fun onCancelContentPermissionRequest(permissionRequest: PermissionRequest) { - session.contentPermissionRequest = Consumable.empty() + store?.dispatch( + ContentAction.ConsumePermissionsRequest( + session.id, + permissionRequest + ) + ) } override fun onAppPermissionRequest(permissionRequest: PermissionRequest) { - session.appPermissionRequest = Consumable.from(permissionRequest) + store?.dispatch( + ContentAction.UpdateAppPermissionsRequest( + session.id, + permissionRequest + ) + ) } override fun onPromptRequest(promptRequest: PromptRequest) { diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt index cd2ca97964e..3ef53be69f5 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/SessionTest.kt @@ -22,7 +22,6 @@ import mozilla.components.concept.engine.manifest.Size import mozilla.components.concept.engine.manifest.WebAppManifest import mozilla.components.concept.engine.media.RecordingDevice import mozilla.components.concept.engine.permission.PermissionRequest -import mozilla.components.support.base.observer.Consumable import mozilla.components.support.test.any import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.eq @@ -580,61 +579,6 @@ class SessionTest { defaultObserver.onRecordingDevicesChanged(session, emptyList()) } - @Test - fun `permission requests will be set on session if no observer consumes them`() { - val contentPermissionRequest: PermissionRequest = mock() - val appPermissionRequest: PermissionRequest = mock() - - val session = Session("https://www.mozilla.org") - session.contentPermissionRequest = Consumable.from(contentPermissionRequest) - session.appPermissionRequest = Consumable.from(appPermissionRequest) - assertFalse(session.contentPermissionRequest.isConsumed()) - - var contentPermissionRequestIsSet = false - var appPermissionRequestIsSet = false - session.contentPermissionRequest.consume { - contentPermissionRequestIsSet = true - true - } - session.appPermissionRequest.consume { - appPermissionRequestIsSet = true - true - } - assertTrue(contentPermissionRequestIsSet) - assertTrue(appPermissionRequestIsSet) - } - - @Test - fun `permission requests will not be set on session if consumed by observer`() { - var contentPermissionCallbackExecuted = false - var appPermissionCallbackExecuted = false - - val session = Session("https://www.mozilla.org") - session.register(object : Session.Observer { - override fun onContentPermissionRequested(session: Session, permissionRequest: PermissionRequest): Boolean { - contentPermissionCallbackExecuted = true - return true - } - - override fun onAppPermissionRequested(session: Session, permissionRequest: PermissionRequest): Boolean { - appPermissionCallbackExecuted = true - return true - } - }) - - val contentPermissionRequest: PermissionRequest = mock() - session.contentPermissionRequest = Consumable.from(contentPermissionRequest) - - val appPermissionRequestIsSet: PermissionRequest = mock() - session.appPermissionRequest = Consumable.from(appPermissionRequestIsSet) - - assertTrue(contentPermissionCallbackExecuted) - assertTrue(session.contentPermissionRequest.isConsumed()) - - assertTrue(appPermissionCallbackExecuted) - assertTrue(session.appPermissionRequest.isConsumed()) - } - @Test fun `handle empty blocked trackers list race conditions`() { val observer = mock(Session.Observer::class.java) diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineObserverTest.kt b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineObserverTest.kt index 4c161463e6f..86f57e322eb 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineObserverTest.kt +++ b/components/browser/session/src/test/java/mozilla/components/browser/session/engine/EngineObserverTest.kt @@ -8,6 +8,8 @@ import android.content.Intent import android.graphics.Bitmap import android.view.WindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.Job +import kotlinx.coroutines.test.runBlockingTest import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.action.ContentAction @@ -29,7 +31,6 @@ import mozilla.components.concept.engine.permission.PermissionRequest import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.window.WindowRequest import mozilla.components.concept.fetch.Response -import mozilla.components.support.base.observer.Consumable import mozilla.components.support.test.any import mozilla.components.support.test.eq import mozilla.components.support.test.libstate.ext.waitUntilIdle @@ -42,6 +43,7 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset @@ -308,30 +310,30 @@ class EngineObserverTest { @Test fun engineObserverClearsContentPermissionRequestIfNewPageStartsLoading() { - val session = Session("https://www.mozilla.org") - val permissionRequest: PermissionRequest = mock() - val observer = EngineObserver(session, mock()) - - observer.onContentPermissionRequest(permissionRequest) - - observer.onLocationChange("https://getpocket.com") + val session = Session("https://www.mozilla.org", id = "sessionId") + val store: BrowserStore = mock() + val observer = EngineObserver(session, store) + val action = ContentAction.ClearPermissionRequests("sessionId") + doReturn(Job()).`when`(store).dispatch(action) - assertTrue(session.contentPermissionRequest.isConsumed()) - verify(permissionRequest).reject() + runBlockingTest { + observer.onLocationChange("https://getpocket.com") + verify(store).dispatch(action) + } } @Test fun engineObserverDoesNotClearContentPermissionRequestIfSamePageStartsLoading() { val session = Session("https://www.mozilla.org") - val permissionRequest: PermissionRequest = mock() - val observer = EngineObserver(session, mock()) - - observer.onContentPermissionRequest(permissionRequest) - - observer.onLocationChange("https://www.mozilla.org/hello.html") + val store: BrowserStore = mock() + val observer = EngineObserver(session, store) + val action = ContentAction.ClearPermissionRequests("sessionId") + doReturn(Job()).`when`(store).dispatch(action) - assertFalse(session.contentPermissionRequest.isConsumed()) - verify(permissionRequest, never()).reject() + runBlockingTest { + observer.onLocationChange("https://www.mozilla.org/hello.html") + verify(store, never()).dispatch(action) + } } @Test @@ -509,39 +511,36 @@ class EngineObserverTest { @Test fun engineSessionObserverWithContentPermissionRequests() { val permissionRequest = mock(PermissionRequest::class.java) - val session = Session("") - val observer = EngineObserver(session, mock()) - - assertTrue(session.contentPermissionRequest.isConsumed()) - observer.onContentPermissionRequest(permissionRequest) - assertFalse(session.contentPermissionRequest.isConsumed()) + val session = Session("url", id = "id") + val store: BrowserStore = mock() + val observer = EngineObserver(session, store) + val action = ContentAction.UpdatePermissionsRequest( + session.id, + permissionRequest + ) + doReturn(Job()).`when`(store).dispatch(action) - observer.onCancelContentPermissionRequest(permissionRequest) - assertTrue(session.contentPermissionRequest.isConsumed()) + runBlockingTest { + observer.onContentPermissionRequest(permissionRequest) + verify(store).dispatch(action) + } } @Test fun engineSessionObserverWithAppPermissionRequests() { val permissionRequest = mock(PermissionRequest::class.java) val session = Session("") - val observer = EngineObserver(session, mock()) - - assertTrue(session.appPermissionRequest.isConsumed()) - observer.onAppPermissionRequest(permissionRequest) - assertFalse(session.appPermissionRequest.isConsumed()) - } - - @Test - fun engineObserverConsumesContentPermissionRequestIfNewPageStartsLoading() { - val permissionRequest = mock(PermissionRequest::class.java) - val session = Session("https://www.mozilla.org") - session.contentPermissionRequest = Consumable.from(permissionRequest) - - val observer = EngineObserver(session, mock()) - observer.onLocationChange("https://getpocket.com") + val store: BrowserStore = mock() + val observer = EngineObserver(session, store) + val action = ContentAction.UpdateAppPermissionsRequest( + session.id, + permissionRequest + ) - verify(permissionRequest).reject() - assertTrue(session.contentPermissionRequest.isConsumed()) + runBlockingTest { + observer.onAppPermissionRequest(permissionRequest) + verify(store).dispatch(action) + } } @Test diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt index 8d7ef8b2f64..1927c37a172 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt @@ -37,6 +37,7 @@ import mozilla.components.concept.engine.content.blocking.Tracker import mozilla.components.concept.engine.history.HistoryItem import mozilla.components.concept.engine.manifest.WebAppManifest import mozilla.components.concept.engine.media.Media +import mozilla.components.concept.engine.permission.PermissionRequest import mozilla.components.concept.engine.mediasession.MediaSession import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.search.SearchRequest @@ -445,6 +446,52 @@ sealed class ContentAction : BrowserAction() { * Updates the [LoadRequestState] of the [ContentState] with the given [sessionId]. */ data class UpdateLoadRequestAction(val sessionId: String, val loadRequest: LoadRequestState) : ContentAction() + + /** + * Adds a new content permission request to the [ContentState] list. + * */ + data class UpdatePermissionsRequest( + val sessionId: String, + val permissionRequest: PermissionRequest + ) : ContentAction() + + /** + * Deletes a content permission request from the [ContentState] list. + * */ + data class ConsumePermissionsRequest( + val sessionId: String, + val permissionRequest: PermissionRequest + ) : ContentAction() + + /** + * Removes all content permission requests from the [ContentState] list. + * */ + data class ClearPermissionRequests( + val sessionId: String + ) : ContentAction() + + /** + * Adds a new app permission request to the [ContentState] list. + * */ + data class UpdateAppPermissionsRequest( + val sessionId: String, + val appPermissionRequest: PermissionRequest + ) : ContentAction() + + /** + * Deletes an app permission request from the [ContentState] list. + * */ + data class ConsumeAppPermissionsRequest( + val sessionId: String, + val appPermissionRequest: PermissionRequest + ) : ContentAction() + + /** + * Removes all app permission requests from the [ContentState] list. + * */ + data class ClearAppPermissionRequests( + val sessionId: String + ) : ContentAction() } /** diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/ext/PermissionRequest.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/ext/PermissionRequest.kt new file mode 100644 index 00000000000..5d9c3e251eb --- /dev/null +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/ext/PermissionRequest.kt @@ -0,0 +1,15 @@ +/* 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.ext +import mozilla.components.concept.engine.permission.PermissionRequest + +/** + * @returns true if the given [permissionRequest] is contained in this list otherwise false + * */ +fun List.containsPermission(permissionRequest: PermissionRequest): Boolean { + return this.any { + it.uri == permissionRequest.uri && + it.permissions == permissionRequest.permissions + } +} diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt index 5d23df09e3a..44eda44dbdb 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt @@ -6,6 +6,7 @@ package mozilla.components.browser.state.reducer import android.net.Uri import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.ext.containsPermission import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.SessionState @@ -128,6 +129,66 @@ internal object ContentStateReducer { is ContentAction.UpdateHistoryStateAction -> updateContentState(state, action.sessionId) { it.copy(history = HistoryState(action.historyList, action.currentIndex)) } + is ContentAction.UpdatePermissionsRequest -> updateContentState( + state, + action.sessionId + ) { + if (!it.permissionRequestsList.containsPermission(action.permissionRequest)) { + it.copy( + permissionRequestsList = it.permissionRequestsList + action.permissionRequest + ) + } else { + it + } + } + is ContentAction.ConsumePermissionsRequest -> updateContentState( + state, + action.sessionId + ) { + if (it.permissionRequestsList.containsPermission(action.permissionRequest)) { + it.copy( + permissionRequestsList = it.permissionRequestsList - action.permissionRequest + ) + } else { + it + } + } + is ContentAction.UpdateAppPermissionsRequest -> updateContentState( + state, + action.sessionId + ) { + if (!it.appPermissionRequestsList.containsPermission(action.appPermissionRequest)) { + it.copy( + appPermissionRequestsList = it.appPermissionRequestsList + action.appPermissionRequest + ) + } else { + it + } + } + is ContentAction.ConsumeAppPermissionsRequest -> updateContentState( + state, + action.sessionId + ) { + if (it.appPermissionRequestsList.containsPermission(action.appPermissionRequest)) { + it.copy( + appPermissionRequestsList = it.appPermissionRequestsList - action.appPermissionRequest + ) + } else { + it + } + } + is ContentAction.ClearPermissionRequests -> updateContentState( + state, + action.sessionId + ) { + it.copy(permissionRequestsList = emptyList()) + } + is ContentAction.ClearAppPermissionRequests -> updateContentState( + state, + action.sessionId + ) { + it.copy(appPermissionRequestsList = emptyList()) + } is ContentAction.UpdateLoadRequestAction -> updateContentState(state, action.sessionId) { it.copy(loadRequest = action.loadRequest) } diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt index 4c9e849b13b..bdad48a15f1 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt @@ -10,6 +10,7 @@ import mozilla.components.browser.state.state.content.FindResultState import mozilla.components.browser.state.state.content.HistoryState import mozilla.components.concept.engine.HitResult import mozilla.components.concept.engine.manifest.WebAppManifest +import mozilla.components.concept.engine.permission.PermissionRequest import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.concept.engine.search.SearchRequest import mozilla.components.concept.engine.window.WindowRequest @@ -43,6 +44,8 @@ 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 appPermissionRequestsList Holds unprocessed app requests. */ data class ContentState( val url: String, @@ -67,6 +70,8 @@ data class ContentState( val webAppManifest: WebAppManifest? = null, val firstContentfulPaint: Boolean = false, val history: HistoryState = HistoryState(), + val permissionRequestsList: List = emptyList(), + val appPermissionRequestsList: List = emptyList(), val pictureInPictureEnabled: Boolean = false, val loadRequest: LoadRequestState? = null ) diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt index cf790488f25..a52af746f55 100644 --- a/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt +++ b/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt @@ -14,6 +14,11 @@ interface PermissionRequest { */ val uri: String? + /** + * A unique identifier for the request. + */ + val id: String + /** * List of requested permissions. */ diff --git a/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionRequestTest.kt b/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionRequestTest.kt index 0d463ac559d..243536a4f2a 100644 --- a/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionRequestTest.kt +++ b/components/concept/engine/src/test/java/mozilla/components/concept/engine/permission/PermissionRequestTest.kt @@ -72,7 +72,8 @@ class PermissionRequestTest { private class MockPermissionRequest( override val permissions: List, - override val uri: String = "" + override val uri: String = "", + override val id: String = "" ) : PermissionRequest { var granted = false var rejected = false diff --git a/components/feature/sitepermissions/build.gradle b/components/feature/sitepermissions/build.gradle index 78f7a322c4d..cbfd4a8f3d2 100644 --- a/components/feature/sitepermissions/build.gradle +++ b/components/feature/sitepermissions/build.gradle @@ -44,6 +44,13 @@ android { } } +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions.freeCompilerArgs += [ + "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi" + ] +} + + dependencies { implementation project(':browser-session') implementation project(':concept-engine') @@ -66,6 +73,7 @@ dependencies { testImplementation Dependencies.androidx_test_junit testImplementation Dependencies.testing_robolectric testImplementation Dependencies.testing_mockito + testImplementation Dependencies.testing_coroutines androidTestImplementation project(':support-android-test') diff --git a/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt b/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt index 88b4ec7bbe5..8c17fd6fc5f 100644 --- a/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt +++ b/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt @@ -34,6 +34,7 @@ private const val KEY_SHOULD_SHOW_DO_NOT_ASK_AGAIN_CHECKBOX = "KEY_SHOULD_SHOW_D private const val KEY_SHOULD_PRESELECT_DO_NOT_ASK_AGAIN_CHECKBOX = "KEY_SHOULD_PRESELECT_DO_NOT_ASK_AGAIN_CHECKBOX" private const val KEY_IS_NOTIFICATION_REQUEST = "KEY_IS_NOTIFICATION_REQUEST" private const val DEFAULT_VALUE = Int.MAX_VALUE +private const val KEY_PERMISSION_ID = "KEY_PERMISSION_ID" internal class SitePermissionsDialogFragment : AppCompatDialogFragment() { @@ -65,6 +66,8 @@ internal class SitePermissionsDialogFragment : AppCompatDialogFragment() { safeArguments.getBoolean(KEY_SHOULD_SHOW_DO_NOT_ASK_AGAIN_CHECKBOX, true) internal val shouldPreselectDoNotAskAgainCheckBox: Boolean get() = safeArguments.getBoolean(KEY_SHOULD_PRESELECT_DO_NOT_ASK_AGAIN_CHECKBOX, false) + internal val permissionRequestId: String get() = + safeArguments.getString(KEY_PERMISSION_ID, "") // State @@ -100,7 +103,7 @@ internal class SitePermissionsDialogFragment : AppCompatDialogFragment() { override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) - feature?.onDismiss(sessionId) + feature?.onDismiss(permissionRequestId, sessionId) } private fun Dialog.setContainerView(rootView: View) { @@ -131,7 +134,7 @@ internal class SitePermissionsDialogFragment : AppCompatDialogFragment() { val negativeButton = rootView.findViewById