From d84eff858768003b78206a08369291dc52b1b33d Mon Sep 17 00:00:00 2001 From: Mugurell Date: Mon, 13 Dec 2021 20:15:24 +0200 Subject: [PATCH] [components] For https://github.com/mozilla-mobile/android-components/issues/10739 - Add support for the Storage Access API prompt --- .../gecko/permission/GeckoPermissionRequest.kt | 4 +- .../permission/GeckoSitePermissionsStorage.kt | 24 +++++ .../permission/GeckoSitePermissionsStorageTest.kt | 93 +++++++++++++++++-- .../concept/engine/permission/PermissionRequest.kt | 2 + .../concept/engine/permission/SitePermissions.kt | 2 + .../engine/permission/SitePermissionsStorage.kt | 2 +- .../feature/sitepermissions/build.gradle | 1 + .../8.json | 100 ++++++++++++++++++++ .../OnDiskSitePermissionsStorage.kt | 2 + .../SitePermissionsDialogFragment.kt | 30 +++++- .../sitepermissions/SitePermissionsFeature.kt | 86 +++++++++++++++++ .../sitepermissions/SitePermissionsRules.kt | 7 +- .../sitepermissions/db/SitePermissionsDatabase.kt | 18 +++- .../sitepermissions/db/SitePermissionsEntity.kt | 5 + .../res/layout/mozac_site_permissions_prompt.xml | 41 +++++++- .../src/main/res/values/strings.xml | 8 ++ .../OnDiskSitePermissionsStorageTest.kt | 2 + .../SitePermissionsDialogFragmentTest.kt | 103 +++++++++++++++++++++ .../sitepermissions/SitePermissionsFeatureTest.kt | 78 ++++++++++++++++ .../sitepermissions/SitePermissionsRulesTest.kt | 11 +++ .../sitepermissions/db/SitePermissionEntityTest.kt | 4 + .../src/main/res/drawable/mozac_ic_cookies.xml | 14 +++ .../android/android-components/docs/changelog.md | 3 + .../mozilla/samples/browser/BaseBrowserFragment.kt | 3 +- 24 files changed, 628 insertions(+), 15 deletions(-) create mode 100644 mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/8.json create mode 100644 mobile/android/android-components/components/ui/icons/src/main/res/drawable/mozac_ic_cookies.xml diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt index 20cc8dbdf0d6..49cb6f517c31 100644 --- a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt +++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoPermissionRequest.kt @@ -27,6 +27,7 @@ import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_ import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS import java.util.UUID /** @@ -66,7 +67,8 @@ sealed class GeckoPermissionRequest constructor( PERMISSION_AUTOPLAY_AUDIBLE to Permission.ContentAutoPlayAudible(), PERMISSION_AUTOPLAY_INAUDIBLE to Permission.ContentAutoPlayInaudible(), PERMISSION_PERSISTENT_STORAGE to Permission.ContentPersistentStorage(), - PERMISSION_MEDIA_KEY_SYSTEM_ACCESS to Permission.ContentMediaKeySystemAccess() + PERMISSION_MEDIA_KEY_SYSTEM_ACCESS to Permission.ContentMediaKeySystemAccess(), + PERMISSION_STORAGE_ACCESS to Permission.ContentCrossOriginStorageAccess() ) } diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorage.kt b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorage.kt index 801a443b4a3a..d00b92b169ee 100644 --- a/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorage.kt +++ b/mobile/android/android-components/components/browser/engine-gecko/src/main/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorage.kt @@ -18,6 +18,7 @@ import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOW import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED import mozilla.components.concept.engine.permission.SitePermissions.Status.NO_DECISION import mozilla.components.concept.engine.permission.SitePermissionsStorage +import mozilla.components.support.ktx.kotlin.stripDefaultPort import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission @@ -30,6 +31,7 @@ import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_ import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING import org.mozilla.geckoview.StorageController import org.mozilla.geckoview.StorageController.ClearFlags @@ -154,6 +156,7 @@ class GeckoSitePermissionsStorage( val geckoLocation = geckoPermissionsByType[PERMISSION_GEOLOCATION]?.firstOrNull() val geckoMedia = geckoPermissionsByType[PERMISSION_MEDIA_KEY_SYSTEM_ACCESS]?.firstOrNull() val geckoLocalStorage = geckoPermissionsByType[PERMISSION_PERSISTENT_STORAGE]?.firstOrNull() + val geckoCrossOriginStorageAccess = geckoPermissionsByType[PERMISSION_STORAGE_ACCESS]?.firstOrNull() val geckoAudible = geckoPermissionsByType[PERMISSION_AUTOPLAY_AUDIBLE]?.firstOrNull() val geckoInAudible = geckoPermissionsByType[PERMISSION_AUTOPLAY_INAUDIBLE]?.firstOrNull() @@ -199,6 +202,15 @@ class GeckoSitePermissionsStorage( updatedPermission = updatedPermission.copy(localStorage = NO_DECISION) } + if (geckoCrossOriginStorageAccess != null) { + removeTemporaryPermissionIfAny(geckoCrossOriginStorageAccess) + geckoStorage.setPermission( + geckoCrossOriginStorageAccess, + userSitePermissions.crossOriginStorageAccess.toGeckoStatus() + ) + updatedPermission = updatedPermission.copy(crossOriginStorageAccess = NO_DECISION) + } + if (geckoAudible != null) { removeTemporaryPermissionIfAny(geckoAudible) geckoStorage.setPermission( @@ -241,6 +253,12 @@ class GeckoSitePermissionsStorage( val geckoLocation = geckoPermissionByType[PERMISSION_GEOLOCATION]?.firstOrNull() val geckoMedia = geckoPermissionByType[PERMISSION_MEDIA_KEY_SYSTEM_ACCESS]?.firstOrNull() val geckoStorage = geckoPermissionByType[PERMISSION_PERSISTENT_STORAGE]?.firstOrNull() + // Currently we'll receive the "storage_access" permission for all iframes of the same parent + // so we need to ensure we are reporting the permission for the current iframe request. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1746436 for more details. + val geckoCrossOriginStorageAccess = geckoPermissionByType[PERMISSION_STORAGE_ACCESS]?.firstOrNull { + it.thirdPartyOrigin == onDiskPermissions.origin.stripDefaultPort() + } val geckoAudible = geckoPermissionByType[PERMISSION_AUTOPLAY_AUDIBLE]?.firstOrNull() val geckoInAudible = geckoPermissionByType[PERMISSION_AUTOPLAY_INAUDIBLE]?.firstOrNull() @@ -272,6 +290,12 @@ class GeckoSitePermissionsStorage( ) } + if (geckoCrossOriginStorageAccess != null && geckoCrossOriginStorageAccess.value != VALUE_PROMPT) { + combinedPermissions = combinedPermissions?.copy( + crossOriginStorageAccess = geckoCrossOriginStorageAccess.value.toStatus() + ) + } + /** * Autoplay permissions don't have initial values, so when the value is changed on * the gecko storage we trust it. diff --git a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt index 923edc711dfa..2d87ec73602a 100644 --- a/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt +++ b/mobile/android/android-components/components/browser/engine-gecko/src/test/java/mozilla/components/browser/engine/gecko/permission/GeckoSitePermissionsStorageTest.kt @@ -41,6 +41,7 @@ import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_ import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_MEDIA_KEY_SYSTEM_ACCESS import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_PERSISTENT_STORAGE +import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_STORAGE_ACCESS import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_TRACKING import org.mozilla.geckoview.StorageController import org.mozilla.geckoview.StorageController.ClearFlags @@ -116,6 +117,23 @@ class GeckoSitePermissionsStorageTest { } @Test + fun `GIVEN a crossOriginStorageAccess permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runBlockingTest { + val sitePermissions = createNewSitePermission().copy(crossOriginStorageAccess = BLOCKED) + val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_STORAGE_ACCESS) + val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_STORAGE_ACCESS, geckoPermissions, mock()) + val permissionsCaptor = argumentCaptor() + + doReturn(Unit).`when`(geckoStorage).clearGeckoCacheFor(sitePermissions.origin) + + geckoStorage.save(sitePermissions, geckoRequest) + + verify(onDiskStorage).save(permissionsCaptor.capture(), any()) + + assertEquals(NO_DECISION, permissionsCaptor.value.crossOriginStorageAccess) + verify(storageController).setPermission(geckoPermissions, VALUE_DENY) + } + + @Test fun `GIVEN a mediaKeySystemAccess permission WHEN saving THEN the permission is saved in the gecko storage and set to the default value on the disk storage`() = runBlockingTest { val sitePermissions = createNewSitePermission().copy(mediaKeySystemAccess = ALLOWED) val geckoPermissions = geckoContentPermission("mozilla.org", PERMISSION_MEDIA_KEY_SYSTEM_ACCESS) @@ -204,7 +222,8 @@ class GeckoSitePermissionsStorageTest { @Test fun `GIVEN multiple saved temporary permissions WHEN clearing all temporary permission THEN all permissions are cleared`() = runBlockingTest { val geckoAutoPlayPermissions = geckoContentPermission("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE) - val geckoStoragePermissions = geckoContentPermission("mozilla.org", PERMISSION_PERSISTENT_STORAGE) + val geckoPersistentStoragePermissions = geckoContentPermission("mozilla.org", PERMISSION_PERSISTENT_STORAGE) + val geckoStorageAccessPermissions = geckoContentPermission("mozilla.org", PERMISSION_STORAGE_ACCESS) val geckoRequest = GeckoPermissionRequest.Content("mozilla.org", PERMISSION_AUTOPLAY_AUDIBLE, geckoAutoPlayPermissions, mock()) assertTrue(geckoStorage.geckoTemporaryPermissions.isEmpty()) @@ -213,14 +232,19 @@ class GeckoSitePermissionsStorageTest { assertEquals(1, geckoStorage.geckoTemporaryPermissions.size) - geckoStorage.saveTemporary(geckoRequest.copy(geckoPermission = geckoStoragePermissions)) + geckoStorage.saveTemporary(geckoRequest.copy(geckoPermission = geckoPersistentStoragePermissions)) assertEquals(2, geckoStorage.geckoTemporaryPermissions.size) + geckoStorage.saveTemporary(geckoRequest.copy(geckoPermission = geckoStorageAccessPermissions)) + + assertEquals(3, geckoStorage.geckoTemporaryPermissions.size) + geckoStorage.clearTemporaryPermissions() verify(storageController).setPermission(geckoAutoPlayPermissions, VALUE_PROMPT) - verify(storageController).setPermission(geckoStoragePermissions, VALUE_PROMPT) + verify(storageController).setPermission(geckoPersistentStoragePermissions, VALUE_PROMPT) + verify(storageController).setPermission(geckoStorageAccessPermissions, VALUE_PROMPT) assertTrue(geckoStorage.geckoTemporaryPermissions.isEmpty()) } @@ -256,6 +280,7 @@ class GeckoSitePermissionsStorageTest { val sitePermissions = SitePermissions( origin = "mozilla.dev", localStorage = ALLOWED, + crossOriginStorageAccess = ALLOWED, location = ALLOWED, notification = ALLOWED, microphone = ALLOWED, @@ -272,7 +297,8 @@ class GeckoSitePermissionsStorageTest { geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS), geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE), geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE), - geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE) + geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE), + geckoContentPermission(type = PERMISSION_STORAGE_ACCESS) ) doReturn(geckoPermissions).`when`(geckoStorage).findGeckoContentPermissionBy(anyString(), anyBoolean()) @@ -288,6 +314,7 @@ class GeckoSitePermissionsStorageTest { assertEquals(NO_DECISION, permission.location) assertEquals(NO_DECISION, permission.notification) assertEquals(NO_DECISION, permission.localStorage) + assertEquals(NO_DECISION, permission.crossOriginStorageAccess) assertEquals(NO_DECISION, permission.mediaKeySystemAccess) assertEquals(ALLOWED, permission.camera) assertEquals(ALLOWED, permission.microphone) @@ -300,6 +327,7 @@ class GeckoSitePermissionsStorageTest { val sitePermissions = SitePermissions( origin = "mozilla.dev", localStorage = ALLOWED, + crossOriginStorageAccess = ALLOWED, location = ALLOWED, notification = ALLOWED, microphone = ALLOWED, @@ -315,6 +343,7 @@ class GeckoSitePermissionsStorageTest { geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION, value = VALUE_ALLOW), geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS, value = VALUE_ALLOW), geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE, value = VALUE_ALLOW), + geckoContentPermission(type = PERMISSION_STORAGE_ACCESS, value = VALUE_ALLOW), geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE, value = VALUE_ALLOW), geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE, value = VALUE_ALLOW) ) @@ -327,6 +356,7 @@ class GeckoSitePermissionsStorageTest { assertEquals(ALLOWED, foundPermissions.location) assertEquals(ALLOWED, foundPermissions.notification) assertEquals(ALLOWED, foundPermissions.localStorage) + assertEquals(ALLOWED, foundPermissions.crossOriginStorageAccess) assertEquals(ALLOWED, foundPermissions.mediaKeySystemAccess) assertEquals(ALLOWED, foundPermissions.camera) assertEquals(ALLOWED, foundPermissions.microphone) @@ -339,6 +369,7 @@ class GeckoSitePermissionsStorageTest { val onDiskPermissions = SitePermissions( origin = "mozilla.dev", localStorage = ALLOWED, + crossOriginStorageAccess = ALLOWED, location = ALLOWED, notification = ALLOWED, microphone = ALLOWED, @@ -354,6 +385,7 @@ class GeckoSitePermissionsStorageTest { geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION, value = VALUE_DENY), geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS, value = VALUE_DENY), geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE, value = VALUE_DENY), + geckoContentPermission(type = PERMISSION_STORAGE_ACCESS, value = VALUE_DENY), geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE, value = VALUE_DENY), geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE, value = VALUE_DENY) ).groupByType() @@ -363,6 +395,7 @@ class GeckoSitePermissionsStorageTest { assertEquals(BLOCKED, mergedPermissions.location) assertEquals(BLOCKED, mergedPermissions.notification) assertEquals(BLOCKED, mergedPermissions.localStorage) + assertEquals(BLOCKED, mergedPermissions.crossOriginStorageAccess) assertEquals(BLOCKED, mergedPermissions.mediaKeySystemAccess) assertEquals(ALLOWED, mergedPermissions.camera) assertEquals(ALLOWED, mergedPermissions.microphone) @@ -375,6 +408,7 @@ class GeckoSitePermissionsStorageTest { val onDiskPermissions = SitePermissions( origin = "mozilla.dev", localStorage = ALLOWED, + crossOriginStorageAccess = ALLOWED, location = ALLOWED, notification = ALLOWED, microphone = ALLOWED, @@ -394,6 +428,7 @@ class GeckoSitePermissionsStorageTest { assertEquals(BLOCKED, mergedPermissions.location) assertEquals(ALLOWED, mergedPermissions.notification) assertEquals(ALLOWED, mergedPermissions.localStorage) + assertEquals(ALLOWED, mergedPermissions.crossOriginStorageAccess) assertEquals(ALLOWED, mergedPermissions.mediaKeySystemAccess) assertEquals(ALLOWED, mergedPermissions.camera) assertEquals(ALLOWED, mergedPermissions.microphone) @@ -402,6 +437,40 @@ class GeckoSitePermissionsStorageTest { } @Test + fun `GIVEN different cross_origin_storage_access permissions WHEN mergePermissions is called THEN they are filtered by origin url`() { + val onDiskPermissions = SitePermissions( + origin = "mozilla.dev", + localStorage = ALLOWED, + crossOriginStorageAccess = NO_DECISION, + location = ALLOWED, + notification = ALLOWED, + microphone = ALLOWED, + camera = ALLOWED, + bluetooth = ALLOWED, + mediaKeySystemAccess = ALLOWED, + autoplayAudible = AutoplayStatus.ALLOWED, + autoplayInaudible = AutoplayStatus.ALLOWED, + savedAt = 0 + ) + val geckoPermission1 = geckoContentPermission( + type = PERMISSION_STORAGE_ACCESS, value = VALUE_DENY, thirdPartyOrigin = "mozilla.com" + ) + val geckoPermission2 = geckoContentPermission( + type = PERMISSION_STORAGE_ACCESS, value = VALUE_ALLOW, thirdPartyOrigin = "mozilla.dev" + ) + val geckoPermission3 = geckoContentPermission( + type = PERMISSION_STORAGE_ACCESS, value = VALUE_PROMPT, thirdPartyOrigin = "mozilla.org" + ) + + val mergedPermissions = geckoStorage.mergePermissions( + onDiskPermissions, + mapOf(PERMISSION_STORAGE_ACCESS to listOf(geckoPermission1, geckoPermission2, geckoPermission3)) + ) + + assertEquals(onDiskPermissions.copy(crossOriginStorageAccess = ALLOWED), mergedPermissions!!) + } + + @Test fun `WHEN removing a site permissions THEN permissions should be removed from the on disk and gecko storage`() = runBlockingTest { val onDiskPermissions = createNewSitePermission() @@ -420,6 +489,7 @@ class GeckoSitePermissionsStorageTest { geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION), geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS), geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE), + geckoContentPermission(type = PERMISSION_STORAGE_ACCESS), geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE), geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE), geckoContentPermission(type = PERMISSION_TRACKING) @@ -449,6 +519,7 @@ class GeckoSitePermissionsStorageTest { geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS), geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS), geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE), + geckoContentPermission(type = PERMISSION_STORAGE_ACCESS), geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE), geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE), geckoContentPermission(type = PERMISSION_TRACKING) @@ -458,7 +529,7 @@ class GeckoSitePermissionsStorageTest { geckoStorage.geckoTemporaryPermissions.addAll(geckoPermissions) - assertEquals(9, geckoStorage.geckoTemporaryPermissions.size) + assertEquals(10, geckoStorage.geckoTemporaryPermissions.size) geckoPermissions.forEach { geckoStorage.removeTemporaryPermissionIfAny(it) @@ -485,6 +556,7 @@ class GeckoSitePermissionsStorageTest { geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION), geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS), geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE), + geckoContentPermission(type = PERMISSION_STORAGE_ACCESS), geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE), geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE), geckoContentPermission(type = PERMISSION_TRACKING) @@ -506,6 +578,7 @@ class GeckoSitePermissionsStorageTest { val onDiskPermissions = SitePermissions( origin = "mozilla.dev", localStorage = ALLOWED, + crossOriginStorageAccess = ALLOWED, location = ALLOWED, notification = ALLOWED, microphone = ALLOWED, @@ -521,6 +594,7 @@ class GeckoSitePermissionsStorageTest { geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION, value = VALUE_DENY), geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS, value = VALUE_DENY), geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE, value = VALUE_DENY), + geckoContentPermission(type = PERMISSION_STORAGE_ACCESS, value = VALUE_DENY), geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE, value = VALUE_DENY), geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE, value = VALUE_DENY) ) @@ -533,6 +607,7 @@ class GeckoSitePermissionsStorageTest { assertEquals(BLOCKED, foundPermissions.location) assertEquals(BLOCKED, foundPermissions.notification) assertEquals(BLOCKED, foundPermissions.localStorage) + assertEquals(BLOCKED, foundPermissions.crossOriginStorageAccess) assertEquals(BLOCKED, foundPermissions.mediaKeySystemAccess) assertEquals(ALLOWED, foundPermissions.camera) assertEquals(ALLOWED, foundPermissions.microphone) @@ -550,13 +625,14 @@ class GeckoSitePermissionsStorageTest { geckoContentPermission(type = PERMISSION_DESKTOP_NOTIFICATION), geckoContentPermission(type = PERMISSION_MEDIA_KEY_SYSTEM_ACCESS), geckoContentPermission(type = PERMISSION_PERSISTENT_STORAGE), + geckoContentPermission(type = PERMISSION_STORAGE_ACCESS), geckoContentPermission(type = PERMISSION_AUTOPLAY_AUDIBLE), geckoContentPermission(type = PERMISSION_AUTOPLAY_INAUDIBLE) ) val filteredPermissions = geckoPermissions.filterNotTemporaryPermissions(temporary)!! - assertEquals(5, filteredPermissions.size) + assertEquals(6, filteredPermissions.size) assertFalse(filteredPermissions.any { it.permission == PERMISSION_GEOLOCATION }) } @@ -601,6 +677,7 @@ class GeckoSitePermissionsStorageTest { return SitePermissions( origin = "mozilla.dev", localStorage = ALLOWED, + crossOriginStorageAccess = BLOCKED, location = BLOCKED, notification = NO_DECISION, microphone = NO_DECISION, @@ -614,10 +691,12 @@ class GeckoSitePermissionsStorageTest { internal fun geckoContentPermission( uri: String = "mozilla.dev", type: Int, - value: Int = VALUE_PROMPT + value: Int = VALUE_PROMPT, + thirdPartyOrigin: String = "mozilla.dev" ): ContentPermission { val prompt: ContentPermission = mock() ReflectionUtils.setField(prompt, "uri", uri) + ReflectionUtils.setField(prompt, "thirdPartyOrigin", thirdPartyOrigin) ReflectionUtils.setField(prompt, "permission", type) ReflectionUtils.setField(prompt, "value", value) return prompt diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt index 8a9e845c482f..ea7dd55e50cb 100644 --- a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt +++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/PermissionRequest.kt @@ -82,6 +82,8 @@ sealed class Permission { data class ContentPersistentStorage(override val id: String? = "", override val desc: String? = "") : Permission() data class ContentMediaKeySystemAccess(override val id: String? = "", override val desc: String? = "") : Permission() + data class ContentCrossOriginStorageAccess(override val id: String? = "", override val desc: String? = "") : + Permission() data class AppCamera(override val id: String? = "", override val desc: String? = "") : Permission() data class AppAudio(override val id: String? = "", override val desc: String? = "") : Permission() diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissions.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissions.kt index a7ee953f3764..5fd5aa1343f3 100644 --- a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissions.kt +++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissions.kt @@ -26,6 +26,7 @@ data class SitePermissions( val autoplayAudible: AutoplayStatus = AutoplayStatus.BLOCKED, val autoplayInaudible: AutoplayStatus = AutoplayStatus.ALLOWED, val mediaKeySystemAccess: Status = NO_DECISION, + val crossOriginStorageAccess: Status = NO_DECISION, val savedAt: Long ) : Parcelable { enum class Status( @@ -88,6 +89,7 @@ data class SitePermissions( Permission.AUTOPLAY_AUDIBLE -> autoplayAudible.toStatus() Permission.AUTOPLAY_INAUDIBLE -> autoplayInaudible.toStatus() Permission.MEDIA_KEY_SYSTEM_ACCESS -> mediaKeySystemAccess + Permission.STORAGE_ACCESS -> crossOriginStorageAccess } } } diff --git a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissionsStorage.kt b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissionsStorage.kt index 5dfeb77b984a..0938dc8ea026 100644 --- a/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissionsStorage.kt +++ b/mobile/android/android-components/components/concept/engine/src/main/java/mozilla/components/concept/engine/permission/SitePermissionsStorage.kt @@ -69,6 +69,6 @@ interface SitePermissionsStorage { enum class Permission { MICROPHONE, BLUETOOTH, CAMERA, LOCAL_STORAGE, NOTIFICATION, LOCATION, AUTOPLAY_AUDIBLE, - AUTOPLAY_INAUDIBLE, MEDIA_KEY_SYSTEM_ACCESS + AUTOPLAY_INAUDIBLE, MEDIA_KEY_SYSTEM_ACCESS, STORAGE_ACCESS } } diff --git a/mobile/android/android-components/components/feature/sitepermissions/build.gradle b/mobile/android/android-components/components/feature/sitepermissions/build.gradle index e38b6bce05cf..1e98454601ff 100644 --- a/mobile/android/android-components/components/feature/sitepermissions/build.gradle +++ b/mobile/android/android-components/components/feature/sitepermissions/build.gradle @@ -56,6 +56,7 @@ dependencies { implementation project(':concept-engine') implementation project(':ui-icons') implementation project(':support-ktx') + implementation project(':feature-tabs') implementation Dependencies.kotlin_stdlib implementation Dependencies.kotlin_coroutines diff --git a/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/8.json b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/8.json new file mode 100644 index 000000000000..f0ad6daa6860 --- /dev/null +++ b/mobile/android/android-components/components/feature/sitepermissions/schemas/mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase/8.json @@ -0,0 +1,100 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "a4391f9f5b2a6448070c7f5cefb1b086", + "entities": [ + { + "tableName": "site_permissions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `location` INTEGER NOT NULL, `notification` INTEGER NOT NULL, `microphone` INTEGER NOT NULL, `camera` INTEGER NOT NULL, `bluetooth` INTEGER NOT NULL, `local_storage` INTEGER NOT NULL, `autoplay_audible` INTEGER NOT NULL, `autoplay_inaudible` INTEGER NOT NULL, `media_key_system_access` INTEGER NOT NULL, `cross_origin_storage_access` INTEGER NOT NULL, `saved_at` INTEGER NOT NULL, PRIMARY KEY(`origin`))", + "fields": [ + { + "fieldPath": "origin", + "columnName": "origin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notification", + "columnName": "notification", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "microphone", + "columnName": "microphone", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "camera", + "columnName": "camera", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bluetooth", + "columnName": "bluetooth", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localStorage", + "columnName": "local_storage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoplayAudible", + "columnName": "autoplay_audible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoplayInaudible", + "columnName": "autoplay_inaudible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaKeySystemAccess", + "columnName": "media_key_system_access", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "crossOriginStorageAccess", + "columnName": "cross_origin_storage_access", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "savedAt", + "columnName": "saved_at", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "origin" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a4391f9f5b2a6448070c7f5cefb1b086')" + ] + } +} \ No newline at end of file diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorage.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorage.kt index 278b9eddd7e4..95d41c46ae10 100644 --- a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorage.kt +++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/OnDiskSitePermissionsStorage.kt @@ -28,6 +28,7 @@ import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permi import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.MEDIA_KEY_SYSTEM_ACCESS import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.MICROPHONE import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.NOTIFICATION +import mozilla.components.concept.engine.permission.SitePermissionsStorage.Permission.STORAGE_ACCESS import mozilla.components.feature.sitepermissions.db.SitePermissionsDatabase import mozilla.components.feature.sitepermissions.db.toSitePermissionsEntity @@ -118,6 +119,7 @@ class OnDiskSitePermissionsStorage( map.putIfAllowed(AUTOPLAY_AUDIBLE, autoplayAudible.toStatus(), permission) map.putIfAllowed(AUTOPLAY_INAUDIBLE, autoplayInaudible.toStatus(), permission) map.putIfAllowed(MEDIA_KEY_SYSTEM_ACCESS, mediaKeySystemAccess, permission) + map.putIfAllowed(STORAGE_ACCESS, crossOriginStorageAccess, permission) } } return map diff --git a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt index dd99a878c911..766e1cca82b4 100644 --- a/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt +++ b/mobile/android/android-components/components/feature/sitepermissions/src/main/java/mozilla/components/feature/sitepermissions/SitePermissionsDialogFragment.kt @@ -28,15 +28,17 @@ internal const val KEY_TITLE = "KEY_TITLE" private const val KEY_DIALOG_GRAVITY = "KEY_DIALOG_GRAVITY" private const val KEY_DIALOG_WIDTH_MATCH_PARENT = "KEY_DIALOG_WIDTH_MATCH_PARENT" private const val KEY_TITLE_ICON = "KEY_TITLE_ICON" +private const val KEY_MESSAGE = "KEY_MESSAGE" private const val KEY_POSITIVE_BUTTON_BACKGROUND_COLOR = "KEY_POSITIVE_BUTTON_BACKGROUND_COLOR" private const val KEY_POSITIVE_BUTTON_TEXT_COLOR = "KEY_POSITIVE_BUTTON_TEXT_COLOR" +private const val KEY_SHOULD_SHOW_LEARN_MORE_LINK = "KEY_SHOULD_SHOW_LEARN_MORE_LINK" private const val KEY_SHOULD_SHOW_DO_NOT_ASK_AGAIN_CHECKBOX = "KEY_SHOULD_SHOW_DO_NOT_ASK_AGAIN_CHECKBOX" 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() { +internal open class SitePermissionsDialogFragment : AppCompatDialogFragment() { // Safe Arguments @@ -48,6 +50,8 @@ internal class SitePermissionsDialogFragment : AppCompatDialogFragment() { safeArguments.getString(KEY_TITLE, "") internal val icon get() = safeArguments.getInt(KEY_TITLE_ICON, DEFAULT_VALUE) + internal val message: String? get() = + safeArguments.getString(KEY_MESSAGE, null) internal val dialogGravity: Int get() = safeArguments.getInt(KEY_DIALOG_GRAVITY, DEFAULT_VALUE) @@ -62,6 +66,8 @@ internal class SitePermissionsDialogFragment : AppCompatDialogFragment() { internal val isNotificationRequest get() = safeArguments.getBoolean(KEY_IS_NOTIFICATION_REQUEST, false) + internal val shouldShowLearnMoreLink: Boolean get() = + safeArguments.getBoolean(KEY_SHOULD_SHOW_LEARN_MORE_LINK, false) internal val shouldShowDoNotAskAgainCheckBox: Boolean get() = safeArguments.getBoolean(KEY_SHOULD_SHOW_DO_NOT_ASK_AGAIN_CHECKBOX, true) internal val shouldPreselectDoNotAskAgainCheckBox: Boolean get() = @@ -129,6 +135,22 @@ internal class SitePermissionsDialogFragment : AppCompatDialogFragment() { rootView.findViewById(R.id.title).text = title rootView.findViewById(R.id.icon).setImageResource(icon) + message?.let { + rootView.findViewById(R.id.message).apply { + visibility = VISIBLE + text = it + } + } + if (shouldShowLearnMoreLink) { + rootView.findViewById(R.id.learn_more).apply { + visibility = VISIBLE + isLongClickable = false + setOnClickListener { + dismiss() + feature?.onLearnMorePress(permissionRequestId, sessionId) + } + } + } val positiveButton = rootView.findViewById